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:
124
CLAUDE.md
Normal file
124
CLAUDE.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# httpauth-firebase
|
||||
|
||||
Firebase-backed HTTP middleware for authentication, identity enrichment, and
|
||||
role-based access control.
|
||||
|
||||
## Purpose
|
||||
|
||||
`httpauth-firebase` provides three composable `net/http` middleware functions that
|
||||
implement the full auth stack: JWT verification via Firebase, app-specific identity
|
||||
enrichment via a caller-supplied `IdentityEnricher`, and permission enforcement via
|
||||
`rbac`. The output contract is always `rbac.Identity` — downstream code and business
|
||||
logic are decoupled from Firebase types entirely.
|
||||
|
||||
## Tier & Dependencies
|
||||
|
||||
**Tier:** 4 (transport auth layer; depends on Tier 0 `rbac` and external Firebase SDK)
|
||||
**Module:** `code.nochebuena.dev/go/httpauth-firebase`
|
||||
**Direct imports:** `code.nochebuena.dev/go/rbac`, `firebase.google.com/go/v4/auth`
|
||||
|
||||
`httpauth-firebase` does not import `logz`, `httpmw`, or `httputil`. It has no
|
||||
logger parameter — errors are returned as HTTP responses, not logged here.
|
||||
|
||||
## 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.
|
||||
|
||||
## Patterns
|
||||
|
||||
**Full stack (most routes):**
|
||||
|
||||
```go
|
||||
r.Use(httpauth.AuthMiddleware(firebaseAuthClient, []string{"/health", "/metrics/*"}))
|
||||
r.Use(httpauth.EnrichmentMiddleware(userEnricher, httpauth.WithTenantHeader("X-Tenant-ID")))
|
||||
|
||||
// Per-route RBAC:
|
||||
r.With(httpauth.AuthzMiddleware(permProvider, "orders", rbac.Write)).
|
||||
Post("/orders", httputil.Handle(v, svc.CreateOrder))
|
||||
```
|
||||
|
||||
**Token-only (webhook verification):**
|
||||
|
||||
```go
|
||||
r.Use(httpauth.AuthMiddleware(firebaseAuthClient, nil))
|
||||
// No enrichment, no authz — just verify the token is valid
|
||||
```
|
||||
|
||||
**Reading identity in a handler:**
|
||||
|
||||
```go
|
||||
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)
|
||||
```
|
||||
|
||||
**Public paths (bypass token check):**
|
||||
|
||||
```go
|
||||
// path.Match patterns — supports * wildcard
|
||||
publicPaths := []string{"/health", "/ready", "/metrics/*"}
|
||||
r.Use(httpauth.AuthMiddleware(firebaseAuthClient, publicPaths))
|
||||
```
|
||||
|
||||
**Interfaces the application must implement:**
|
||||
|
||||
```go
|
||||
// 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
|
||||
}
|
||||
```
|
||||
|
||||
## What to Avoid
|
||||
|
||||
- Do not use `AuthMiddleware` alone and assume the request is fully authorised.
|
||||
Token verification only confirms the token is valid; it does not enforce
|
||||
application-level roles or permissions.
|
||||
- Do not put `AuthzMiddleware` before `EnrichmentMiddleware` in the chain.
|
||||
`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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Testing Notes
|
||||
|
||||
- `compliance_test.go` verifies at compile time that `*mockVerifier` satisfies
|
||||
`TokenVerifier`, `*mockEnricher` satisfies `IdentityEnricher`, and `*mockProvider`
|
||||
satisfies `PermissionProvider`.
|
||||
- `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.
|
||||
- Use `httptest.NewRecorder()` and `httptest.NewRequest()` for all HTTP-level tests.
|
||||
Reference in New Issue
Block a user