feat(httpauth): initial release — provider-agnostic HTTP auth middleware
Provides SetTokenData for upstream AuthMiddleware implementations, EnrichmentMiddleware and AuthzMiddleware compatible with any provider that calls SetTokenData, ClaimsPermissionProvider for JWT-embedded permissions, and CachedPermissionProvider for TTL-backed runtime resolution via any Cache implementation.
This commit is contained in:
111
CLAUDE.md
Normal file
111
CLAUDE.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# httpauth
|
||||
|
||||
Provider-agnostic HTTP middleware for identity enrichment and RBAC authorization.
|
||||
|
||||
## Purpose
|
||||
|
||||
`httpauth` is the shared foundation for all `httpauth-*` provider modules. It provides
|
||||
`EnrichmentMiddleware`, `AuthzMiddleware`, two `rbac.PermissionProvider` implementations
|
||||
(`ClaimsPermissionProvider` and `CachedPermissionProvider`), and `SetTokenData` — the
|
||||
bridge between a provider-specific `AuthMiddleware` and the rest of the auth stack.
|
||||
|
||||
Any `AuthMiddleware` that calls `SetTokenData` after token verification is compatible.
|
||||
Downstream code reads identity exclusively via `rbac.FromContext` — no provider type
|
||||
leaks past the middleware boundary.
|
||||
|
||||
## Tier & Dependencies
|
||||
|
||||
**Tier:** 3 (transport auth layer; depends only on Tier 0 `rbac`; no external SDK)
|
||||
**Module:** `code.nochebuena.dev/go/httpauth`
|
||||
**Direct imports:** `code.nochebuena.dev/go/rbac` only
|
||||
|
||||
`httpauth` does not import `logz`, `httpmw`, `httputil`, Firebase, or any JWT library.
|
||||
It has no logger parameter — errors are returned as HTTP responses.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
- **`rbac.PermissionProvider` without redefinition:** `AuthzMiddleware` accepts
|
||||
`rbac.PermissionProvider` directly. `rbac` is the single source of truth for this
|
||||
interface. Provider-specific modules (e.g. `httpauth-firebase`) previously defined
|
||||
their own `PermissionProvider` locally — that duplication is removed.
|
||||
- **`SetTokenData` as the integration contract:** Provider-specific `AuthMiddleware`
|
||||
implementations call `SetTokenData(ctx, uid, claims)` after verifying the token.
|
||||
The context keys are unexported typed structs. `EnrichmentMiddleware` reads them
|
||||
via the unexported helpers `getUID` and `getClaims` in the same package.
|
||||
- **Two permission strategies:** `ClaimsPermissionProvider` (JWT-embedded, no DB call)
|
||||
and `CachedPermissionProvider` (TTL-backed runtime resolution) are first-class
|
||||
implementations. Choose based on token size and revocation requirements.
|
||||
- **Cache falls through on error:** `CachedPermissionProvider` treats cache errors as
|
||||
misses — a cache outage degrades gracefully to the inner provider.
|
||||
|
||||
## Patterns
|
||||
|
||||
**Full stack with self-issued JWT:**
|
||||
|
||||
```go
|
||||
r.Use(jwtauth.AuthMiddleware(signer, publicPaths, nil))
|
||||
r.Use(httpauth.EnrichmentMiddleware(myEnricher, httpauth.WithTenantHeader("X-Tenant-ID")))
|
||||
|
||||
// Simple app — permissions embedded in JWT:
|
||||
claimsProvider := httpauth.NewClaimsPermissionProvider("permisos")
|
||||
r.With(httpauth.AuthzMiddleware(claimsProvider, "usuarios", rbac.Permission(1))).
|
||||
Get("/usuarios", handler)
|
||||
|
||||
// Complex app — runtime resolution with cache:
|
||||
cachedProvider := httpauth.NewCachedPermissionProvider(dbProvider, valkeyCache, 5*time.Minute)
|
||||
r.With(httpauth.AuthzMiddleware(cachedProvider, "usuarios", rbac.Permission(1))).
|
||||
Get("/usuarios", handler)
|
||||
```
|
||||
|
||||
**Provider-specific AuthMiddleware calling SetTokenData:**
|
||||
|
||||
```go
|
||||
// Inside httpauth-jwt or httpauth-firebase AuthMiddleware, after token verification:
|
||||
ctx := httpauth.SetTokenData(r.Context(), verified.UID, verified.Claims)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
```
|
||||
|
||||
**Reading identity in a handler:**
|
||||
|
||||
```go
|
||||
identity, ok := rbac.FromContext(r.Context())
|
||||
if !ok {
|
||||
// should not happen if EnrichmentMiddleware is in the chain
|
||||
}
|
||||
```
|
||||
|
||||
**Implementing Cache (e.g. with Valkey):**
|
||||
|
||||
```go
|
||||
type valkeyCache struct{ client valkey.Client }
|
||||
|
||||
func (c *valkeyCache) Get(ctx context.Context, key string) (int64, bool, error) { ... }
|
||||
func (c *valkeyCache) Set(ctx context.Context, key string, val int64, ttl time.Duration) error { ... }
|
||||
```
|
||||
|
||||
**Cache key for manual invalidation:** `rbac:{uid}:{resource}`
|
||||
|
||||
## What to Avoid
|
||||
|
||||
- Do not call `SetTokenData` from application or domain layer code. It is the
|
||||
exclusive responsibility of provider-specific `AuthMiddleware` implementations.
|
||||
- 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 import `httpauth` from service or domain layers. It is a transport package.
|
||||
- Do not define a local `PermissionProvider` interface in provider modules that import
|
||||
this package — use `rbac.PermissionProvider` directly.
|
||||
|
||||
## Testing Notes
|
||||
|
||||
- `compliance_test.go` verifies at compile time that mock types satisfy `IdentityEnricher`
|
||||
and `Cache`, and that `rbac.PermissionProvider` is satisfied by the two built-in
|
||||
provider implementations.
|
||||
- `EnrichmentMiddleware` tests use `injectTokenData(uid, claims, next)` — a helper
|
||||
that calls `SetTokenData` to bypass a real upstream `AuthMiddleware`.
|
||||
- `AuthzMiddleware` tests pre-populate context with `rbac.SetInContext` — no enrichment
|
||||
middleware needed.
|
||||
- `ClaimsPermissionProvider` tests exercise both `float64` (JSON decode) and `int64`
|
||||
paths for the mask value.
|
||||
- `CachedPermissionProvider` tests exercise cache hit, miss, cache error fallthrough,
|
||||
and inner provider error propagation.
|
||||
Reference in New Issue
Block a user