JWT Guides

How I Debug JWT Key Rotation Without Breaking Verification

See the workflow I use to debug JWT key rotation, kid changes, overlapping JWKS keys, cache refresh problems and tokens that still need an older signing key.

By JWTDecoder.toolsLast verified 2026-09-16

Editorial voice: first-person phrasing describes the documented JWTDecoder.tools workflow; JWTDecoder.tools is the publisher and organizational author.

KEY A → A + B → KEY B
KEY A → A + B → KEY B · TTL · cache · exp

When JWT verification starts failing around a signing-key change, I treat it as a distributed key-lifecycle and cache problem, not just an invalid signature problem. A successful rotation has to coordinate key publication, signing cutover, verifier cache behavior, token lifetime, and eventual retirement of the old key.

If I have a token and the issuer's trusted key set, I start with the JWKS Validator and compare kid, signature result, and claim lifetime.

The safe sequence is publish before sign

For a planned asymmetric rotation from K1 to K2, the sequence I want is:

1. Publish public K2 in trusted JWKS while K1 remains.
2. Give verifiers time/a mechanism to learn K2.
3. Start signing new JWTs with private K2 / kid=K2.
4. Continue publishing public K1 for still-valid K1 tokens.
5. Retire K1 only when policy says old tokens no longer need it.

The dangerous order is “start signing K2, publish K2 later.” Every verifier with a K1-only cache will see an unknown kid and fail.

I inspect four values before touching cache settings

From the token:

{
  "alg": "RS256",
  "kid": "K2"
}

and payload:

{
  "iss": "https://login.example.com/",
  "iat": 1789200000,
  "exp": 1789203600
}

I want:

  • expected issuer;
  • kid;
  • issue time;
  • expiration time.

Those tell me which generation signed the token and how long the corresponding public key may still be needed.

A rotation timeline exposes failures better than a static JWKS dump

t0   JWKS = [K1]
t1   publish K2 -> JWKS = [K1, K2]
t2   begin signing K2
t3   old K1 tokens still arrive
t4   last acceptable K1 token expires
t5   remove K1 according to policy

If failures begin at t2, I inspect how fast verifiers learn K2. If failures begin at t5, I inspect whether K1 was removed before all required old tokens aged out.

Unknown kid should trigger a controlled refresh, not blind trust

A useful verifier strategy is:

lookup kid in cache
  found -> verify
  missing
    ↓
refresh from already trusted jwks_uri
    ↓
lookup again
  still missing -> reject

The refresh must remain bound to the issuer's trusted JWKS location. I do not let arbitrary JWT header URLs redefine where keys come from.

I prevent an unknown-kid thundering herd

If thousands of requests arrive with the same new kid, a naive implementation can make every worker fetch JWKS simultaneously.

I prefer controls such as:

  • single-flight/deduplicated refresh per issuer;
  • short backoff after failed refresh;
  • rate limiting;
  • normal HTTP cache semantics (Cache-Control, validators such as ETag when supported);
  • metrics for refreshes and unknown-kid events.

I also avoid long negative caching of an unknown kid, because that can prolong an outage immediately after a legitimate rotation.

Planned rotation and emergency revocation have different goals

For planned rotation I usually want continuity. For a compromised key I may intentionally want old K1 tokens to stop verifying before their exp.

ScenarioOld public key behavior
planned rolloverretain long enough for intended token acceptance window
suspected key compromiserevoke/retire according to incident policy, even if sessions break
issuer migrationexplicit migration rules; do not mix issuers casually

There is no universal “always keep old key until every token expires” rule for emergency events.

How I estimate the planned overlap window

I start from application facts, not an arbitrary number:

old-key publication overlap
  >= maximum remaining lifetime of K1-signed tokens
     + small clock/policy buffer as required by the system

I also make sure K2 is published before K2 signing by enough time or with an unknown-kid refresh mechanism that consumers actually implement.

Cache TTL affects how quickly new keys are learned; token lifetime affects how long old keys remain necessary. They solve different halves of rotation.

Stale JWKS cache has recognizable symptoms

Examples:

new tokens (kid=K2) fail
old tokens (kid=K1) pass
issuer JWKS already contains K2

That strongly suggests a verifier still holds a K1-only cache.

The opposite:

new K2 tokens pass
old K1 tokens suddenly fail
current JWKS contains only K2
old token exp is still in future

suggests K1 may have been removed before the intended overlap ended.

Duplicate kid is an ambiguity problem

A kid is a selection identifier, not a globally unique cryptographic identity. If a JWKS contains multiple applicable keys with the same kid, I do not silently choose the first one.

I filter kty, use, key_ops, alg, and curve where relevant, and reject remaining ambiguity. The JWT kid guide covers this selection layer.

Issuer binding still matters during rotation

My trust path remains:

expected issuer
  ↓
trusted discovery/metadata
  ↓
trusted jwks_uri
  ↓
K1/K2 candidates
  ↓
kid + metadata filtering
  ↓
signature verification

A K2 with the right kid from the wrong tenant is not a successful rotation.

I test both old and new tokens before declaring the rollout healthy

My rollout test set includes:

old K1 token, still unexpired -> should follow planned old-key policy
new K2 token                 -> must verify
unknown kid                  -> one controlled refresh, then reject if absent
bad signature with K2 kid    -> reject
wrong issuer with same kid   -> reject

That catches more than checking whether /jwks.json contains two objects.

I monitor rotation as an operational event

Useful metrics/log fields include:

  • JWT verification failure rate by reason;
  • unknown kid count;
  • JWKS refresh count and latency;
  • cache age;
  • selected issuer and kid (without logging whole tokens);
  • ratio of K1 vs K2 tokens during transition;
  • last successful JWKS refresh.

A rotation outage becomes much easier to diagnose when I can see “K2 unknown on 40% of instances” instead of a generic spike in 401s.

Key rotation decision matrix

SymptomLikely failureNext check
K2 tokens fail, K1 passesstale verifier cache / K2 published too latetrusted JWKS + refresh behavior
K1 tokens fail, K2 passesK1 retired too earlyold-token lifetime/policy
only one instance failsper-instance cache divergencecache age/refresh logs
every unknown kid causes many fetchesthundering herdsingle-flight/rate limit
same kid selects wrong keyambiguitykty/alg/use/key_ops/issuer
both K1 and K2 signatures failwrong issuer/key sourcetrust path

My production rotation workflow

  1. Determine maximum token lifetime and incident policy.
  2. Publish K2 public material in the issuer's trusted JWKS before using K2 to sign.
  3. Confirm consumers can see K2 or refresh safely on unknown kid.
  4. Begin issuing K2-signed tokens while K1 remains available for planned overlap.
  5. Verify representative K1 and K2 tokens with the JWKS Validator.
  6. Watch unknown-kid, signature-failure, cache-age, and refresh metrics.
  7. Keep K1 available for the intended old-token acceptance window in a planned rollover.
  8. Treat emergency compromise as a different policy that may intentionally invalidate sessions.
  9. Remove K1 only when the selected policy says it is safe.
  10. Keep algorithm, issuer, audience, and signature checks enabled throughout the rollout.

JWT key rotation is not merely “generate a new key.” It is a coordinated distributed transition where new keys must become discoverable before use and old keys must remain verifiable for exactly as long as policy requires — no longer, no shorter.

Sources

Related guides