Skip to content

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
  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)

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
});
SettingDefaultDescription
jwksRefreshIntervalMs3600000 (1 hour)Minimum interval between JWKS refetches

Cache Priority

The effective cache duration is determined by:

  1. Cache-Control header from the JWKS response (if present)
  2. jwksRefreshIntervalMs as the minimum interval
  3. 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 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.

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:

  1. Checks if the cache is stale (beyond jwksRefreshIntervalMs)
  2. If stale, fetches fresh JWKS and retries the lookup
  3. If the kid is still not found, throws a JwksError

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

JWKS Error Types

ErrorDescription
JwksErrorGeneral JWKS fetch or processing error
JwksKeyNotFoundErrorNo key matching the token’s kid was found after refresh
JwksFetchErrorNetwork error when fetching the JWKS endpoint
JwksRedirectErrorCross-origin redirect detected (SSRF protection)

Next Steps