Files
auth/docs/adr/index.md
Rene Nochebuena 8a306abed0 feat(auth): initial implementation — authmw and rbac (v1.0.0)
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
2026-05-29 16:11:21 +00:00

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 IdentityEnricher is always application code
  • AuthzMiddleware is per-route, not global
  • The PermissionProvider chain 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 IdentityEnricher returning a 500 is a server-side failure. It must be logged — silent 500s are invisible in production.
  • httputil.Error is 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.Error handles log level routing, error code extraction, and platform code inclusion. Duplicating this in auth would create two implementations that drift over time.
  • The dependency is one-directional: web does not depend on auth. 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.WithValue entries; 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.
  • WithCacheKey on CachedPermissionProvider receives the full bag — no new parameters needed to include extra dimensions (hardware IDs, grant codes) in the cache key.
  • WithTenantHeader is now a thin wrapper over WithBagEnricher, 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.