JWKS Key Rotation & 'kid' Errors: Complete Guide + Online Decoder Tool
Stop JWKS validation headaches. Learn how key rotation and the 'kid' claim work, and use our FREE, expert online tool to decode, verify, and troubleshoot your JWKS tokens fast.
When JWT verification suddenly starts failing, key rotation is often the culprit. Understanding how JSON Web Key Sets (JWKS) work and how the kid (key ID) header drives key selection helps you debug verification failures and implement robust retry strategies.
π¨ Decoding Errors? Don't wait! Use our Advanced JWT Decoder & JWKS Tool now to instantly verify your tokens and check for
kidmismatches.
What is JWKS?
JWKS (JSON Web Key Set) is a collection of public keys used to verify JWT signatures. Issuers expose JWKS at a well-known endpoint (typically /.well-known/jwks.json) so verifiers can fetch the correct keys.
JWKS structure
A typical JWKS response looks like this:
{
"keys": [
{
"kty": "RSA",
"kid": "key-2024-01",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmb...",
"e": "AQAB"
},
{
"kty": "RSA",
"kid": "key-2024-02",
"use": "sig",
"alg": "RS256",
"n": "1wx8bhpfcHdTvQiLKYXZqtO0oondrRnc...",
"e": "AQAB"
}
]
}
Key fields:
kty- Key type (RSA,EC,octfor symmetric)kid- Key ID (unique identifier)use- Key usage (sigfor signatures,encfor encryption)alg- Algorithm (RS256,ES256,HS256, etc.)n,e- RSA public key material (modulus and exponent)x,y- EC public key material (coordinates)k- Symmetric key material (base64url)
How JWKS works
- Issuer generates keys - Creates public/private key pairs
- Issuer publishes JWKS - Exposes public keys at
/.well-known/jwks.json - Token includes
kid- JWT header specifies which key was used - Verifier fetches JWKS - Downloads keys from issuer endpoint
- Verifier selects key - Matches
kidfrom token header to JWKS - Verifier verifies signature - Uses selected public key
Example JWT header with kid
{
"alg": "RS256",
"kid": "key-2024-01",
"typ": "JWT"
}
The kid tells the verifier: "Use the key with ID key-2024-01 from the JWKS to verify this token."
What is the 'kid' (Key ID) and Why Does It Fail Validation?
The kid (Key ID) is a unique identifier in the JWT header that tells the verifier which public key from the JWKS endpoint should be used to verify the token's signature.
Why 'kid' is critical
- Key selection: When a JWKS contains multiple keys (common during rotation), the
kididentifies the exact key used to sign the token - Security: Prevents trying all keys (which is insecure and inefficient)
- Rotation support: Allows issuers to maintain multiple active keys during rotation periods
Why validation fails
Common failure scenarios:
- Missing
kid: Token header doesn't includekid, but JWKS requires it kidnot in JWKS: Thekidvalue in the token doesn't match any key in the fetched JWKS- Stale JWKS cache: Your cached JWKS is outdated and doesn't include the new key referenced by
kid - Wrong JWKS endpoint: Fetching JWKS from incorrect issuer URL
- Key rotation timing: New key added to JWKS, but your cache hasn't refreshed yet
How to diagnose:
- Decode your token header using our JWT Decoder to see the
kidvalue - Check the
iss(issuer) claim to identify the correct JWKS endpoint - Fetch the JWKS from
{iss}/.well-known/jwks.json - Verify the
kidexists in the JWKSkeysarray - If missing, refresh your JWKS cache and retry
Understanding key rotation
The Rotation Problem
Key rotation is necessary for security, but it introduces timing and caching challenges that cause validation failures:
Why rotation causes errors:
- Caching delays: Verifiers cache JWKS responses to reduce network calls. When a new key is added, cached JWKS doesn't include it immediately
- Timing windows: There's a gap between when the issuer adds a new key and when all verifiers refresh their caches
- Multiple active keys: During gradual rotation, both old and new keys exist simultaneously, requiring correct
kidmatching - Cache invalidation: Verifiers must detect when their cached JWKS is stale and refresh it
The caching problem:
// Problem: Cached JWKS doesn't have new key
const cachedJwks = {
keys: [
{ kid: "key-2024-01", ... } // Old key only
]
};
// New token uses new key
const token = {
header: { kid: "key-2024-02" }, // New key ID
// ...
};
// Verification fails: kid not found in cached JWKS
verifyToken(token, cachedJwks); // Error: Key with kid "key-2024-02" not found
The timing problem:
T+0: Issuer adds new key to JWKS
T+5m: Some verifiers refresh cache, see new key
T+30m: Your verifier still has stale cache
T+60m: Your cache expires, finally refreshes
During the T+5m to T+60m window, tokens signed with the new key fail validation if your cache hasn't refreshed.
Why rotate keys?
Security reasons:
- Compromised keys need replacement
- Regular rotation reduces attack window
- Compliance requirements (PCI DSS, SOC 2)
- Time-based rotation policies
Operational reasons:
- Key expiration
- Algorithm upgrades
- Multi-tenant key isolation
Rotation scenarios
Scenario 1: Gradual rotation (recommended)
What happens:
- New key added to JWKS (
kid: "key-2024-02") - Old key retained (
kid: "key-2024-01") - New tokens use new key
- Old tokens still verify with old key
- After old tokens expire, old key removed
Timeline:
Day 1: JWKS = [key-2024-01]
Day 30: JWKS = [key-2024-01, key-2024-02] β New key added
Day 60: JWKS = [key-2024-01, key-2024-02] β Both active
Day 90: JWKS = [key-2024-02] β Old key removed
Why this works:
- No service interruption
- Old tokens continue working
- Smooth transition
Implementation:
// Issuer adds new key but keeps old one
const jwks = {
keys: [
{ kid: "key-2024-01", ... }, // Old key (still valid)
{ kid: "key-2024-02", ... } // New key (active)
]
};
Scenario 2: Immediate rotation (risky)
What happens:
- Old key removed from JWKS
- New key added
- Tokens signed with old key fail verification
Problem:
- Active tokens become invalid
- Service disruption
- User sessions break
When it happens:
- Emergency key compromise
- Configuration mistakes
- Forced rotation without planning
Example failure:
// Token signed with old key
const token = {
header: { alg: "RS256", kid: "key-2024-01" },
// ...
};
// JWKS no longer has key-2024-01
const jwks = {
keys: [
{ kid: "key-2024-02", ... } // Only new key
]
};
// Verification fails!
verify(token, jwks); // Error: Key not found
Scenario 3: Multi-tenant/issuer
What happens:
- Different issuers have different JWKS endpoints
- Each tenant may have separate keys
- Verifier must fetch correct JWKS per issuer
Example:
// Tenant A tokens
const tenantAToken = {
header: { kid: "tenant-a-key-1" },
payload: { iss: "https://tenant-a.example.com" }
};
// Tenant B tokens
const tenantBToken = {
header: { kid: "tenant-b-key-1" },
payload: { iss: "https://tenant-b.example.com" }
};
// Different JWKS endpoints
const tenantAJwks = await fetch('https://tenant-a.example.com/.well-known/jwks.json');
const tenantBJwks = await fetch('https://tenant-b.example.com/.well-known/jwks.json');
How verifiers select keys
The kid matching process
Step-by-step:
- Decode token header - Extract
kidandalg(use our JWT Decoder for quick inspection) - Fetch JWKS - Download from issuer endpoint (with caching)
- Find matching key - Search JWKS
keysarray for entry wherekidmatches token headerkid - Verify algorithm - Ensure
algmatches expected - Verify signature - Use selected public key
Complete implementation with kid mapping:
async function getKeyFromJwks(token, issuer) {
// Step 1: Decode header to get kid
const [headerSegment] = token.split('.');
const header = JSON.parse(
Buffer.from(headerSegment, 'base64url').toString()
);
if (!header.kid) {
throw new Error('Token missing kid in header');
}
// Step 2: Fetch JWKS (with caching)
const jwks = await getJwks(issuer);
// Step 3: Map kid to correct key
const key = jwks.keys.find(k => k.kid === header.kid);
if (!key) {
throw new Error(`Key with kid "${header.kid}" not found in JWKS`);
}
return key;
}
// Enhanced JWKS fetching with caching
const jwksCache = new Map();
async function getJwks(issuer, forceRefresh = false) {
const cacheKey = issuer;
const cached = jwksCache.get(cacheKey);
// Return cached if still valid and not forcing refresh
if (!forceRefresh && cached && Date.now() < cached.expiresAt) {
return cached.jwks;
}
// Fetch fresh JWKS from issuer endpoint
const jwksUri = `${issuer}/.well-known/jwks.json`;
const response = await fetch(jwksUri);
if (!response.ok) {
throw new Error(`Failed to fetch JWKS: ${response.statusText}`);
}
const jwks = await response.json();
// Cache with 1 hour TTL
jwksCache.set(cacheKey, {
jwks,
expiresAt: Date.now() + 3600000 // 1 hour
});
return jwks;
}
Common mistakes
Mistake 1: Ignoring kid
Wrong approach:
// BAD - tries all keys
function verifyToken(token, jwks) {
for (const key of jwks.keys) {
try {
return verify(token, key); // Tries every key!
} catch (e) {
continue;
}
}
throw new Error('Verification failed');
}
Why it's bad:
- Security risk (wrong key might accidentally verify)
- Performance issue (tries multiple keys)
- Violates JWT spec
Correct approach:
// GOOD - uses kid to select key
function verifyToken(token, jwks) {
const header = decodeHeader(token);
const key = jwks.keys.find(k => k.kid === header.kid);
if (!key) {
throw new Error(`Key with kid ${header.kid} not found`);
}
return verify(token, key);
}
Mistake 2: Not validating algorithm
Wrong approach:
// BAD - doesn't check algorithm
function verifyToken(token, jwks) {
const key = findKeyByKid(token, jwks);
return verify(token, key); // Uses whatever alg is in key
}
Why it's bad:
- Algorithm confusion attacks
- Could accept wrong algorithm
- Security vulnerability
Correct approach:
// GOOD - validates algorithm
function verifyToken(token, jwks, allowedAlgorithms) {
const header = decodeHeader(token);
// Check algorithm is allowed
if (!allowedAlgorithms.includes(header.alg)) {
throw new Error(`Algorithm ${header.alg} not allowed`);
}
const key = findKeyByKid(token, jwks);
// Ensure key algorithm matches
if (key.alg && key.alg !== header.alg) {
throw new Error('Algorithm mismatch');
}
return verify(token, key);
}
Mistake 3: Not caching JWKS
Wrong approach:
// BAD - fetches JWKS on every request
async function verifyToken(token) {
const jwks = await fetch('https://issuer.com/.well-known/jwks.json');
return verify(token, jwks);
}
Why it's bad:
- Performance overhead
- Rate limiting issues
- Unnecessary network calls
Correct approach:
// GOOD - caches JWKS with TTL
const jwksCache = new Map();
async function getJwks(issuer, forceRefresh = false) {
const cacheKey = issuer;
const cached = jwksCache.get(cacheKey);
// Return cached if still valid
if (!forceRefresh && cached && Date.now() < cached.expiresAt) {
return cached.jwks;
}
// Fetch fresh JWKS
const jwks = await fetch(`${issuer}/.well-known/jwks.json`).then(r => r.json());
// Cache with 1 hour TTL
jwksCache.set(cacheKey, {
jwks,
expiresAt: Date.now() + 3600000
});
return jwks;
}
Robust retry strategy
The problem
When key rotation happens:
- Your cached JWKS is stale
- Token verification fails
- You need fresh JWKS
- Retry verification
Implementation
Basic retry pattern:
async function verifyTokenWithRetry(token, issuer) {
try {
// First attempt with cached JWKS
const jwks = await getJwks(issuer);
return verifyToken(token, jwks);
} catch (error) {
// If verification fails, refresh JWKS and retry once
if (error.message.includes('Key not found') ||
error.message.includes('kid')) {
const freshJwks = await getJwks(issuer, forceRefresh = true);
return verifyToken(token, freshJwks);
}
throw error; // Re-throw if not a key issue
}
}
Enhanced retry with logging:
async function verifyTokenWithRetry(token, issuer, options = {}) {
const maxRetries = options.maxRetries || 1;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const forceRefresh = attempt > 0; // Refresh on retry
const jwks = await getJwks(issuer, forceRefresh);
return verifyToken(token, jwks);
} catch (error) {
lastError = error;
// Only retry on key-related errors
if (error.message.includes('Key not found') ||
error.message.includes('kid') ||
error.message.includes('signature')) {
if (attempt < maxRetries) {
console.warn(`Verification failed, retrying (attempt ${attempt + 1})`, {
issuer,
kid: decodeHeader(token).kid,
error: error.message
});
continue;
}
}
// Don't retry for other errors
throw error;
}
}
throw lastError;
}
When to retry
Retry on:
- β
Key not found (
kidmissing from JWKS) - β Signature verification failure (might be stale key)
- β JWKS fetch errors (network issues)
Don't retry on:
- β Token expired (
expclaim) - β Invalid token format
- β Algorithm not allowed
- β Issuer mismatch
Real-world implementation examples
Node.js with jwks-rsa
Using jwks-rsa library:
const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');
const client = jwksClient({
jwksUri: 'https://issuer.com/.well-known/jwks.json',
cache: true,
cacheMaxAge: 3600000, // 1 hour
rateLimit: true,
jwksRequestsPerMinute: 5
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) {
return callback(err);
}
callback(null, key.getPublicKey());
});
}
function verifyToken(token) {
return new Promise((resolve, reject) => {
jwt.verify(token, getKey, {
algorithms: ['RS256'], // Pin allowed algorithms
issuer: 'https://issuer.com',
audience: 'my-api'
}, (err, decoded) => {
if (err) {
// Retry logic could go here
return reject(err);
}
resolve(decoded);
});
});
}
Python with PyJWT
Using PyJWT with cryptography:
import jwt
import requests
from functools import lru_cache
from datetime import datetime, timedelta
@lru_cache(maxsize=1)
def get_jwks(issuer, ttl=3600):
"""Fetch and cache JWKS with TTL"""
response = requests.get(f"{issuer}/.well-known/jwks.json")
response.raise_for_status()
return response.json()
def get_signing_key(token, issuer):
"""Get signing key from JWKS based on kid"""
header = jwt.get_unverified_header(token)
kid = header.get('kid')
if not kid:
raise ValueError('Token missing kid header')
jwks = get_jwks(issuer)
# Find key by kid
for key in jwks.get('keys', []):
if key.get('kid') == kid:
return key
raise ValueError(f'Key with kid {kid} not found in JWKS')
def verify_token_with_retry(token, issuer, max_retries=1):
"""Verify token with JWKS retry on key rotation"""
for attempt in range(max_retries + 1):
try:
key = get_signing_key(token, issuer)
decoded = jwt.decode(
token,
key,
algorithms=['RS256'],
issuer=issuer,
audience='my-api'
)
return decoded
except (jwt.InvalidTokenError, ValueError) as e:
if attempt < max_retries and 'kid' in str(e):
# Clear cache and retry
get_jwks.cache_clear()
continue
raise
Troubleshooting common issues
If you're experiencing validation failures beyond key rotation issues, see our complete guide on fixing invalid JWT errors for systematic troubleshooting of signature, algorithm, and claim validation problems.
Issue 1: Wrong issuer URL
Symptom:
Error: Key with kid "key-123" not found in JWKS
Cause:
- Fetching JWKS from wrong endpoint
- Issuer URL mismatch
Solution:
// Extract issuer from token
const payload = decodePayload(token);
const issuer = payload.iss; // Use exact issuer from token
// Fetch JWKS from issuer's well-known endpoint
const jwksUri = `${issuer}/.well-known/jwks.json`;
const jwks = await fetch(jwksUri).then(r => r.json());
Common mistakes:
- Hardcoding issuer URL
- Not handling trailing slashes
- Using wrong environment (dev vs prod)
Issue 2: Stale JWKS cache
Symptom:
Verification fails after key rotation
Cause:
- Cached JWKS doesn't have new key
- TTL too long
Solution:
// Shorter TTL during rotation windows
const jwksCache = {
ttl: 300000, // 5 minutes instead of 1 hour
// ...
};
// Or implement cache invalidation
function invalidateJwksCache(issuer) {
jwksCache.delete(issuer);
}
Issue 3: Algorithm mismatch
Symptom:
Error: Algorithm not allowed
Cause:
- Token uses different algorithm than expected
- Key algorithm doesn't match token algorithm
Solution:
// Pin allowed algorithms
const allowedAlgorithms = ['RS256']; // Only RS256
// Verify algorithm matches
const header = decodeHeader(token);
if (!allowedAlgorithms.includes(header.alg)) {
throw new Error(`Algorithm ${header.alg} not allowed`);
}
// Ensure key algorithm matches
const key = findKeyByKid(token, jwks);
if (key.alg && key.alg !== header.alg) {
throw new Error('Algorithm mismatch between token and key');
}
Issue 4: Multiple issuers/tenants
Symptom:
Verification fails for some tenants but not others
Cause:
- Using wrong JWKS for tenant
- Not isolating keys per tenant
Solution:
// Cache JWKS per issuer
const jwksCache = new Map();
async function getJwksForIssuer(issuer) {
if (!jwksCache.has(issuer)) {
const jwks = await fetch(`${issuer}/.well-known/jwks.json`).then(r => r.json());
jwksCache.set(issuer, jwks);
}
return jwksCache.get(issuer);
}
// Use issuer from token
function verifyToken(token) {
const payload = decodePayload(token);
const issuer = payload.iss;
const jwks = await getJwksForIssuer(issuer);
return verifyToken(token, jwks);
}
Best practices summary
- Cache JWKS with TTL - Reduce network calls, refresh periodically
- Select key by kid - Never try all keys, always match
kid - Pin allowed algorithms - Prevent algorithm confusion attacks
- Implement retry logic - Refresh JWKS on verification failure
- Handle multiple issuers - Cache JWKS per issuer
- Log key rotation events - Monitor for rotation issues
- Validate issuer - Ensure
issclaim matches expected - Use libraries -
jwks-rsa,PyJWThandle edge cases
Debugging with JWT Encoder/Decoder
Our JWT Encoder/Decoder helps you troubleshoot JWKS and kid issues:
- Inspect
kidin token header - See exactly which key ID your token references - View
issclaim - Identify the issuer to fetch the correct JWKS endpoint - Check
alg- Verify the algorithm matches your expectations - Decode without verification - Debug token structure before verification
- Expert Mode - Use JWKS field to verify keys are formatted correctly
Example debugging workflow:
- Paste token into our JWT Decoder to decode header
- Note the
kidvalue from the decoded header - Check
issclaim to identify the issuer URL - Fetch JWKS from
{iss}/.well-known/jwks.json - Search JWKS for the
kidvalue - if missing, your cache may be stale - Refresh JWKS cache and retry verification
- Verify algorithm matches between token header and JWKS key
If validation fails, use our tool to quickly check if the kid exists in your JWKS. Paste your JWKS URI into the tool's JWKS field to ensure keys are formatted correctly and the kid you're looking for is present.
Next steps
- Decode tokens with our JWT Encoder/Decoder to inspect
kidandissinstantly - Fix validation errors - See our complete guide on fixing invalid JWT errors for common issues
- Learn algorithms - Understand HS256 vs RS256 differences
- Master fundamentals - Read JWT tokens explained for the basics
Try JWT Encoder/Decoder Now
Ready to put this into practice? Use our free JWT Encoder/Decoder tool. It works entirely in your browser with no signup required.
Launch JWT Encoder/DecoderFrequently Asked Questions
Q What does the kid header mean?
It identifies which key in the JWKS should verify the signature; rotations change which key is valid.
Related Articles
JWT Tokens Explained - Complete Guide to JSON Web Tokens
Learn everything about JWT tokens, how they work, when to use them, and best practices for secure implementation in your applications.
HS256 vs RS256 β What Changes When Decoding?
Understand differences between symmetric and asymmetric JWT algorithms and what you can infer at decode time.
Fix βInvalid JWTβ Errors β Common Causes and Checks
Troubleshoot invalid JWT errors with a systematic checklist for decoding and validation.