Files
skills/docs/api-stack/api/api-auth-jwt/src/jwks-cache/jwks-cache.md
2026-05-29 08:20:56 +01:00

3.1 KiB

jwks-cache — TTL-bounded JWKS resolution

(no submodules)

Purpose

In-process JWKS cache for @suaveplan/api-auth-jwt. Fetches a JWKS document from a remote endpoint, holds it under a positive TTL, negative-caches transient failures, dedupes concurrent fetches into a single network request, and supports forced refresh on a kid miss (the standard "the IdP rotated keys" recovery path). Throws AUTH_JWT.JWKS_FETCH_FAILED only when neither a fresh fetch nor a stale cached document is available — under partial outages we prefer stale-but-valid keys to a 401 storm.

Features

  • Positive TTL (default 10 minutes) with monotonic clock injection.
  • Negative cache window (default 30 seconds) on fetch failure — within the window callers see the stale document if any, otherwise the error.
  • In-flight dedup: concurrent getKeyForKid callers share a single network round-trip.
  • forceRefresh() for operator-triggered rotation.
  • kid miss against a cached document triggers exactly one refresh.
  • JSON shape validation ({ keys: object[] }) at the boundary.

Usage: Basic

import { JwksCache } from "@suaveplan/api-auth-jwt/jwks-cache";

const cache = new JwksCache({
    url: "https://idp.example/.well-known/jwks.json",
    ttlMs: 10 * 60 * 1000,
    negativeMs: 30 * 1000,
    clock: { nowMs: () => Date.now() },
    fetchImpl: fetch,
});

const jwk = await cache.getKeyForKid("kid-2024-01");

Usage: Advanced

Inject a fake clock and a controllable fetch impl for tests of the expiry and dedup behaviours:

let now = 0;
const calls: number[] = [];
const cache = new JwksCache({
    url: "https://idp.example/.well-known/jwks.json",
    ttlMs: 1000,
    negativeMs: 100,
    clock: { nowMs: () => now },
    fetchImpl: async () => {
        calls.push(now);
        return new Response(JSON.stringify({ keys: [{ kid: "a", kty: "RSA", n: "...", e: "AQAB" }] }), {
            status: 200,
            headers: { "content-type": "application/json" },
        });
    },
});
await cache.getKeyForKid("a");
now = 500;
await cache.getKeyForKid("a"); // cache hit, no second fetch

API Reference

  • class JwksCache — constructor takes { url, ttlMs, negativeMs, clock, fetchImpl }.
  • getKeyForKid(kid?: string): Promise<Record<string, unknown> | undefined> — fetch-as-needed, returns undefined when no key matches.
  • forceRefresh(): Promise<JwksDocument> — clear cache and refetch.
  • JwksDocument — { keys: ReadonlyArray<Record<string, unknown>> }.

Implementation Notes

When the token carries no kid, the cache only returns a key when the JWKS contains exactly one entry — ambiguous lookups must be a 401, not a guess. The negative cache window is short by design (30s default); longer windows accumulate authentication outages, shorter windows hammer the IdP. The in-flight dedup uses a single Promise field rather than a queue — the second concurrent caller awaits the same promise as the first. The "kid miss after a just-fetched document" guard prevents infinite refresh loops when the IdP legitimately does not have a matching key.