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:
49
docs/adr/ADR-001-provider-specific-naming.md
Normal file
49
docs/adr/ADR-001-provider-specific-naming.md
Normal 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.
|
||||
55
docs/adr/ADR-002-rbac-identity-output-contract.md
Normal file
55
docs/adr/ADR-002-rbac-identity-output-contract.md
Normal 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.
|
||||
60
docs/adr/ADR-003-composable-middleware-stack.md
Normal file
60
docs/adr/ADR-003-composable-middleware-stack.md
Normal 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.
|
||||
Reference in New Issue
Block a user