Files
httpauth/CHANGELOG.md
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

6.3 KiB

Changelog

All notable changes to this module will be documented in this file.

The format is based on Keep a Changelog, and this module adheres to Semantic Versioning.

1.0.2 — 2026-05-18

Added

  • ClaimsPermissionProvider.ResolveMask now supports a wildcard resource key "*" as a fallback when the requested resource is not present in the JWT masks claim. Specific resource key takes precedence; wildcard is consulted only when the exact key is absent. Primary use case: ADMIN role carries {"*": MaxInt64} — one JWT entry grants access to every resource without per-resource enumeration.
  • ClaimsPermissionProvider.ResolveMask now handles json.Number mask values in addition to the existing int64 and float64 paths. json.Number is produced by jwt.WithJSONNumber() (used in httpauth-jwt); it preserves math.MaxInt64 exactly where float64 would overflow.
  • WriteJSONError(w http.ResponseWriter, status int, code, message string) — exported helper that writes a structured JSON error body (Content-Type: application/json, {"code":"...","message":"..."}). Intended for use by all httpauth-* provider packages so the error format is enforced in one place.

Fixed

  • AuthzMiddleware and EnrichmentMiddleware now return JSON error bodies instead of plain-text http.Error responses. Error codes are stable gRPC-aligned names from xerrors: UNAUTHENTICATED (401), PERMISSION_DENIED (403), INTERNAL (500). Previously AuthzMiddleware also used the wrong code UNAUTHORIZED for 401 responses (correct value is UNAUTHENTICATED).

Dependencies

  • Added code.nochebuena.dev/go/xerrors v1.0.1 (for code constants in error responses).

1.0.0 — 2026-05-08

Added

  • NewChainPermissionProvider(providers ...rbac.PermissionProvider) rbac.PermissionProvider — tries each provider in order and returns the first non-zero mask; propagates errors immediately without consulting subsequent providers; primary use case is a JWT claims fast-path (ClaimsPermissionProvider) chained with a DB-backed fallback (CachedPermissionProvider)

Changed

  • Dependency code.nochebuena.dev/go/rbac bumped from v0.9.0 to v1.0.0

Unchanged

SetTokenData, EnrichmentMiddleware, AuthzMiddleware, IdentityEnricher, WithTenantHeader, Cache, NewClaimsPermissionProvider, and NewCachedPermissionProvider are API-compatible with v0.1.0.

0.1.0 - 2026-05-08

Added

  • SetTokenData(ctx, uid, claims) context.Context — injects verified uid and raw claims into the request context; called by provider-specific AuthMiddleware implementations (e.g. httpauth-firebase, httpauth-jwt) after token verification; downstream middleware reads these values via unexported helpers in the same package
  • IdentityEnricher interface — application-implemented; receives uid string and claims map[string]any, returns rbac.Identity; called by EnrichmentMiddleware on every authenticated request
  • EnrichOpt functional option type for configuring EnrichmentMiddleware
  • WithTenantHeader(header string) EnrichOpt — reads a tenant ID from the named request header and attaches it to the identity via rbac.Identity.WithTenant; absent header leaves TenantID as an empty string with no error
  • EnrichmentMiddleware(enricher IdentityEnricher, opts ...EnrichOpt) func(http.Handler) http.Handler — reads uid and claims stored by any upstream AuthMiddleware via SetTokenData, calls enricher.Enrich, and stores the resulting rbac.Identity in context via rbac.SetInContext; returns 401 if no uid is present; returns 500 if the enricher fails
  • AuthzMiddleware(provider rbac.PermissionProvider, resource string, required rbac.Permission) func(http.Handler) http.Handler — reads rbac.Identity from context via rbac.FromContext, resolves the permission mask via the provided rbac.PermissionProvider, and gates the request against the required permission bit; returns 401 if no identity is in context; returns 403 if the permission check fails or the provider returns an error; uses rbac.PermissionProvider directly without redefining it
  • Cache interface — abstracts the caching backend for permission masks; Get(ctx, key) (int64, bool, error) and Set(ctx, key, value, ttl) error; implementations are typically backed by Valkey or Redis
  • NewCachedPermissionProvider(inner rbac.PermissionProvider, cache Cache, ttl time.Duration) rbac.PermissionProvider — wraps any rbac.PermissionProvider with a TTL-based cache layer; cache key format is rbac:{uid}:{resource}; on cache miss falls through to inner and populates the cache; on cache error falls through silently — never fails due to cache unavailability
  • NewClaimsPermissionProvider(claimsKey string) rbac.PermissionProvider — reads pre-computed permission masks from JWT claims stored in the request context by SetTokenData; expects claims[claimsKey] to be a map[string]any where each key is a resource name and the value is the bitmask as int64 or float64 (JSON decodes numbers as float64); returns 0 without error if the claim is absent

Design Notes

  • AuthzMiddleware uses rbac.PermissionProvider directly rather than redefining a local interface; rbac is the single source of truth for this contract
  • EnrichmentMiddleware and AuthzMiddleware are provider-agnostic — they depend only on SetTokenData having been called upstream; any AuthMiddleware that calls SetTokenData (Firebase, JWT, API key, etc.) is compatible without changes to the enrichment or authorization layer
  • Two rbac.PermissionProvider implementations ship with this module for the two common architectures: ClaimsPermissionProvider for simple applications that embed permissions in the JWT (no per-request DB or network call), and CachedPermissionProvider for applications where the permission set is too large to embed or needs to be independently revocable
  • CachedPermissionProvider uses TTL-based expiry exclusively; explicit invalidation is left to callers who can interact with the Cache directly using the rbac:{uid}:{resource} key format