# 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.