Rene Nochebuena 601e6a5144 fix(httpauth): wildcard resource fallback, json.Number precision, xerrors JSON error bodies (#1)
ClaimsPermissionProvider.ResolveMask now falls back to the "*" resource key when the
exact resource is absent in the JWT masks claim. Specific resource takes precedence;
wildcard is the fallback. Enables ADMIN wildcard masks ({"*": MaxInt64}) to pass every
endpoint guard without per-resource entries.

Add json.Number handling alongside existing int64/float64 paths. json.Number is
produced by jwt.WithJSONNumber() (httpauth-jwt) and preserves math.MaxInt64 exactly —
float64 would round to 2^63 and overflow on cast back to int64.

Export WriteJSONError(w, status, code, message) — shared JSON error helper for all
httpauth-* provider packages. Ensures a consistent {"code":"...","message":"..."} body
and Content-Type: application/json across the full middleware stack.

AuthzMiddleware and EnrichmentMiddleware now use WriteJSONError with xerrors code
constants (UNAUTHENTICATED, PERMISSION_DENIED, INTERNAL) instead of http.Error which
writes text/plain. AuthzMiddleware also fixes a wrong code string: UNAUTHORIZED →
UNAUTHENTICATED (stable gRPC-aligned name, matching xerrors.ErrUnauthorized).

Add xerrors v1.0.1 as a direct dependency for the code constants.

Reviewed-on: #1
Co-authored-by: Rene Nochebuena Guerrero <rene@nochebuena.dev>
Co-committed-by: Rene Nochebuena Guerrero <rene@nochebuena.dev>
2026-05-18 14:15:12 -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 64 KiB
2026-05-18 14:16:42 -06:00
Languages
Go 100%