JWKS (JSON Web Key Set)

Authentication Security Notes Jan 6, 2025 JAVASCRIPT

Definition

Imagine you’re a bank teller who needs to verify that checks are legitimately signed by account holders. You don’t have everyone’s signature on file - instead, you have a directory where you can look up the official signature for any account number. When a check comes in, you look up the signature, compare it to the one on the check, and either accept or reject it. JWKS (JSON Web Key Set) is this signature directory for the digital world - it’s a public endpoint where applications can look up the cryptographic keys needed to verify that JWTs (JSON Web Tokens) are authentic.

When you log in using “Sign in with Google” or any OAuth/OpenID Connect provider, you receive JWTs that are cryptographically signed. But how does your application verify that signature? It needs Google’s public key. Rather than hardcoding keys (which would break when Google rotates them), your application fetches Google’s JWKS from a well-known endpoint like https://www.googleapis.com/oauth2/v3/certs. This endpoint returns a JSON object containing all of Google’s current public keys. Your app finds the right key (matched by kid - key ID in the JWT header) and uses it to verify the signature.

The “Set” in JWKS is crucial - providers publish multiple keys for key rotation. Google might have keys “key-001” and “key-002” active simultaneously. When they rotate, they add “key-003” and eventually remove “key-001”. Your application doesn’t need to know about rotations - it just fetches the JWKS and finds the matching key. This seamless rotation is why JWKS is the standard for distributed authentication. Every major identity provider (Google, Auth0, Okta, Azure AD) publishes a JWKS endpoint, and every JWT library knows how to use it.

Example

Real-World Scenario 1: Verifying Google Sign-In Tokens Your app implements “Sign in with Google.” When users authenticate, Google sends an ID token (a JWT) to your app. The JWT header contains "kid": "12345abc". Your app fetches Google’s JWKS from https://www.googleapis.com/oauth2/v3/certs, finds the key with matching kid, and uses that public key to verify the JWT signature. If verification succeeds, you know the token genuinely came from Google.

Real-World Scenario 2: Auth0 Multi-Tenant Setup Your SaaS app uses Auth0 for authentication. Each customer (tenant) has their own Auth0 domain. When tokens arrive, your app determines the tenant from the issuer claim (iss), constructs the JWKS URL (https://{tenant}.auth0.com/.well-known/jwks.json), fetches the keys, and verifies. The JWKS pattern lets you support multiple identity providers with the same verification logic.

Real-World Scenario 3: Microservices Token Validation In a microservices architecture, each service needs to validate JWT access tokens. Rather than sharing secrets (security nightmare), the auth service publishes a JWKS endpoint. Every microservice fetches and caches these keys. When the auth service rotates keys, services automatically pick up new keys from JWKS. Zero coordination needed, zero downtime during rotation.

Real-World Scenario 4: Key Rotation in Production Your identity provider needs to rotate keys (security best practice). They add a new key to JWKS while keeping the old one. New tokens use the new key (kid: "2024-key"), old tokens still validate with old key (kid: "2023-key"). After 24 hours, they remove the old key. Applications with reasonable JWKS cache TTLs handle this transparently - no code changes, no outages.

Analogy

The Public Notary Directory: Imagine a directory of public notaries with their official stamp patterns. When you receive a notarized document, you look up the notary’s stamp in the directory to verify it’s authentic. JWKS is this directory - it contains the “official stamps” (public keys) that identity providers use to sign tokens.

The Embassy Seal Registry: Embassies use official seals on documents. A central registry publishes which seal patterns belong to which embassy. When you receive a sealed document, you verify against the registry. JWKS is the seal registry for JWTs - it tells you which public keys belong to which issuers.

The Phone Book for Signatures: In the old days, banks kept signature cards on file. When a check came in, they’d compare the signature to the card. JWKS is a digital phone book where identity providers publish their signature patterns (public keys). Anyone can look up the pattern to verify a signature.

The Certificate Authority: HTTPS uses certificate authorities to verify websites. JWKS is like a mini-CA for JWTs - it publishes the public keys that verify token signatures. Instead of trusting a certificate chain, you trust the JWKS endpoint of the issuer.

Code Example


// Example JWKS endpoint response
// GET https://oauth.provider.com/.well-known/jwks.json
{
  "keys": [
    {
      "kty": "RSA",                    // Key type
      "kid": "key-2024-001",           // Key ID (matches JWT header)
      "use": "sig",                    // Key usage: signature
      "alg": "RS256",                  // Algorithm
      "n": "0vx7agoebGcQSuuPiLJXZpt...N4IOJnoEhw",  // RSA modulus
      "e": "AQAB"                       // RSA exponent
    },
    {
      "kty": "RSA",
      "kid": "key-2024-002",           // Second key for rotation
      "use": "sig",
      "alg": "RS256",
      "n": "xK94kVtxLhP...gqI7Y",
      "e": "AQAB"
    }
  ]
}

// Fetch and use JWKS to verify JWT
const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');

// Create client with caching
const client = jwksClient({
  jwksUri: 'https://oauth.provider.com/.well-known/jwks.json',
  cache: true,                    // Cache keys
  cacheMaxAge: 600000,            // 10 minute cache
  rateLimit: true,                // Prevent DoS on JWKS endpoint
  jwksRequestsPerMinute: 10
});

// Function to get signing key by kid
function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    if (err) {
      console.error('Key not found:', header.kid);
      return callback(err);
    }
    const signingKey = key.getPublicKey();
    callback(null, signingKey);
  });
}

