JWKS Management
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
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
The SDK caches JWKS to avoid fetching keys on every token validation. Caching is controlled by three mechanisms:
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
The jwksRefreshIntervalMs configuration sets a minimum interval between JWKS fetches. Even if a key is not found, the SDK will not refetch more frequently than this interval.
const authrim = new AuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com', jwksRefreshIntervalMs: 1800000, // 30 minutes});| Setting | Default | Description |
|---|---|---|
jwksRefreshIntervalMs | 3600000 (1 hour) | Minimum interval between JWKS refetches |
Cache Priority
The effective cache duration is determined by:
- Cache-Control header from the JWKS response (if present)
- jwksRefreshIntervalMs as the minimum interval
- 24-hour maximum as the upper bound
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
The SDK handles key rotation automatically. When a token references a kid (Key ID) that is not in the cached JWKS, the SDK:
- Checks if the cache is stale (beyond
jwksRefreshIntervalMs) - If stale, fetches fresh JWKS and retries the lookup
- If the
kidis still not found, throws aJwksError
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| D{"Cache stale?"}
D -->|Yes| E["Fetch fresh JWKS"]
D -->|No| F["Throw JwksError
(kid not found)"]
E --> G{"kid in fresh JWKS?"}
G -->|Yes| H["Use fresh key"]
G -->|No| F
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
Custom CacheProvider
For multi-server deployments, you may want to share JWKS across instances using a distributed cache. Implement the CacheProvider interface:
import type { CacheProvider } from '@authrim/server/providers';import type { JwkSet } from '@authrim/server';
const redisCache: CacheProvider<JwkSet> = { async get(key: string): Promise<JwkSet | undefined> { const data = await redis.get(key); if (!data) return undefined; return JSON.parse(data) as JwkSet; },
async set(key: string, value: JwkSet, ttlMs: number): Promise<void> { await redis.set(key, JSON.stringify(value), 'PX', ttlMs); },
async delete(key: string): Promise<void> { await redis.del(key); },};
const authrim = new AuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com', jwksCache: redisCache,});Benefits of a shared cache:
- Fewer JWKS fetches — One server fetches, all servers benefit
- Faster cold starts — New instances pick up cached keys immediately
- Consistent key state — All servers see the same keys at the same time
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 = new AuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com',});
// Explicit JWKS URI — skips discovery, fetches directlyconst authrim = new AuthrimServer({ 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
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 Types
| Error | Description |
|---|---|
JwksError | General JWKS fetch or processing error |
JwksKeyNotFoundError | No key matching the token’s kid was found after refresh |
JwksFetchError | Network error when fetching the JWKS endpoint |
JwksRedirectError | Cross-origin redirect detected (SSRF protection) |
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