12 August 2026
Why a JWT fails to verify
A verification failure has four possible layers - structure, key resolution, signature and freshness - and the error message rarely says which one.
A library that refuses a token usually says so in three or four words. invalid signature, JWT verification failed, unable to find a signing key. Those
sentences describe the last thing that went wrong, which is not always the first
thing that went wrong, and they are the reason a verification bug can hold up an
afternoon.
It helps to treat verification as four separate questions asked in order. A token that fails does so at exactly one of them, and knowing which narrows the causes from dozens to a handful.
1. Structure
Before anything cryptographic happens, the token has to be the shape it claims to be. A compact JWS is three base64url segments separated by dots; a compact JWE is five. Failures here are usually transport rather than cryptography:
- A truncated token. A column with a length limit, a log line that was cut, a header that hit a proxy's size cap. The signature check will fail, but the cause is the storage.
- Base64 rather than base64url.
+and/where-and_belong, or padding that should not be there. Common when a token has been through a system that re-encoded it helpfully. - Whitespace. A trailing newline from a shell pipeline, or a space introduced by a copy out of a wrapped terminal.
- The wrong artifact entirely. Five segments when the code expects three means the sender encrypted where the recipient expected a signature.
A decoder that tolerates malformed input is more useful here than a library, because a library's job is to refuse. Anything that reads the segments and reports what it found - rather than stopping at the first thing it dislikes - will name the problem in one step.
2. Key resolution
The token is well formed, and now the recipient has to decide which key to check
it with. The kid header names one; a JWKS endpoint publishes the set.
- An unknown
kid. The signer has rotated and the verifier is holding a cached key set. Most libraries cache a JWKS for a fixed period, so a rotation produces failures that stop on their own after the cache expires - which is what makes them hard to reproduce. - No
kidat all, and more than one key published. Some verifiers try every key until one works. That succeeds, and it is worth knowing it is happening, because a verifier that tries every key is not checking which key signed. - The wrong key set. Staging tokens against production keys is the usual version of this, and the error is indistinguishable from a bad signature.
The distinction that matters: no key was found and a key was found and did not match are different failures with different causes, and several libraries report both as an invalid signature.
3. Signature
Now the arithmetic. If the structure is intact and the right key was used, a failure here means the bytes that were signed are not the bytes being checked.
- The payload changed after signing. Re-serialising JSON is enough. A verifier that parses and re-encodes before checking will fail on key ordering or whitespace alone, because the signature covers the exact encoded bytes.
- Algorithm confusion. The token says
HS256, the verifier expectsRS256, and the code passes the public key as the HMAC secret. The token verifies, and it should not - this is the failure worth testing for deliberately, because it is the one that does not produce an error. alg: none. A token declaring itself unsigned. Any verifier that honours that header accepts anything.
The last two are the reason a verification suite needs tokens that are wrong in specific ways, rather than one valid token and the assumption that everything else fails.
4. Freshness
The signature is real and the key is right. What remains is time.
expin the past. The straightforward case.nbfin the future. Almost always clock difference rather than an attack. Two machines a few seconds apart will produce intermittent failures under load and none in a test.- No leeway, or too much. A verifier with no tolerance rejects tokens that were valid when they were sent; one with minutes of tolerance accepts tokens that expired minutes ago.
Freshness failures are the ones that pass in development and fail in production, because development rarely has two clocks.
Narrowing it down
The order matters because a failure at one layer is often reported as a failure
at another. A truncated token reads as a bad signature. An unknown kid reads as
a bad signature. A re-serialised payload reads as a bad signature. Working down
the list - is it the right shape, was the right key found, does the arithmetic
hold, is it still in date - turns one unhelpful message into one specific cause.
The Inspector reads a token layer by layer and names what it finds
at each, including the cases a library refuses to parse. It needs no account, and
nothing pasted into it is stored. For the failures that should be tested rather
than debugged - a stripped signature, an alg of none, a payload tampered with
after signing - the test tokens list what each one is and the outcome
a service ought to reach.