feat(httpauth): initial release — provider-agnostic HTTP auth middleware

Provides SetTokenData for upstream AuthMiddleware implementations,
EnrichmentMiddleware and AuthzMiddleware compatible with any provider that
calls SetTokenData, ClaimsPermissionProvider for JWT-embedded permissions,
and CachedPermissionProvider for TTL-backed runtime resolution via any
Cache implementation.
This commit is contained in:
2026-05-07 21:37:25 -06:00
commit 18e5a16f7e
16 changed files with 879 additions and 0 deletions

29
CHANGELOG.md Normal file
View File

@@ -0,0 +1,29 @@
# Changelog
All notable changes to this module will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [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
[0.1.0]: https://code.nochebuena.dev/go/httpauth/releases/tag/v0.1.0