JWKS Management
Overview
Section titled “Overview”@authrim/server manages JSON Web Key Sets (JWKS) automatically. It discovers the authorization server’s public keys, caches them efficiently, and handles key rotation — all without manual intervention.
Understanding the JWKS lifecycle helps you tune performance and troubleshoot key-related issues in production.
How JWKS Auto-Discovery Works
Section titled “How JWKS Auto-Discovery Works”When you call authrim.init(), the SDK performs OIDC Discovery to locate the JWKS endpoint:
sequenceDiagram
participant SDK as @authrim/server
participant Issuer as Authorization Server
SDK->>Issuer: GET /.well-known/openid-configuration
Issuer-->>SDK: { "jwks_uri": "https://auth.example.com/jwks" }
SDK->>Issuer: GET /jwks
Issuer-->>SDK: { "keys": [ { "kid": "key-1", ... } ] }
SDK->>SDK: Import and cache public keys
- Discovery — Fetches the OIDC Discovery document from
{issuer}/.well-known/openid-configuration - JWKS Fetch — Retrieves the JWKS from the
jwks_uriin the discovery document - Key Import — Imports each JWK as a platform-native CryptoKey for signature verification
- Cache — Stores the key set in memory (or a custom cache provider)
Caching Behavior
Section titled “Caching Behavior”The SDK caches imported JWKS keys to avoid fetching and importing keys on every token validation. Caching is controlled by two inputs:
Cache-Control Header
Section titled “Cache-Control Header”The SDK respects the Cache-Control header from the JWKS endpoint response. If the authorization server returns Cache-Control: max-age=3600, the SDK will not refetch for 3600 seconds.
The SDK enforces a maximum cache duration of 24 hours, regardless of the Cache-Control value. This ensures that even if the server returns an extremely long max-age, keys are eventually refreshed.
jwksRefreshIntervalMs
Section titled “jwksRefreshIntervalMs”The jwksRefreshIntervalMs configuration is the fallback cache TTL when the JWKS response does not include a usable Cache-Control: max-age directive.
const authrim = createAuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com', jwksRefreshIntervalMs: 1800000, // 30 minutes});| Setting | Default | Description |
|---|---|---|
jwksRefreshIntervalMs | 3600000 (1 hour) | Fallback TTL for cached JWKS keys |
Cache Priority
Section titled “Cache Priority”The effective cache duration is determined by:
- Cache-Control header from the JWKS response (if present)
- jwksRefreshIntervalMs as the fallback TTL when
Cache-Controlis absent or unusable - 24-hour maximum as the upper bound for
Cache-Control: max-age
Invalidating the Cache
Section titled “Invalidating the Cache”Force a JWKS refresh using invalidateJwksCache():
// Force the SDK to refetch JWKS on the next validationauthrim.invalidateJwksCache();This is useful when:
- You receive a notification that keys have been rotated
- You detect signature verification failures that may indicate stale keys
- You are implementing a manual key rotation workflow
After invalidation, the next validateToken() call triggers a fresh JWKS fetch.
Key Rotation Support
Section titled “Key Rotation Support”The SDK handles key rotation automatically. When a token references a kid (Key ID) that is not in the cached JWKS, the SDK:
- Fetches fresh JWKS once using the single-flight path
- Retries key selection with the fresh key set
- If the
kidis still not found, returnsjwks_key_not_foundthrough token validation
This auto-retry mechanism handles the common rotation scenario where the authorization server starts signing with a new key before the resource server’s cache expires.
flowchart TD
A["Token with kid: 'key-2'"] --> B{"kid in cache?"}
B -->|Yes| C["Use cached key"]
B -->|No| E["Fetch fresh JWKS"]
E --> G{"kid in fresh JWKS?"}
G -->|Yes| H["Use fresh key"]
G -->|No| F["Return jwks_key_not_found"]
Single-Flight Pattern
Section titled “Single-Flight Pattern”When multiple requests arrive simultaneously and trigger a JWKS refresh, the SDK coalesces them into a single network request. This prevents the “thundering herd” problem:
sequenceDiagram
participant R1 as Request 1
participant R2 as Request 2
participant R3 as Request 3
participant SDK as @authrim/server
participant Auth as Authorization Server
R1->>SDK: validateToken() — kid not found
R2->>SDK: validateToken() — kid not found
R3->>SDK: validateToken() — kid not found
SDK->>Auth: GET /jwks (single request)
Auth-->>SDK: { "keys": [...] }
SDK-->>R1: Validation result
SDK-->>R2: Validation result
SDK-->>R3: Validation result
All three requests wait for the same JWKS fetch and share the result. This:
- Reduces load on the authorization server
- Ensures consistent key state across concurrent validations
- Minimizes latency (only one round-trip)
SSRF Protection
Section titled “SSRF Protection”Custom CacheProvider
Section titled “Custom CacheProvider”You can inject a custom CacheProvider for process-local caches with custom eviction or observability. The SDK cache value contains imported key objects, so do not use a JSON Redis example unless your provider can safely preserve and restore the runtime key representation.
import type { CacheProvider } from '@authrim/server/providers';
const cache = new Map<string, { value: any[]; expiresAt: number }>();
const localJwksCache: CacheProvider<any[]> = { get(key) { const entry = cache.get(key); if (!entry || entry.expiresAt <= Date.now()) { cache.delete(key); return undefined; } return entry.value; },
set(key, value, ttlMs = 3600_000) { cache.set(key, { value, expiresAt: Date.now() + ttlMs }); },
delete(key) { cache.delete(key); },};
const authrim = createAuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com', jwksCache: localJwksCache,});Use this hook when you need process-local TTL policy, metrics, or integration with an isolate-local cache. For cross-process distributed caching, prefer caching the authorization server’s JWKS response outside the SDK path, or implement a provider that can re-import keys safely before returning them.
Explicit jwksUri vs Auto-Discovery
Section titled “Explicit jwksUri vs Auto-Discovery”By default, the SDK discovers the JWKS endpoint from the OIDC Discovery document. You can override this with an explicit jwksUri:
// Auto-discovery (default) — fetches from .well-known/openid-configurationconst authrim = createAuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com',});
// Explicit JWKS URI — skips discovery, fetches directlyconst authrim = createAuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com', jwksUri: 'https://auth.example.com/.well-known/jwks.json',});Use explicit jwksUri when:
- The authorization server does not support OIDC Discovery
- You want to avoid the initial discovery request for faster startup
- You are using a non-standard JWKS endpoint path
Key Import Warnings
Section titled “Key Import Warnings”The SDK may emit warnings during key import when encountering unexpected key parameters. These warnings are informational and do not prevent key usage:
- Unknown key type — A JWK with an unrecognized
ktyvalue is skipped - Missing required parameters — A JWK missing required fields (e.g.,
nandefor RSA) is skipped - Unsupported algorithm — A JWK with an
algvalue not in the supported list is skipped
Skipped keys do not affect validation of tokens signed with other keys in the set.
JWKS Error Codes
Section titled “JWKS Error Codes”| Code | Description |
|---|---|
jwks_fetch_error | JWKS fetch failed, the response was invalid, or a cross-origin redirect was blocked |
jwks_key_not_found | No key matching the token’s kid was found after refresh |
jwks_key_ambiguous | The token omitted kid and zero or multiple keys matched the algorithm |
jwks_key_import_error | A selected key could not be imported for signature verification |
Next Steps
Section titled “Next Steps”- Token Validation — JWT validation pipeline that uses JWKS
- DPoP Validation — DPoP proof verification with JWK Thumbprint
- Security Considerations — SSRF protection and production security guidance
- Introspection & Revocation — Alternative to local JWT validation