Introduces code.nochebuena.dev/einherjar/auth — the provider-agnostic HTTP
authentication and authorization layer of the Einherjar framework. Absorbs
two micro-lib packages (httpauth, rbac) as sub-packages, replacing the
Identity-only context model with a SecurityBag-native design and adding a
composable enrichment chain.
authmw:
- BagEnricher function type — enriches the request-scoped SecurityBag after
the base Identity is built; registered via WithBagEnricher; multiple
enrichers run in order, each receiving the bag from the previous
- IdentityEnricher interface — application-layer contract for loading user
data from uid+claims
- EnrichmentMiddleware — builds SecurityBag from uid+claims, runs enricher
chain, stores via security.SetBagInContext; 401 on missing uid, 500 on
enricher error; routes all errors through httputil.Error
- AuthzMiddleware — per-route permission gate; 401 on missing identity,
403 on provider error (fail-closed) or insufficient permissions
- EnrichOpt type + WithTenantHeader (reads TenantID from header, implemented
as a BagEnricher) + WithBagEnricher (registers custom enrichers for
hardware IDs, grant codes, or any bag attribute)
- SetTokenData / GetClaims — integration contract for auth-jwt / auth-firebase
rbac:
- NewClaimsPermissionProvider — reads flat JWT claim bitmasks from context;
wildcard "*" fallback; handles int64/float64/json.Number; zero DB calls
- NewCachedPermissionProvider — TTL cache wrapping any PermissionProvider;
default key "rbac:{uid}:{resource}" or "rbac:{tenantID}:{uid}:{resource}";
TenantID sourced from SecurityBag automatically; accepts ...CachedOpt
- CachedOpt type + WithCacheKey — overrides the key function for extra
dimensions (hardware IDs, grant codes read from bag attributes)
- NewChainPermissionProvider — tries providers in order; first non-zero wins;
errors short-circuit; typical pattern: claims → cached DB fallback
- Cache interface — pluggable backend satisfied by cache-valkey via duck typing
Compliance test (package auth_test) enforces CT-6 (≤1 exported TypeSpec/file),
compile-time interface satisfaction, and behavioural coverage across the full
middleware and provider surface: enrichment success/failure, tenant header,
custom BagEnricher, bag-in-context, authz allowed/denied/error, claims
hit/wildcard/missing/float64, cached hit/miss/error/tenant-key/custom-key,
chain first-non-zero/fallthrough/error.
Depends on contracts v1.0.0, core v1.0.0, web v1.0.0.
- identifiable.go: package-level Module variable (observability.Identifiable) for version
identification — auth is middleware-only; not registered with the launcher
4.6 KiB
Architecture Decision Records — einherjar/auth
ADR-001: No root factory
Status: Accepted
Context: Previous starters (web) expose a root New(logger, ...Config) factory that pre-wires a complete, opinionated stack for the common case. The same pattern was evaluated for auth.
Decision: auth exposes no root factory. The module is composition-only.
Rationale: web.New works because the right middleware stack is always the same (Recover → RequestID → RequestLogger → CORS). Auth has no equivalent universal default:
- The auth provider is application-specific (JWT vs Firebase vs custom)
- The
IdentityEnricheris always application code AuthzMiddlewareis per-route, not global- The
PermissionProviderchain depends on the application's permission model
A root factory would either be a thin wrapper (adds no value) or force premature architectural choices on the caller.
ADR-002: Logger added to both middleware functions
Status: Accepted
Context: micro-lib's EnrichmentMiddleware and AuthzMiddleware were logger-free — errors were written as HTTP responses with no log emission.
Decision: Both middleware functions accept logging.Logger and route all errors through httputil.Error(logger, w, r, err).
Rationale:
- An
IdentityEnricherreturning a 500 is a server-side failure. It must be logged — silent 500s are invisible in production. httputil.Erroris the single error-processing point for the framework. Using it in auth keeps error response shape, log levels, and field enrichment identical to application handlers.- 401 (missing token) and 403 (permission denied) log at Warn — client errors, not operator alerts.
- Log level routing is already handled by
httputil.Error; no per-middleware logic needed.
ADR-003: auth depends on web for httputil.Error
Status: Accepted
Context: auth error responses could be written independently (custom JSON write, like micro-lib's WriteJSONError) to avoid a dependency on web.
Decision: auth depends on code.nochebuena.dev/einherjar/web solely for httputil.Error.
Rationale:
- Consistent error shape across the entire API surface — auth errors and handler errors are indistinguishable to clients.
httputil.Errorhandles log level routing, error code extraction, and platform code inclusion. Duplicating this inauthwould create two implementations that drift over time.- The dependency is one-directional:
webdoes not depend onauth. The dependency graph remains acyclic.
Trade-off accepted: auth cannot be used without web. This is acceptable — auth is an HTTP middleware layer and web is the HTTP layer. Separating them was never a goal.
ADR-004: BagEnricher chain over arbitrary context injection
Status: Accepted
Context: The original design used WithTenantHeader as the sole enrichment option, storing additional attributes via arbitrary context.WithValue calls. This made enrichment opaque — permission providers had no structured way to access request attributes like hardware IDs or grant codes without knowing the exact context key used elsewhere.
Decision: Enrichment is modeled as a chain of BagEnricher functions that transform a security.SecurityBag. EnrichmentMiddleware creates the bag from the base Identity, runs all registered BagEnricher functions in order, and stores the final bag via security.SetBagInContext. Permission providers retrieve the bag once with security.BagFromContext(ctx) and have access to all enriched attributes via bag.Get(key).
Rationale:
- Single structured object per request instead of scattered
context.WithValueentries; easier to reason about what's in the security context at any point in the chain. - Permission providers access any bag attribute in one call rather than knowing which context key to look up.
WithCacheKeyonCachedPermissionProviderreceives the full bag — no new parameters needed to include extra dimensions (hardware IDs, grant codes) in the cache key.WithTenantHeaderis now a thin wrapper overWithBagEnricher, so the same extension point covers all enrichment use cases.- Framework-owned attributes remain typed fields on
Identity. Only application-defined attributes go into the bag's string-keyed map. Callers define their own key constants to maintain compile-time safety at the type-assertion call site.
Trade-off accepted: Bag attributes are any — type assertions are required at read sites. This is inherent to a string-keyed heterogeneous map. The contract is: the package that writes an attribute owns the constant and documents the type; callers that read it perform the assertion.