4.1 KiB
resolver — createJwtSessionResolver
(no submodules)
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
cookieNameis supplied; header still wins when both are present. - JWKS resolution via
JwksCachewith positive TTL, negative cache, in-flight dedup, andkid-miss refresh. - Static-key path for HS-family symmetric tokens or single-key deployments.
- Mandatory
issandaudenforcement (single string or list);expandnbfenforced when present with configurableclockToleranceSec. - Per-request soft failures collapse to
nullso the upstreamSessionResolverChaincan fall through to the next adapter; operator failures (JWKS unreachable, malformed mapper, malformed static key) surface asAuthJwtErrorwith the documented codes. - Idempotent
dispose()flips an internal flag so subsequentresolve()calls returnnullrather than emitting late traffic on a torn-down resolver.
Usage: Basic
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:
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-authSessionResolvershape (resolve+ idempotentdispose).
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.