Add NewChainPermissionProvider: tries each rbac.PermissionProvider in order, returns the first non-zero mask, propagates errors immediately. Primary use case: ClaimsPermissionProvider (JWT fast-path, no DB call) chained with CachedPermissionProvider (DB fallback). Bump rbac dependency to v1.0.0. API committed as stable.
httpauth
Provider-agnostic HTTP middleware for identity enrichment and RBAC authorization.
Overview
Three composable func(http.Handler) http.Handler middleware functions and two rbac.PermissionProvider implementations:
| Component | Responsibility |
|---|---|
EnrichmentMiddleware |
Calls app-provided IdentityEnricher; stores rbac.Identity in context |
AuthzMiddleware |
Resolves permission mask via rbac.PermissionProvider; gates request |
ClaimsPermissionProvider |
Reads pre-computed masks from JWT claims — no DB call |
CachedPermissionProvider |
Wraps any provider with a TTL cache; falls through on miss or error |
SetTokenData |
Injects uid + claims from any verified token into the request context |
Any upstream AuthMiddleware that calls SetTokenData is compatible — Firebase, self-issued JWT, API key, etc.
Installation
require code.nochebuena.dev/go/httpauth v0.1.0
Usage
With JWT-embedded permissions (simple apps)
// Auth middleware (e.g. httpauth-jwt) calls httpauth.SetTokenData after verification.
// JWT claims include: { "permisos": { "usuarios": 515, "roles": 6 } }
r.Use(jwtauth.AuthMiddleware(signer, publicPaths, nil))
r.Use(httpauth.EnrichmentMiddleware(myEnricher))
claimsProvider := httpauth.NewClaimsPermissionProvider("permisos")
r.With(httpauth.AuthzMiddleware(claimsProvider, "usuarios", rbac.Permission(1))).
Get("/usuarios", handler)
With runtime resolution + cache (complex apps)
r.Use(jwtauth.AuthMiddleware(signer, publicPaths, nil))
r.Use(httpauth.EnrichmentMiddleware(myEnricher, httpauth.WithTenantHeader("X-Tenant-ID")))
dbProvider := myapp.NewDBPermissionProvider(db)
cachedProvider := httpauth.NewCachedPermissionProvider(dbProvider, valkeyCache, 5*time.Minute)
r.With(httpauth.AuthzMiddleware(cachedProvider, "usuarios", rbac.Permission(1))).
Get("/usuarios", handler)
Interfaces
IdentityEnricher
type IdentityEnricher interface {
Enrich(ctx context.Context, uid string, claims map[string]any) (rbac.Identity, error)
}
Implement in your application to load user data and return an rbac.Identity.
Cache
type Cache interface {
Get(ctx context.Context, key string) (int64, bool, error)
Set(ctx context.Context, key string, value int64, ttl time.Duration) error
}
Implement with Valkey, Redis, or any in-memory store. Cache keys follow the format rbac:{uid}:{resource}.
rbac.PermissionProvider (from the rbac package)
type PermissionProvider interface {
ResolveMask(ctx context.Context, uid, resource string) (rbac.PermissionMask, error)
}
AuthzMiddleware accepts any implementation — ClaimsPermissionProvider, CachedPermissionProvider, or your own.
SetTokenData
func SetTokenData(ctx context.Context, uid string, claims map[string]any) context.Context
Called by provider-specific AuthMiddleware implementations after token verification. EnrichmentMiddleware reads the injected values automatically.
Options
| Option | Description |
|---|---|
WithTenantHeader(header) |
Reads TenantID from the named request header. If absent, TenantID remains "". |
HTTP status codes
| Condition | Status |
|---|---|
| No uid in context (EnrichmentMiddleware) | 401 |
| Enricher error | 500 |
No rbac.Identity in context (AuthzMiddleware) |
401 |
| Permission denied or provider error | 403 |
Cache key format
CachedPermissionProvider uses rbac:{uid}:{resource} as the cache key. To invalidate manually, delete the key directly via your Cache implementation.