Files
httpauth-firebase/docs/adr/ADR-003-composable-middleware-stack.md
Rene Nochebuena d1de096c72 docs(httpauth-firebase): fix rbac tier reference from 1 to 0
rbac is a Tier 0 module (no micro-lib dependencies). The dependency line
incorrectly cited it as Tier 1. The module's own tier (4) is unchanged —
it remains the auth layer above the transport infrastructure.
2026-03-19 13:44:45 +00:00

3.0 KiB

ADR-003: Composable Three-Middleware Auth Stack

Status: Accepted Date: 2026-03-18

Context

HTTP authentication and authorisation involve at least three distinct concerns:

  1. Token verification: Is this JWT valid and non-revoked? Who issued it?
  2. Identity enrichment: Given the verified UID, what role and tenant does this user have in the application?
  3. Permission enforcement: Does this identity have the required permission on this resource?

A single "auth middleware" that handles all three concerns is a common pattern, but it creates problems:

  • Enrichment requires a user-store lookup. Not every route needs enrichment (e.g. public endpoints that only verify the token is structurally valid).
  • Permission enforcement is per-resource and per-route. A single monolithic middleware cannot vary by route without routing logic inside it.
  • Testing each concern in isolation becomes difficult when they are combined.

Decision

Three separate middleware functions are provided:

  • AuthMiddleware(verifier, publicPaths): Verifies the Bearer token using Firebase. Stores uid and claims in context. Returns 401 on missing or invalid token. Routes matching publicPaths (glob patterns) are bypassed entirely.
  • EnrichmentMiddleware(enricher, opts...): Reads uid + claims from context. Calls IdentityEnricher.Enrich (app-provided). Stores rbac.Identity in context. Returns 401 if no UID is present (i.e. AuthMiddleware was not in the chain). Optionally reads a tenant ID from a configurable request header.
  • AuthzMiddleware(provider, resource, required): Reads rbac.Identity from context. Calls PermissionProvider.ResolveMask to get the permission mask for the identity's UID on the named resource. Returns 401 if no identity is present, 403 if the mask does not include the required permission.

Each function returns a func(http.Handler) http.Handler, composable with any standard middleware stack.

Consequences

  • Each middleware can be tested in isolation. AuthMiddleware tests do not need a real user store; EnrichmentMiddleware tests do not need a Firebase client; AuthzMiddleware tests do not need either.
  • Routes can apply only the subset they need. A webhook endpoint that only verifies token validity can use AuthMiddleware alone. A read-only endpoint can use AuthMiddleware + EnrichmentMiddleware without AuthzMiddleware.
  • The order constraint is enforced by dependency: EnrichmentMiddleware depends on values written by AuthMiddleware; AuthzMiddleware depends on values written by EnrichmentMiddleware. Incorrect ordering produces 401 (missing UID or identity) rather than a silent bug.
  • publicPaths in AuthMiddleware uses path.Match for glob matching. Patterns like /health, /metrics/* can be listed to skip token verification entirely.
  • IdentityEnricher and PermissionProvider are interfaces injected at construction time, making the middleware fully testable without a real Firebase connection or database.