/** * jwksIdentity — verify the caller's bearer token against a published key set. * * Pattern: Adapter (GoF) over `jose`, behind the vendor-neutral * {@link IdentityVerifier} port. * Role: the ONE identity verifier this release ships. It covers cloud IdPs * and on-prem ones with the same code, because JWKS is the same * protocol in both: a URL that publishes signing keys, and tokens * signed by one of them. * Emits: nothing. The door that called it reports the refusal — by CLASS. * * **Status: field-validated — an independent field trial, 2026-08-13.** The * trial pointed this adapter at a remote JWKS endpoint during a live, * cloud-backed host run (a real `nodeHost` in a live Google Cloud project, * sessions in a real Firestore) and verified an RS256 token end to end: issuer, * audience, expiry, subject and roles. A valid token paired with a CLAIMED * other identity was rejected; missing, expired and wrong-audience tokens each * answered 401; four different bearer tokens appeared in no captured response * body or error; and the remote key set was fetched once over the network, then * served from the cache below. * * Two bounds on that rung, so nobody reads more into it than happened. The * finding does not name the identity provider, so what is field-proven is the * PROTOCOL path — a real fetch, a real `jose` verification, a real cache — and * not interop with any particular commercial IdP. And the provider behind that * door was a deterministic mock: "401 before any model call" was measured as * *rejected requests made zero provider calls*, which is the cheapest-refusal * claim and not a statement about a real completion's cost. * * ── What it checks, and what it refuses to pretend to check ───────────────── * Signature (against the key the token's `kid` names, fetched and cached from * `jwksUrl`), `iss`, `aud`, `exp` and `nbf`. That is the whole list, and it is * the whole list on purpose: * * • **It is not an authorization decision.** A verified token proves WHO, not * WHAT-THEY-MAY-DO. Roles and claims come back on the result so your own * policy can decide; this adapter never reads them. * • **It does not check revocation.** A JWT is valid until it expires, and no * amount of key fetching changes that. Short lifetimes are the answer; an * adapter that implied otherwise would be selling a guarantee the protocol * does not make. * • **`alg: none` cannot get through**, because a JWKS resolver has no key for * it — verified against the real library rather than assumed (see the pin * test). Neither can a token signed with an algorithm outside `algorithms`. * * ── The token never comes back out ────────────────────────────────────────── * Every failure is re-raised as {@link IdentityNotVerifiedError} carrying only * the CLASS — `expired`, `wrong-audience`, `wrong-issuer`, `not-yet-valid`, * `unverifiable`. The library's own message is dropped, and the original is * deliberately NOT attached as `cause`: a cause travels into every serializer * that walks own properties, and one `JSON.stringify` would undo all of this. * (The `sdkFailure` law, applied to a credential rather than to a cloud SDK.) * * A key set this adapter could not FETCH is a different fact and gets a * different class: {@link VerifierUnavailableError}, which the host answers 503 * rather than 401. Your IdP being down is not the caller presenting a bad * token, and telling every client to re-authenticate against an unreachable * provider is the wrong instruction at the worst moment. * * @example * const host = await standingAgent({ * agent, sessions, host: nodeHost({ port: 8080 }), * identity: { verify: jwksIdentity({ * jwksUrl: 'https://idp.example.com/.well-known/jwks.json', * issuer: 'https://idp.example.com/', * audience: 'my-api', * }).verify }, * }); */ import type { IdentityVerifier } from '../../hosting/identityVerification.js'; /** * The slice of `jose` this adapter uses — declared STRUCTURALLY, so a stub, a * pinned fork, or a future major satisfies it without this package taking a * hard type dependency on an optional peer. (The `UnpdfBackend` precedent.) */ export interface JoseBackend { createRemoteJWKSet(url: URL, options?: Record): unknown; jwtVerify(token: string, key: unknown, options?: Record): Promise<{ payload: Record; }>; } export interface JwksIdentityOptions { /** Where the signing keys are published — an IdP's * `.../.well-known/jwks.json`. */ readonly jwksUrl: string; /** The `iss` every accepted token must carry. Required: a verifier that * accepted any issuer would accept a token minted by anybody who publishes * a key set. */ readonly issuer: string | readonly string[]; /** The `aud` every accepted token must carry — THIS API's name at the IdP. * Required for the same reason: a token minted for a different service is * a valid token, and honouring it is confused-deputy by construction. */ readonly audience: string | readonly string[]; /** * Which claim names the user. Default `'sub'` — the subject, which is what * `sub` means. Point it at `'email'`, `'oid'` or a namespaced claim when your * IdP's stable id lives elsewhere; a token whose chosen claim is missing or * blank is `unverifiable`, never silently anonymous. */ readonly userIdClaim?: string; /** * Which claim carries roles. Default `'roles'`. Read leniently — a string, * an array of strings, or a space-delimited string (the `scope` convention) — * and ABSENT when the claim is absent. Never invented. */ readonly rolesClaim?: string; /** * Signature algorithms to accept. Default: the RSA and ECDSA families * (`RS256/384/512`, `PS256/384/512`, `ES256/384/512`). * * Symmetric algorithms (`HS*`) are deliberately NOT in the default: with a * JWKS the key is public, and a verifier that accepts an HMAC alg over a * published key is the classic algorithm-confusion forgery. Name them * explicitly if a deployment genuinely needs one and you know why. */ readonly algorithms?: readonly string[]; /** Seconds of clock skew tolerated on `exp`/`nbf`. Default 0 — the library * does not decide how much drift your fleet has. */ readonly clockToleranceSeconds?: number; /** How long a fetched key set is reused before re-fetching, in ms. Default * is `jose`'s own (10 minutes). */ readonly cacheMaxAgeMs?: number; /** Timeout for the key-set fetch, in ms. Default is `jose`'s own (5s). */ readonly fetchTimeoutMs?: number; /** * An ALREADY-IMPORTED `jose`. Supply it and the lazy load never happens — * which is what makes this work in a BUNDLED app, where a bare specifier * reaches the runtime unresolved: * * ```ts * import * as jose from 'jose'; * jwksIdentity({ jwksUrl, issuer, audience, backend: jose }); * ``` */ readonly backend?: JoseBackend; } /** Raised when a token must be verified and `jose` is not installed. */ export declare class MissingJwksSupportError extends Error { readonly code: "ERR_MISSING_JWKS_SUPPORT"; constructor(); } export declare function jwksIdentity(options: JwksIdentityOptions): IdentityVerifier; //# sourceMappingURL=jwks.d.ts.map