101 lines
3.9 KiB
Markdown
101 lines
3.9 KiB
Markdown
# `session`
|
|
|
|
<!-- begin:auto-docs -->
|
|
_(no submodules)_
|
|
<!-- end:auto-docs -->
|
|
|
|
## 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
|
|
|
|
```ts
|
|
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
|
|
|
|
```ts
|
|
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.
|