// Verify token using JWKS
async function verifyToken(token) {
  return new Promise((resolve, reject) => {
    jwt.verify(token, getKey, {
      algorithms: ['RS256'],
      audience: 'your-client-id',
      issuer: 'https://oauth.provider.com'
    }, (err, decoded) => {
      if (err) {
        return reject(new Error(`Token verification failed: ${err.message}`));
      }
      resolve(decoded);
    });
  });
}

// Middleware for Express
const verifyJWT = async (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }

  const token = authHeader.substring(7);

  try {
    req.user = await verifyToken(token);
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

// Discovery: Find JWKS URL from OpenID Configuration
// GET https://oauth.provider.com/.well-known/openid-configuration
{
  "issuer": "https://oauth.provider.com",
  "jwks_uri": "https://oauth.provider.com/.well-known/jwks.json",
  "authorization_endpoint": "...",
  "token_endpoint": "..."
}

Diagram

sequenceDiagram
    participant Client as Your App
    participant IDP as Identity Provider
    participant JWKS as JWKS Endpoint

    Note over Client: Receives JWT with kid in header

    Client->>Client: 1. Decode JWT header
kid: "key-2024-001" Client->>JWKS: 2. GET /.well-known/jwks.json JWKS->>Client: 3. Return key set Note over Client: {
"keys": [
{ "kid": "key-2024-001", "n": "...", "e": "..." },
{ "kid": "key-2024-002", "n": "...", "e": "..." }
]
} Client->>Client: 4. Find key matching kid Client->>Client: 5. Verify JWT signature with public key alt Signature Valid Client->>Client: 6. Accept token, extract claims else Signature Invalid Client->>Client: 6. Reject token end Note over JWKS: Keys rotate periodically.
Multiple keys allow seamless rotation.

Security Notes

SECURITY NOTES

CRITICAL: JWKS endpoint provides public keys. Cache keys to improve performance.

JWKS Endpoint:

  • Public keys: JSON Web Key Set endpoint
  • Key rotation: Provides current signing keys
  • Key ID (kid): Each key has unique identifier
  • Algorithm: Specifies key algorithm (RSA, ECDSA, etc.)

Usage:

  • Key lookup: Get public key by key ID (kid)
  • Signature validation: Use public key to validate JWT signature
  • Key rotation: Automatically use new keys when rotated

Performance:

  • Caching: Cache keys with reasonable TTL
  • Revalidation: Check for new keys periodically
  • Lazy loading: Load keys on demand
  • Distribution: Cache across load balancers

Security:

  • HTTPS mandatory: Fetch keys over HTTPS only
  • Signature validation: Validate response signature
  • Rate limiting: Limit JWKS requests
  • Access control: Restrict JWKS access if needed

Standards & RFCs