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.
This commit is contained in:
2026-03-19 13:44:45 +00:00
commit d1de096c72
17 changed files with 1188 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
# ADR-001: Provider-Specific Module Naming (httpauth-firebase)
**Status:** Accepted
**Date:** 2026-03-18
## Context
An auth middleware module originally named `httpauth` was designed to be
provider-agnostic: it defined a `TokenVerifier` interface and duck-typed
`firebase.Client` so the module never imported Firebase directly. The intent was
that any JWT provider could be supported by implementing `TokenVerifier`.
Under global ADR-001, framework modules import their named dependencies directly
rather than duck-typing them away. An auth module for Firebase should import
`firebase.google.com/go/v4/auth` directly, since that is its explicit purpose.
This raises the naming question: if the module imports Firebase, should it still be
called `httpauth`?
## Decision
The module is named **`httpauth-firebase`**:
- Go module path: `code.nochebuena.dev/go/httpauth-firebase`
- Directory: `micro-lib/httpauth-firebase/`
- Package name: `httpauth` (short import alias; the module path carries the provider name)
A different auth provider would live in `httpauth-auth0`, `httpauth-jwks`, etc.
All `httpauth-*` modules converge on the same output contract: `rbac.Identity` stored
in context via `rbac.SetInContext`.
The `TokenVerifier` interface is retained **solely for unit-test mockability**
not for provider abstraction. In production, `*auth.Client` from
`firebase.google.com/go/v4/auth` satisfies `TokenVerifier` directly without any
adapter.
## Consequences
- The module name is honest: scanning the module list, a developer immediately knows
this is Firebase-specific and not a generic auth abstraction.
- Other auth providers are accommodated by creating sibling modules
(`httpauth-auth0`, etc.) with identical output contracts, not by implementing an
interface in this module.
- The `TokenVerifier` interface remains exported so test code outside the package can
implement it (`mockVerifier` in `compliance_test.go`). Its presence does not imply
that swapping providers at runtime is a supported pattern.
- Applications that switch from Firebase to another provider replace the module
import; the rest of the auth stack (`EnrichmentMiddleware`, `AuthzMiddleware`) and
all business logic remain unchanged because they depend on `rbac.Identity`, not on
Firebase types.

View File

@@ -0,0 +1,55 @@
# ADR-002: rbac.Identity as the Output Contract
**Status:** Accepted
**Date:** 2026-03-18
## Context
`AuthMiddleware` verifies a Firebase JWT and extracts a UID and claims map from the
decoded token. Downstream code needs a richer identity: application-specific role,
tenant, display name, email. Several design options exist:
1. Expose Firebase token fields (`auth.Token`) directly in context — ties all
downstream code to Firebase types.
2. Define a custom identity struct in `httpauth-firebase` — decouples from Firebase
but creates a module-specific contract that other `httpauth-*` modules cannot share.
3. Use `rbac.Identity` as the shared identity type — all `httpauth-*` modules
produce the same type; downstream code and `AuthzMiddleware` depend on `rbac`
only, not on any Firebase types.
## Decision
`EnrichmentMiddleware` calls `rbac.SetInContext(ctx, identity)` to store the
enriched identity. `AuthzMiddleware` reads it with `rbac.FromContext(ctx)`.
Downstream business logic and service handlers use `rbac.FromContext` directly —
they never import `httpauth-firebase`.
The flow is:
1. `AuthMiddleware` (this module): verifies Firebase JWT → stores `uid` + `claims`
in context under unexported keys local to this package.
2. `EnrichmentMiddleware` (this module): reads `uid` + `claims` → calls
`IdentityEnricher.Enrich` → stores `rbac.Identity` via `rbac.SetInContext`.
3. `AuthzMiddleware` (this module): reads `rbac.Identity` via `rbac.FromContext`
calls `PermissionProvider.ResolveMask` → allows or rejects.
The intermediate `uid` + `claims` context values are stored under unexported typed
keys (`ctxUIDKey{}`, `ctxClaimsKey{}`). They are internal to `httpauth-firebase`
and not part of the public API.
## Consequences
- Business logic and service layers only need to import `rbac` to read the caller's
identity. They have no knowledge of Firebase, JWTs, or token claims.
- Switching from Firebase to another provider (e.g. Auth0) requires replacing the
`AuthMiddleware` module. `EnrichmentMiddleware`, `AuthzMiddleware`, and all
downstream code remain unchanged because they operate on `rbac.Identity`.
- `IdentityEnricher` is the application's extension point: it receives the Firebase
UID and raw claims and returns a fully populated `rbac.Identity` with role and
tenant. This is the only place where app-specific user store queries should occur.
- `rbac.Identity.WithTenant(tenantID)` is called in `EnrichmentMiddleware` if a
tenant header is configured. The base identity from `Enrich` is immutable; a new
value is returned.
- Global ADR-003 (context helpers live with data owners) is applied here: `rbac`
owns `SetInContext` and `FromContext` because `rbac.Identity` is a RBAC concern,
not an auth transport concern.

View File

@@ -0,0 +1,60 @@
# 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.