Files
suaveplan/packages/api-stack/api/api-auth-jwt/src/resolver/resolver.md
2026-05-27 23:29:53 +01:00

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 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

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-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.