Files
httpauth-firebase/docs/adr/ADR-003-composable-middleware-stack.md

61 lines
3.0 KiB
Markdown
Raw Normal View History

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