refactor(httpauth-firebase)!: delegate enrichment and authz to httpauth v0.1.0

EnrichmentMiddleware, AuthzMiddleware, IdentityEnricher, PermissionProvider,
and related types are removed from this module. They now live in
code.nochebuena.dev/go/httpauth, the provider-agnostic middleware layer.

AuthMiddleware is updated to call httpauth.SetTokenData, fulfilling the
integration contract between provider-specific auth and generic middleware.
This module now has a single responsibility: Firebase JWT verification.

BREAKING CHANGE: IdentityEnricher, PermissionProvider, EnrichmentMiddleware,
AuthzMiddleware, and WithTenantHeader are no longer exported from this package.
Import code.nochebuena.dev/go/httpauth for those identifiers.
This commit is contained in:
2026-05-07 21:57:01 -06:00
parent d1de096c72
commit 2c90fe22bf
10 changed files with 65 additions and 358 deletions

View File

@@ -13,38 +13,47 @@ logic are decoupled from Firebase types entirely.
## Tier & Dependencies
**Tier:** 4 (transport auth layer; depends on Tier 0 `rbac` and external Firebase SDK)
**Tier:** 4 (transport auth layer; depends on Tier 3 `httpauth` and external Firebase SDK)
**Module:** `code.nochebuena.dev/go/httpauth-firebase`
**Direct imports:** `code.nochebuena.dev/go/rbac`, `firebase.google.com/go/v4/auth`
**Direct imports:** `code.nochebuena.dev/go/httpauth`, `firebase.google.com/go/v4/auth`
**Transitive:** `code.nochebuena.dev/go/rbac` (indirect, pulled in by `httpauth`)
`httpauth-firebase` does not import `logz`, `httpmw`, or `httputil`. It has no
logger parameter — errors are returned as HTTP responses, not logged here.
`EnrichmentMiddleware`, `AuthzMiddleware`, `IdentityEnricher`, `WithTenantHeader`,
`Cache`, and permission providers all live in `code.nochebuena.dev/go/httpauth`.
This module only provides `AuthMiddleware` and the `TokenVerifier` interface.
## Key Design Decisions
- **Provider-specific naming** (ADR-001): The module is named `httpauth-firebase`
because it imports Firebase directly. Other providers live in sibling modules
(`httpauth-auth0`, etc.). All converge on `rbac.Identity`. The `TokenVerifier`
interface is retained for unit-test mockability only.
- **rbac.Identity as output contract** (ADR-002): `EnrichmentMiddleware` stores the
final identity with `rbac.SetInContext`; `AuthzMiddleware` reads it with
`rbac.FromContext`. Downstream handlers only need to import `rbac`. Global ADR-003
applies: `rbac` owns its context helpers.
- **Composable three-middleware stack** (ADR-003): `AuthMiddleware`,
`EnrichmentMiddleware`, and `AuthzMiddleware` are separate functions. Each can be
applied independently; each returns 401 if its upstream dependency is missing from
context.
(`httpauth-jwt`, etc.). All converge on `rbac.Identity` via the shared `httpauth`
contract. The `TokenVerifier` interface is retained for unit-test mockability only.
- **Integration contract via `httpauth.SetTokenData`**: `AuthMiddleware` stores
`uid` and `claims` using `httpauth.SetTokenData`, which writes to context keys
owned by the `httpauth` package. `EnrichmentMiddleware` (from `httpauth`) reads
them. This is the explicit integration point between provider-specific and
provider-agnostic middleware.
- **Single responsibility**: This module only verifies Firebase tokens. Identity
enrichment, RBAC, and caching all live in `code.nochebuena.dev/go/httpauth`.
## Patterns
**Full stack (most routes):**
```go
import (
httpauth "code.nochebuena.dev/go/httpauth-firebase"
httpauthmw "code.nochebuena.dev/go/httpauth"
)
r.Use(httpauth.AuthMiddleware(firebaseAuthClient, []string{"/health", "/metrics/*"}))
r.Use(httpauth.EnrichmentMiddleware(userEnricher, httpauth.WithTenantHeader("X-Tenant-ID")))
r.Use(httpauthmw.EnrichmentMiddleware(userEnricher, httpauthmw.WithTenantHeader("X-Tenant-ID")))
// Per-route RBAC:
r.With(httpauth.AuthzMiddleware(permProvider, "orders", rbac.Write)).
r.With(httpauthmw.AuthzMiddleware(permProvider, "orders", rbac.Write)).
Post("/orders", httputil.Handle(v, svc.CreateOrder))
```
@@ -62,7 +71,7 @@ identity, ok := rbac.FromContext(r.Context())
if !ok {
// should not happen if EnrichmentMiddleware is in the chain
}
fmt.Println(identity.UID, identity.Role, identity.TenantID)
fmt.Println(identity.UID, identity.DisplayName, identity.TenantID)
```
**Public paths (bypass token check):**
@@ -73,21 +82,15 @@ publicPaths := []string{"/health", "/ready", "/metrics/*"}
r.Use(httpauth.AuthMiddleware(firebaseAuthClient, publicPaths))
```
**Interfaces the application must implement:**
**Interfaces the application must implement (defined in httpauth, not here):**
```go
// IdentityEnricher: called by EnrichmentMiddleware
// httpauthmw.IdentityEnricher: called by EnrichmentMiddleware
type MyEnricher struct{ db *sql.DB }
func (e *MyEnricher) Enrich(ctx context.Context, uid string, claims map[string]any) (rbac.Identity, error) {
user, err := e.db.LookupUser(ctx, uid)
if err != nil { return rbac.Identity{}, err }
return rbac.NewIdentity(uid, user.DisplayName, user.Email).WithRole(user.Role), nil
}
// PermissionProvider: called by AuthzMiddleware
type MyPermProvider struct{}
func (p *MyPermProvider) ResolveMask(ctx context.Context, uid, resource string) (rbac.PermissionMask, error) {
return rbac.ReadWrite, nil
return rbac.NewIdentity(uid, user.DisplayName, user.Email), nil
}
```
@@ -100,25 +103,24 @@ func (p *MyPermProvider) ResolveMask(ctx context.Context, uid, resource string)
`AuthzMiddleware` reads `rbac.Identity` from context; if enrichment has not run,
it will return 401.
- Do not read `firebase.Token` fields directly in business logic. The token UID and
claims are stored under unexported context keys and are not part of the public API.
Use `rbac.FromContext` to read the enriched identity.
claims are stored in context by `httpauth.SetTokenData` under keys owned by the
`httpauth` package. Use `rbac.FromContext` to read the enriched identity.
- Do not store application-specific data in Firebase custom claims and bypass the
`IdentityEnricher`. Claims are read by `EnrichmentMiddleware` and passed to
`Enrich`, but the enricher is the correct place to resolve application identity
not raw Firebase claims spread across handlers.
`IdentityEnricher`. Claims are read by `EnrichmentMiddleware` (from `httpauth`)
and passed to `Enrich` the enricher is the correct place to resolve identity.
- Do not import `httpauth-firebase` from service or domain layers. It is a transport
package. Dependency should flow inward: handler → service, never service →
handler/middleware.
- Do not redefine `IdentityEnricher`, `Cache`, or permission providers here. They
live in `code.nochebuena.dev/go/httpauth`.
## Testing Notes
- `compliance_test.go` verifies at compile time that `*mockVerifier` satisfies
`TokenVerifier`, `*mockEnricher` satisfies `IdentityEnricher`, and `*mockProvider`
satisfies `PermissionProvider`.
`TokenVerifier`. Interface checks for `IdentityEnricher`, `Cache`, and permission
providers live in `code.nochebuena.dev/go/httpauth`'s own compliance test.
- `AuthMiddleware` can be tested by injecting a `mockVerifier` that returns a
controlled `*auth.Token`. No real Firebase project is needed.
- `EnrichmentMiddleware` can be tested by pre-populating the request context with
`uid` via an upstream `AuthMiddleware` using a mock verifier.
- `AuthzMiddleware` can be tested by pre-populating the request context with an
`rbac.Identity` via `rbac.SetInContext` — no auth or enrichment middleware needed.
- `EnrichmentMiddleware` and `AuthzMiddleware` tests belong in the `httpauth` module,
not here.
- Use `httptest.NewRecorder()` and `httptest.NewRequest()` for all HTTP-level tests.