112 lines
4.8 KiB
Markdown
112 lines
4.8 KiB
Markdown
|
|
# 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.
|