Debugging JWT Signature Verification Failures
A token that verified yesterday now returns “invalid signature”, nothing obvious changed, and the error tells you nothing beyond the fact that it failed. This page is part of the validating JWTs and rotating signing keys guide and gives an order of investigation that resolves most cases in a few minutes.
Start by Reading the Header
Decode the token’s header — it is unauthenticated data, so read it for diagnosis only — and compare three fields against your configuration. That comparison narrows the cause immediately.
# Decode the header only — no verification, purely diagnostic.
echo "$TOKEN" | cut -d. -f1 | base64 -d 2>/dev/null | jq .
# → {"alg":"RS256","kid":"2026-07-a","typ":"JWT"}
# Is that kid actually published right now?
curl -s "$JWKS_URI" | jq '.keys[].kid'
If the key identifier is absent from the published set, the token was signed by a key that has already been withdrawn — the overlap window was too short — or by a different issuer entirely. If it is present but your process still fails, your cached copy is stale and the refetch path is not working.
The Rotation Case
By far the most common cause is a verifier holding an old key set. Confirm it by comparing what the process has cached against what the endpoint publishes; if they differ, the bug is in your caching rather than in the token.
Two settings fix it permanently. Cap the cache at five to ten minutes so a rotation lands quickly on its own, and refetch once — rate-limited — when an unknown key identifier arrives so the recovery is immediate rather than delayed. A verifier with an unbounded cache and no refetch will keep failing until the process restarts, which is why the classic symptom is “it started working after a deploy” with nobody understanding why.
Emit the active key identifiers as a metric from every verifier. A fleet where one instance is behind produces intermittent failures that are almost impossible to reproduce from a single request, and a dashboard turns that into an obvious outlier.
The Encoding Case
When the algorithm and key both match and the signature still fails, the token has usually been altered in transit rather than forged.
Compare the token your verifier received against the one the issuer produced, byte for byte, and check for surrounding whitespace, a Bearer prefix left in place, or a value that has passed through a URL parameter and been decoded twice. Storage is a frequent culprit: a varchar(255) column silently truncates a token that outgrew it when a claim was added.
Separating Forgery From Misconfiguration
Once the mechanical causes are eliminated, ask whether this is an attack. A single failing token from one client is almost always configuration; a stream of failures with varying key identifiers, unusual algorithms, or alg: none is somebody probing.
Log enough to tell the difference: the algorithm, the key identifier, the issuer claim, the source address and the client version, with the token itself hashed rather than stored. Alert on the shape rather than the volume — any occurrence of an algorithm outside your allowlist is worth a notification, because legitimate clients never produce one.
Keep returning a single generic failure to the caller regardless of cause. Telling a client whether the signature, the audience or the expiry failed is a debugging aid for whoever is forging tokens, and the detail belongs in logs your team can query instead.
Instrumenting So the Next One Is Faster
Most of the time spent on these incidents goes into establishing facts that the system could have recorded for free. Four fields on every verification failure turn a half-day investigation into a query.
Record the algorithm and key identifier from the header, the issuer claim from the payload, the specific validation step that failed, and a hash of the token rather than the token itself. With those, “is this rotation or forgery?” is answerable by grouping: rotation produces a single unknown key identifier across many users, while probing produces varied algorithms and issuers from few sources.
Expose the currently cached key identifiers as a metric from each verifier, and the timestamp of the last successful key-set fetch. A rotation that has not reached one instance shows up immediately as an outlier, which is otherwise the hardest variant to diagnose because most requests succeed.
Add a synthetic check that mints or fetches a token from the provider on a schedule and verifies it through the same code path your services use. It fails minutes after a rotation problem begins rather than when a user reports it, and it distinguishes “our verifier is broken” from “this particular client is sending something odd”.
Finally, keep a small corpus of deliberately invalid tokens in your test suite — expired, wrong audience, wrong issuer, unknown key, alg: none, truncated signature — and assert that each is rejected with a distinct internal reason. When a real failure arrives, you can compare its recorded reason against the corpus and know instantly which category it belongs to.
When the Token Is Fine and the Verifier Is Wrong
Occasionally the token is valid and the configuration is not. The usual culprits are an issuer string that differs by a trailing slash, an audience configured for a different environment, a clock skew large enough that a freshly minted token appears to be from the future, and a maxTokenAge setting stricter than the provider’s actual token lifetime.
Each of those produces an error that names a different check than the one the operator expects, which is why they consume disproportionate time. A wrong audience reports an audience failure, but the underlying cause is usually a configuration value copied from another environment rather than anything about the token itself.
Keeping a known-good configuration snapshot per environment makes this comparison quick rather than archaeological.
Check the clock first, because it is the cheapest to eliminate and the most surprising: a host that has drifted by a few minutes will reject valid tokens with an expiry error while every other system appears healthy. Then compare the configured issuer and audience against the values in a freshly captured token, character for character — these are string comparisons, and a trailing slash counts.
Frequently Asked Questions
Why did verification start failing without any deployment?
Because something changed on the issuer’s side — almost always a key rotation. Your provider published a new key, started signing with it, and your verifier is still holding a cached set that does not contain it. Confirm by comparing the token’s key identifier against the live endpoint, then fix the cache lifetime and the refetch-on-unknown-key path so the next rotation is invisible.
Is it safe to decode a token without verifying it?
For diagnosis, yes, as long as you never act on the contents. Reading the header to find the algorithm and key identifier is exactly how you debug a failure. What is not safe is any code path that reads claims before verification and uses them for a decision — including selecting the key from a value inside the token, which is how key-injection attacks work.
What does "invalid signature" mean when the key is correct?
That the bytes being verified are not the bytes that were signed. Look for truncation, encoding changes, or a payload modified after issuance. Compare lengths against a known-good token, check for a stray Bearer prefix or whitespace, and confirm no storage layer between issuer and verifier has a length limit shorter than your tokens.
Should I fall back to trying every published key?
No. Select by key identifier and reject when it is unknown after one refetch. Trying every key hides distribution problems, wastes CPU on every failed request, and removes the signal that tells you a rotation went wrong. If tokens arrive without a key identifier at all, that is worth fixing at the issuer rather than working around at the verifier.
How do I reproduce the failure locally?
Capture the exact token — hashed in logs, but retrievable from a support session or a test client — and replay it against your verifier with the same configuration. Most “cannot reproduce” cases come from testing with a freshly minted token rather than the failing one, which quietly avoids the truncation, staleness or encoding problem that caused the original failure.
An Investigation Order That Works
Related
- Validating JWTs and rotating signing keys — the validation ladder and the rotation overlap window.
- Caching JWKS without breaking key rotation — the cache settings that prevent this class of failure.
- Configuring identity providers for OIDC — issuer and discovery validation.