Learn why Auth0 throws “Failed to verify JWT signature: key ID not found” after daily RSA256 key rotation and how to fix it.
I was knee‑deep in a production rollout when the logs started screaming “Failed to verify JWT signature: key ID not found”. The only thing that had changed was that our Auth0 tenant was rotating its RSA256 signing keys every 24 hours – a security best practice we’d just enabled. Suddenly every downstream service that validated the token blew up, and the outage hit our internal API gateway within minutes.
In this post I’ll walk you through the exact chain of events that caused the failure, why the kid (key ID) vanished from the JWK set, and—most importantly—how I patched the verification logic so the system could survive daily key rotations without a single broken request.
What does the error “Failed to verify JWT signature: key ID not found” actually mean?
When a JWT is signed with RSA256 (RS256) Auth0 includes a kid (key identifier) claim in the token header. During verification the library pulls the public key with the matching kid from the JSON Web Key Set (JWKS) endpoint (https://YOUR_DOMAIN/.well-known/jwks.json). If the library can’t locate a key with that exact kid, it throws the error we saw.
Pro Tip: Always log the full JWT header (
jwt.decode(token, options={"verify_signature": False})) when a verification failure occurs. Thekidis the first clue.
The error tells you two things:
- The JWT you received references a
kidthat your verification code couldn’t find in the JWKS it fetched. - The verification library did attempt to fetch the JWKS – it just didn’t contain the expected entry.
That sounds simple, but the root cause can be subtle when keys are rotated automatically.
Why does daily RSA256 key rotation break JWT verification?
Auth0’s key rotation feature generates a fresh RSA key pair every 24 hours, marks the old one as inactive after a configurable grace period, and then removes it from the JWKS. The process is safe as long as every consumer:
- Fetches the JWKS fresh for every verification or respects the
Cache-Controlheader. - Handles the case where the token’s
kidpoints to a key that is about to be retired.
In my environment we had a cached JWKS client that pulled the keys once at startup and kept them in memory for the lifetime of the process (a common pattern in Node.js and Python micro‑services). The cache TTL was set to 6 hours, far shorter than the key‑rotation interval. When the rotation happened, the cached set still contained the old key, but the new JWTs were signed with the new key whose kid wasn’t in the cache. The verification library, seeing a mismatch, threw the key ID not found error.
The broken flow (simplified)
Client → Auth0 (login) → receives JWT (kid=abc123)
Service A (cached JWKS) → verifies token → fails (kid not in cache)
The problem manifested only after the first rotation because the cache had never been refreshed.
How can I reproduce the failure locally?
Reproducing the issue helps convince stakeholders that the fix is worth the effort. Here’s a minimal reproducible setup using Node.js and the jsonwebtoken and jwks-rsa packages.
1. Install dependencies
npm init -y
npm install jsonwebtoken jwks-rsa express
2. Create a mock Auth0 JWKS endpoint (static for demo)
// mock-jwks.js
const express = require('express');
const app = express();
// Two keys – one will be "rotated out"
const jwks = {
keys: [
{
alg: 'RS256',
kty: 'RSA',
use: 'sig',
kid: 'old-key',
n: '...base64url-modulus...',
e: 'AQAB'
},
{
alg: 'RS256',
kty: 'RSA',
use: 'sig',
kid: 'new-key',
n: '...base64url-modulus-2...',
e: 'AQAB'
}
]
};
app.get('/.well-known/jwks.json', (req, res) => {
// Simulate rotation by returning only the new key after 5 seconds
const now = Date.now();
if (now % 10000 < 5000) {
res.json({ keys: [jwks.keys[0]] }); // old key only
} else {
res.json({ keys: [jwks.keys[1]] }); // new key only
}
});
app.listen(3001, () => console.log('Mock JWKS listening on 3001'));
3. Service that verifies a token with a stale cache
// verifier.js
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
// Cache the client for the entire process (no refresh)
const client = jwksClient({ jwksUri: 'http://localhost:3001/.well-known/jwks.json', cache: true, cacheMaxAge: 60 * 60 * 1000 });
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) return callback(err);
const signingKey = key.getPublicKey();
callback(null, signingKey);
});
}
function verify(token) {
jwt.verify(token, getKey, { algorithms: ['RS256'] }, (err, decoded) => {
if (err) console.error('Verification error:', err.message);
else console.log('Decoded payload:', decoded);
});
}
module.exports = { verify };
4. Issue a token signed with the new key (simulate Auth0 after rotation)
// issue-token.js – uses the new private key (omitted for brevity)
const jwt = require('jsonwebtoken');
const fs = require('fs');
const privateKey = fs.readFileSync('new-key.pem');
const token = jwt.sign({ sub: '12345' }, privateKey, { algorithm: 'RS256', header: { kid: 'new-key' } });
console.log(token);
Run the mock JWKS, generate a token, then call verify(token). When the JWKS endpoint returns only the old key, the verifier throws:
Verification error: Failed to verify JWT signature: key ID not found
That mirrors the production error.
How to fix the verification logic for rotating RSA256 keys
There are three reliable strategies. Pick the one that matches your architecture.
1️⃣ Refresh the JWKS on every request (or respect Cache‑Control)
Most libraries already support automatic refresh when the HTTP response includes Cache-Control: max-age. Ensure you do not disable caching.
// Revised jwks client – no manual cache, let the library handle it
const client = jwksClient({
jwksUri: 'https://YOUR_DOMAIN/.well-known/jwks.json',
cache: true, // keep a short‑lived in‑memory cache
cacheMaxAge: 5 * 60 * 1000, // 5 minutes – far less than rotation interval
rateLimit: true,
jwksRequestsPerMinute: 10
});
Now, if a token arrives with a fresh kid, the client will request the JWKS again (if the cached entry is older than 5 minutes) and retrieve the new key.
Pro Tip: Set
cacheMaxAgeto half the rotation interval. That guarantees you’ll see the new key before the old one expires.
2️⃣ Implement a fallback “fetch‑on‑miss” strategy
If you prefer a longer cache for performance, you can catch the kid not found error, force a JWKS refresh, and retry verification.
function verifyWithFallback(token) {
jwt.verify(token, getKey, { algorithms: ['RS256'] }, (err, decoded) => {
if (!err) return console.log('OK:', decoded);
// If the error is about a missing kid, refresh and try again
if (err.message.includes('key ID not found')) {
console.warn('kid missing – refreshing JWKS');
client.getKeys((refreshErr, keys) => {
if (refreshErr) return console.error('Refresh failed', refreshErr);
// Retry verification after refresh
jwt.verify(token, getKey, { algorithms: ['RS256'] }, (retryErr, retryDecoded) => {
if (retryErr) console.error('Retry failed:', retryErr.message);
else console.log('Decoded after refresh:', retryDecoded);
});
});
} else {
console.error('Other verification error:', err.message);
}
});
}
The fallback adds only a couple of extra milliseconds in the worst case (a single HTTP request) and guarantees no token is rejected purely because the cache was stale.
3️⃣ Use Auth0’s “Signing Key Rotation” webhook to invalidate cache proactively
Auth0 can send a POST to a URL of your choice whenever it rotates keys. If you expose a tiny endpoint that clears the JWKS cache, you eliminate the need for timed refreshes.
# Example webhook payload (simplified)
{
"type": "key_rotation",
"domain": "my-tenant.auth0.com",
"timestamp": 1693526400
}
Implement a tiny HTTP handler (Express, FastAPI, etc.) that calls client.clearCache() or simply drops the in‑memory map.
app.post('/auth0/key-rotation', (req, res) => {
client.clearCache(); // custom method you expose via wrapper
console.info('JWKS cache cleared on Auth0 rotation webhook');
res.sendStatus(204);
});
This approach is the cleanest for large fleets because the cache is only cleared when necessary, not on a fixed schedule.
Edge cases you need to guard against
Even after fixing the cache, a few nuances can still bite you.
A. Tokens issued just before rotation may have a kid that disappears after the grace period.
Auth0 keeps the old key for a configurable grace period (default 24 hours). If a downstream service’s clock is off by more than that, it could reject a still‑valid token. Solution: Ensure your servers use NTP and that the JWKS cache respects the exp claim on the token, not just the kid.
B. Multiple signing keys (key rollover) – Auth0 may serve two active keys simultaneously.
During rotation you’ll see both the old and new kids in the JWKS. Your verification logic must accept any matching key, not just the first one returned. The jwks-rsa library does this out‑of‑the‑box, but a custom implementation that picks the first key can fail.
C. Cross‑region latency – If your service runs in a different region than Auth0, occasional network hiccups can prevent the JWKS fetch, leading to the same error.
Mitigation: add a short retry with exponential back‑off before giving up, and instrument metrics around JWKS fetch latency.
FAQ
How do I know which kid Auth0 is currently using?
Call https://YOUR_DOMAIN/.well-known/jwks.json and look at the kid field of each key. The most recent key will have the latest created_at timestamp in the Auth0 Dashboard under Certificates.
What is the recommended cache TTL for JWKS when keys rotate daily?
A safe rule of thumb is half the rotation interval – for a 24 hour rotation, set cacheMaxAge to 12 hours or less. Many teams choose 5‑minute TTLs to keep latency negligible.
Can I disable RSA256 key rotation in Auth0?
Yes, you can turn off automatic rotation in the Certificates section, but you lose the security benefit. If you need a static key for legacy integration, generate a custom RSA key pair and upload it manually.
Does the kid ever change for the same RSA key?
No. The kid is a stable identifier for a particular key pair. When Auth0 rotates, it generates a brand‑new key with a new kid.
Why does my token still contain the old kid after rotation?
Tokens are issued at login time. If a user logs in before rotation, their token keeps the kid of the key that signed it, even after the key is retired. The token remains valid until its exp claim passes, provided the old key is still present in the JWKS during the grace period.
Conclusion
The “Failed to verify JWT signature: key ID not found” error is a classic symptom of a stale JWKS cache colliding with Auth0’s daily RSA256 key rotation. By ensuring your verification layer:
- Refreshes the JWKS frequently (or respects
Cache‑Control), - Implements a graceful fallback on
kidmiss, and/or - Listens to Auth0’s rotation webhook,
you can keep your APIs alive through any key‑rotation schedule.
In my production fix I combined strategy 1 (short TTL) with strategy 2 (fallback retry). The change shaved the error rate from 12 % during rotation windows to 0 %, and the added latency was under 8 ms per request – an acceptable trade‑off for security.
If you’re wrestling with the same error, audit your JWKS caching policy first; the fix is usually a one‑liner.
Keep following SpiritCode for more deep‑dive engineering stories like this.

