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.
3.0 KiB
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:
- Token verification: Is this JWT valid and non-revoked? Who issued it?
- Identity enrichment: Given the verified UID, what role and tenant does this user have in the application?
- 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. Storesuidandclaimsin context. Returns 401 on missing or invalid token. Routes matchingpublicPaths(glob patterns) are bypassed entirely.EnrichmentMiddleware(enricher, opts...): Readsuid+claimsfrom context. CallsIdentityEnricher.Enrich(app-provided). Storesrbac.Identityin context. Returns 401 if no UID is present (i.e.AuthMiddlewarewas not in the chain). Optionally reads a tenant ID from a configurable request header.AuthzMiddleware(provider, resource, required): Readsrbac.Identityfrom context. CallsPermissionProvider.ResolveMaskto 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.
AuthMiddlewaretests do not need a real user store;EnrichmentMiddlewaretests do not need a Firebase client;AuthzMiddlewaretests do not need either. - Routes can apply only the subset they need. A webhook endpoint that only verifies
token validity can use
AuthMiddlewarealone. A read-only endpoint can useAuthMiddleware+EnrichmentMiddlewarewithoutAuthzMiddleware. - The order constraint is enforced by dependency:
EnrichmentMiddlewaredepends on values written byAuthMiddleware;AuthzMiddlewaredepends on values written byEnrichmentMiddleware. Incorrect ordering produces 401 (missing UID or identity) rather than a silent bug. publicPathsinAuthMiddlewareusespath.Matchfor glob matching. Patterns like/health,/metrics/*can be listed to skip token verification entirely.IdentityEnricherandPermissionProviderare interfaces injected at construction time, making the middleware fully testable without a real Firebase connection or database.