Initial Commit
This commit is contained in:
108
packages/api-stack/api/api-auth-jwt/src/resolver/resolver.md
Normal file
108
packages/api-stack/api/api-auth-jwt/src/resolver/resolver.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# `resolver` — `createJwtSessionResolver`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Stage-4 concrete `SessionResolver` factory for the API plugin
|
||||
foundation. Reads a JWT from the request (Authorization header or
|
||||
configurable cookie), verifies it via `@suaveplan/jws` against either a
|
||||
JWKS endpoint (cached and refreshed on `kid` miss) or a single static
|
||||
JWK, validates `iss`, `aud`, `exp`, `nbf` against the configured policy,
|
||||
and projects the validated claims onto a `Principal` via an overridable
|
||||
mapper. The factory is the only public entry point — internals
|
||||
(`JwksCache`, claims mapper, error registry) are surface-exported for
|
||||
testability but most consumers use `createJwtSessionResolver(opts)` alone.
|
||||
|
||||
## Features
|
||||
|
||||
- `Authorization: Bearer <jwt>` header extraction with case-insensitive
|
||||
scheme match; configurable header name accepts a raw token form for
|
||||
non-standard headers (e.g. `x-api-token: <jwt>`).
|
||||
- Cookie fallback when `cookieName` is supplied; header still wins when
|
||||
both are present.
|
||||
- JWKS resolution via `JwksCache` with positive TTL, negative cache,
|
||||
in-flight dedup, and `kid`-miss refresh.
|
||||
- Static-key path for HS-family symmetric tokens or single-key
|
||||
deployments.
|
||||
- Mandatory `iss` and `aud` enforcement (single string or list); `exp`
|
||||
and `nbf` enforced when present with configurable `clockToleranceSec`.
|
||||
- Per-request soft failures collapse to `null` so the upstream
|
||||
`SessionResolverChain` can fall through to the next adapter; operator
|
||||
failures (JWKS unreachable, malformed mapper, malformed static key)
|
||||
surface as `AuthJwtError` with the documented codes.
|
||||
- Idempotent `dispose()` flips an internal flag so subsequent
|
||||
`resolve()` calls return `null` rather than emitting late traffic on
|
||||
a torn-down resolver.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt/resolver";
|
||||
|
||||
const resolver = createJwtSessionResolver({
|
||||
jwksUrl: "https://idp.example/.well-known/jwks.json",
|
||||
issuer: "https://idp.example",
|
||||
audience: "https://api.example",
|
||||
algorithms: ["RS256"],
|
||||
});
|
||||
|
||||
const req = new Request("https://api.example/users", {
|
||||
headers: { authorization: "Bearer eyJ..." },
|
||||
});
|
||||
const principal = await resolver.resolve(req);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Static HS256 key, cookie fallback, custom claims mapper, and an
|
||||
injectable clock for deterministic temporal-claim tests:
|
||||
|
||||
```ts
|
||||
import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt/resolver";
|
||||
|
||||
const resolver = createJwtSessionResolver({
|
||||
staticKey: { kty: "oct", k: "<base64url-secret>" },
|
||||
issuer: ["https://idp1.example", "https://idp2.example"],
|
||||
audience: "https://api.example",
|
||||
algorithms: ["HS256"],
|
||||
cookieName: "sp_session",
|
||||
clockToleranceSec: 5,
|
||||
clock: { nowMs: () => 1_700_000_000_000 },
|
||||
claimsToPrincipal: (claims) => {
|
||||
const sub = claims["sub"];
|
||||
if (typeof sub !== "string") return null;
|
||||
return {
|
||||
id: sub as never,
|
||||
scopes: [] as never[],
|
||||
tenantId: typeof claims["org_id"] === "string"
|
||||
? (claims["org_id"] as never)
|
||||
: undefined,
|
||||
claims,
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createJwtSessionResolver(opts: JwtSessionResolverOptions): SessionResolver`
|
||||
— returns the `@suaveplan/api-auth` `SessionResolver` shape (`resolve` +
|
||||
idempotent `dispose`).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The resolver deliberately separates "this is not my caller" (chain
|
||||
falls through, returns `null`) from "the host is mis-configured"
|
||||
(throws `AuthJwtError`). Verification failures inside `@suaveplan/jws`
|
||||
return `null` so a stray Bearer header from an unrelated IdP cannot
|
||||
401-storm the API — the chain's next adapter (anonymous, static key,
|
||||
etc.) still gets its turn. The temporal-claim check uses a single
|
||||
clock reading per request to avoid TOCTOU between the `exp` and `nbf`
|
||||
arms. We re-implement `iss`/`aud`/`exp`/`nbf` enforcement here rather
|
||||
than push it into `@suaveplan/jws` because those are JWT-claim
|
||||
concerns, not JWS-protected-header concerns — the JWS package
|
||||
deliberately handles only RFC 7515 (signature) and leaves RFC 7519
|
||||
(payload claims) to its consumers.
|
||||
Reference in New Issue
Block a user