3.9 KiB
session
(no submodules)
Purpose
The rolling-JWT session resolver — the keystone of the rolled auth
adapter. Builds a SessionResolver (per @suaveplan/api-auth) that
parses-and-verifies a JWT extracted from either the Authorization: Bearer header or a configurable cookie, runs the JWT through the
TokenRotator to find the verifying JWK by kid, projects the matching
user row onto a Principal, and returns null on every soft failure so
the host's resolver chain can move on to the next adapter.
The same module exposes issueToken(userId) so the future
account-lifecycle provider (deferred to api-auth-rolled-provider) can
share a single signing path with the resolver. Keeping issue and verify
co-located means the JWT shape and the rotator wiring stay in lock-step.
Features
- RS256 signing via
@suaveplan/jwscompactSignwith the rotator's active JWK; verification viacompactVerifyagainst the rotator's retired-or-active JWK matched bykid. - Bearer + cookie extraction in one pass — bearer wins when both are present so service clients can override a stale cookie.
- Strict JWT-claim validation:
iss/audexact match,expfuture,iatnot beforeusers.createdAt. - Soft failures (missing token, bad signature, expired, unknown kid)
collapse to
null; hard failures (storage adapter throws) propagate asAuthRolledError. buildSetCookiereturns a deterministicSet-Cookievalue withHttpOnly,Path=/,Max-Age,SameSite,Secure— used by the future provider half but published here so the cookie shape lives next to the cookie parser.
Usage: Basic
import { createRolledSessionResolver, TokenRotator } from "@suaveplan/api-auth-rolled";
const rotator = new TokenRotator({ keys, rotationPeriodMs, clock });
const resolver = createRolledSessionResolver({
options,
rotator,
clock,
cookieName: "suaveplan.session",
sessionTtlSec: 3600,
});
const principal = await resolver.resolve(req);
Usage: Advanced
import { createRolledSessionResolver } from "@suaveplan/api-auth-rolled";
const resolver = createRolledSessionResolver({ /* ... */ });
// Provider half (future) signs new tokens with the same key set as the
// resolver — sharing one path means the rotator's view of the active
// kid is the only source of truth.
const issued = await resolver.issueToken("u_1");
const setCookie = buildSetCookie({
name: "suaveplan.session",
value: issued.token,
maxAgeSec: issued.expiresAt - issued.issuedAt,
sameSite: "lax",
secure: true,
});
API Reference
createRolledSessionResolver({ options, rotator, clock, cookieName, sessionTtlSec }) → SessionResolver & { issueToken, verifyToken }buildSetCookie({ name, value, maxAgeSec, sameSite, secure }) → stringtokenSizeOf(token: string): number— byte length, used by the conformance harness to assert tokens stay under the 8 KiB cookie limit.
Implementation Notes
The resolver intentionally returns null on every parse/verify failure
rather than throwing — SessionResolver chain semantics treat thrown
errors as operator-actionable infrastructure failures, so soft failures
must collapse to null to let the chain advance to the next adapter
(typically api-auth-anonymous). The two cases where the resolver does
throw are AuthRolledError(PRINCIPAL_PROJECTION_FAILED) raised by the
projection step (storage-driver bug) and whatever error the operator's
store.findUserById raises (DB connection lost). The session-resolver
chain handles both by surfacing 500 to the client and routing to the
host's audit log.
The header parse is done manually rather than via compactDecode so a
malformed header (e.g. missing kid) bypasses the rotator lookup
entirely. The rotator can verify with retired-but-non-revoked keys, so
rotating the active key never invalidates a session mid-lifetime —
operators can rotate without coordinating with active users.