Files
skills/docs/api-stack/api/api-auth-rolled/src/session/session.md
2026-05-29 08:20:56 +01:00

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/jws compactSign with the rotator's active JWK; verification via compactVerify against the rotator's retired-or-active JWK matched by kid.
  • 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/aud exact match, exp future, iat not before users.createdAt.
  • Soft failures (missing token, bad signature, expired, unknown kid) collapse to null; hard failures (storage adapter throws) propagate as AuthRolledError.
  • buildSetCookie returns a deterministic Set-Cookie value with HttpOnly, 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 }) → string
  • tokenSizeOf(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.