← Debugging Guides

阅读中文版本

JWT kid Not Found — Key Rotation Breaks Verification

Fix JWT verification failures during key rotation by caching the JWKS by kid, refreshing on unknown kids, and matching the alg.

intermediatedebuggingapijwtauthentication

Symptoms

  • Verification fails with "Unable to find a signing key that matches the kid"
  • Tokens fail only after the identity provider rotates keys
  • Some tokens verify while others fail during rotation

Possible causes

  • The JWKS cache is stale and lacks the new kid
  • Tokens are verified with a single hardcoded key instead of selecting by kid
  • The signature algorithm does not match the key type (RS256 versus ES256)

Diagnosis steps

Decode the token header

node -e "const t=process.argv[1].split('.')[0]; console.log(JSON.parse(Buffer.from(t,'base64url')))" "$TOKEN"

Interpretation

The header alg and kid tell you which key and algorithm the issuer used.

Fetch and inspect the JWKS

curl -s https://idp.example.com/.well-known/jwks.json | jq '.keys[].kid'

Interpretation

If the kid in the token is absent from JWKS, the cache is stale and must be refreshed.

Root cause

JWT verification selects a public key by the token kid from the issuer JWKS. When keys rotate, a cached JWKS misses the new kid, so verification cannot find a matching key and fails until the cache refreshes.

Fix

Cache the JWKS and select by kid, refresh the JWKS once on an unknown kid before failing, validate the alg against an allowlist, and keep a short TTL so rotation is picked up quickly.

Risk note

Accepting the token alg blindly enables algorithm-confusion attacks; always verify alg against an allowlist tied to the key.

Verify the fix

Verify a token signed with the rotated key and confirm success; verify a token with an unknown kid triggers one refresh and then fails cleanly.

Symptoms

When you hit “JWT kid Not Found — Key Rotation Breaks Verification”, guessing wastes time. The guide below follows evidence: reproduce the symptom, run targeted diagnostics, identify the root cause, apply the fix, and verify it with concrete checks.

Observed symptoms include:

  • Verification fails with “Unable to find a signing key that matches the kid”
  • Tokens fail only after the identity provider rotates keys
  • Some tokens verify while others fail during rotation

Reproduce and Diagnose

Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.

Step 1: Decode the token header

node -e "const t=process.argv[1].split('.')[0]; console.log(JSON.parse(Buffer.from(t,'base64url')))" "$TOKEN"

The header alg and kid tell you which key and algorithm the issuer used.

Step 2: Fetch and inspect the JWKS

curl -s https://idp.example.com/.well-known/jwks.json | jq '.keys[].kid'

If the kid in the token is absent from JWKS, the cache is stale and must be refreshed.

Likely contributors, in order of frequency:

  • The JWKS cache is stale and lacks the new kid
  • Tokens are verified with a single hardcoded key instead of selecting by kid
  • The signature algorithm does not match the key type (RS256 versus ES256)

Root Cause

JWT verification selects a public key by the token kid from the issuer JWKS. When keys rotate, a cached JWKS misses the new kid, so verification cannot find a matching key and fails until the cache refreshes.

The Fix

Cache the JWKS and select by kid, refresh the JWKS once on an unknown kid before failing, validate the alg against an allowlist, and keep a short TTL so rotation is picked up quickly.

Watch out: Accepting the token alg blindly enables algorithm-confusion attacks; always verify alg against an allowlist tied to the key.

import { createRemoteJWKSet, jwtVerify } from 'jose';

const jwks = createRemoteJWKSet(new URL('https://idp.example.com/.well-known/jwks.json'));

export async function verify(token) {
  const { payload } = await jwtVerify(token, jwks, {
    algorithms: ['RS256'],              // allowlist, tied to the key
    issuer: 'https://idp.example.com/',
    audience: 'api.example.com',
  });
  return payload;
}
// createRemoteJWKSet refreshes automatically on an unknown kid

Verify and Prevent

Verify a token signed with the rotated key and confirm success; verify a token with an unknown kid triggers one refresh and then fails cleanly.

Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.

This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.