authentication

why

identity is a proof, and proofs belong at the one boundary equipped to check them — the transport edge that holds the credential. when deeper services re-parse tokens or read auth headers themselves, authorization logic scatters and the copies drift: a lax service's check becomes a bypass that the gateway cannot see. proving once and passing a typed principal keeps every deeper decision grounded in one verified fact.

flowchart LR
C[caller] --> E[transport edge: verify credential · load principal]
E --> X[execution context: typed principal]
X --> S[domain service: authorization decisions]
X --> W[workflow input: identity snapshotted at handoff]
E -.typed auth diagnostic.-> F[denial surface]

rule

establish caller identity once, at the outermost boundary that can prove it. every deeper layer receives identity as typed execution context — never re-parses tokens, never reads auth headers directly.

why: re-parsing scatters authorization logic across services, each with its own token validation, and they drift — crafted requests pass a lax service's check, a service that never verifies becomes a bypass, an expired token is accepted by one service and rejected by another. parse exactly once, where it is audited and all paths must pass it.

boundary order

layerresponsibilitymust not
transport edge (middleware/route)verify the credential, load the principal, establish context.contain business rules.
execution contextcarry the typed principal to every downstream owner.be re-derived per service call.
domain serviceread the principal from context for authorization decisions.accept a raw token or header parameter.
workflow/stepreceive the principal as a durable fact at handoff time.re-authenticate mid-run.

authentication failure is a diagnostic error at the boundary: a typed, stable error response naming the correction path, not a stack trace or a bare 401.

credential policy

  • one verifier per credential kind; it owns parsing, validation, and expiry checks.
  • token format is an untrusted boundary input: validate with a named schema like any other external payload.
  • expiry and revocation checks run at the edge, not at each consumer.
  • identity for durable handoffs is snapshotted into the workflow input; a run does not depend on a token remaining valid across hours or retries.

do not

  • read auth headers or decode tokens inside a domain service, mapper, or component.
  • pass identity as an untyped userId: string parameter threaded through five callers when context already carries the principal.
  • invent a second identity vocabulary: one principal type, one context field.
  • swallow an authentication failure into an empty result. denial is a distinct semantic state, per truthful product states.

verification

probe one protected route with a missing, malformed, expired, and valid credential. record status and payload for each: the first three must return the typed authentication diagnostic, the fourth must reach the domain service with the principal populated in context. search domain services for direct header or token reads — there should be none.

good and bad

bad

// deep inside a domain service
const token = req.headers["authorization"];
const userId = decode(token).sub;
// another service does the same differently
const user = decodeJWT(req.headers.authorization);
if (!user) throw "unauthorized";

a service re-parses the credential: one accepts a malformed token that crashes, another an expired token, authorization is duplicated with no one to audit, and crafted requests slip through a direct service call the gateway misses. the failure is a bare throw instead of a typed diagnostic from the edge.

good

// edge verifies the credential once and establishes context
const principal = ctx.principal; // typed, from execution context

identity is proven once at the transport boundary; every deeper layer reads a typed principal and cannot mis-parse what it never sees. the authorization surface stays auditable in one verifier, expiry and revocation apply to every request equally, and adding a service cannot widen the attack surface because new code composes the same verified context.

search pages

go to any page