Initial Commit
This commit is contained in:
100
packages/api-stack/api/api-auth-rolled/src/session/session.md
Normal file
100
packages/api-stack/api/api-auth-rolled/src/session/session.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# `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.
|
||||
Reference in New Issue
Block a user