Rene Nochebuena 3c6636f905 feat(httpauth)!: promote to v1.0.0 — add ChainPermissionProvider, bump rbac to v1.0.0
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.
2026-05-07 23:08:38 -06:00

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.

Description
No description provided
Readme MIT 51 KiB
2026-05-07 23:09:23 -06:00
Languages
Go 100%