Skip to content

JWKS Management

@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.

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
  1. Discovery — Fetches the OIDC Discovery document from {issuer}/.well-known/openid-configuration
  2. JWKS Fetch — Retrieves the JWKS from the jwks_uri in the discovery document
  3. Key Import — Imports each JWK as a platform-native CryptoKey for signature verification
  4. Cache — Stores the key set in memory (or a custom cache provider)

The SDK caches imported JWKS keys to avoid fetching and importing keys on every token validation. Caching is controlled by two inputs:

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.

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
});
SettingDefaultDescription
jwksRefreshIntervalMs3600000 (1 hour)Fallback TTL for cached JWKS keys

The effective cache duration is determined by:

  1. Cache-Control header from the JWKS response (if present)
  2. jwksRefreshIntervalMs as the fallback TTL when Cache-Control is absent or unusable
  3. 24-hour maximum as the upper bound for Cache-Control: max-age

Force a JWKS refresh using invalidateJwksCache():

// Force the SDK to refetch JWKS on the next validation
authrim.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.

The SDK handles key rotation automatically. When a token references a kid (Key ID) that is not in the cached JWKS, the SDK:

  1. Fetches fresh JWKS once using the single-flight path
  2. Retries key selection with the fresh key set
  3. If the kid is still not found, returns jwks_key_not_found through 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"]

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)

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.

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-configuration
const authrim = createAuthrimServer({
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});
// Explicit JWKS URI — skips discovery, fetches directly
const 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

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 kty value is skipped
  • Missing required parameters — A JWK missing required fields (e.g., n and e for RSA) is skipped
  • Unsupported algorithm — A JWK with an alg value not in the supported list is skipped

Skipped keys do not affect validation of tokens signed with other keys in the set.

CodeDescription
jwks_fetch_errorJWKS fetch failed, the response was invalid, or a cross-origin redirect was blocked
jwks_key_not_foundNo key matching the token’s kid was found after refresh
jwks_key_ambiguousThe token omitted kid and zero or multiple keys matched the algorithm
jwks_key_import_errorA selected key could not be imported for signature verification