first commit
This commit is contained in:
91
docs/api-stack/api/api-auth-jwt/src/jwks-cache/jwks-cache.md
Normal file
91
docs/api-stack/api/api-auth-jwt/src/jwks-cache/jwks-cache.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# `jwks-cache` — TTL-bounded JWKS resolution
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user