Why Does JWT Signature Verification Fail?
Signature verification fails when the bytes the verifier hashes are not the bytes that were signed, or when the key it imports is not the key that signed. The five common causes are the wrong secret, an algorithm mismatch, a mutated payload, a base64url decoding error and a key in the wrong format.
What the signature actually covers
For HS256 the signing input is exactly base64url(header) + "." + base64url(payload). The third segment is the raw 32-byte HMAC-SHA256 digest of that string, written in base64url. Nothing else is signed. A single changed byte in either of the first two segments, including a whitespace character, produces a different digest and the check fails.
1. The secret is not the bytes you think it is
An HMAC key is a byte string, not a password with a character set. Libraries disagree about what to do with a string secret. Some encode it as UTF-8, which is the safe default. Others expect a base64 value and decode it first. When one service signs with the UTF-8 bytes of s3cr3t and the other verifies with the six decoded bytes, both are doing the right thing with the wrong key. Pick one convention and write it down.
2. The header algorithm is not the verifier algorithm
The alg field is attacker-controlled data inside the token. A verifier that reads it and obeys it can be pushed to a weaker algorithm. The classic failure is a token whose header says HS256 while the service holds a public key for RS256: the verifier computes an HMAC using the public key text as the secret, and an attacker can do the same. Pass an explicit allowlist to the verifier and reject everything else, including none.
3. The payload was re-serialised on the way
Some gateways and logging proxies decode a token, pretty-print the JSON and re-encode it. The claims look identical and the signature fails, because the signature covers the original bytes and not the claim values. The same thing happens when a token is rebuilt from a parsed object with a different key order or with non-ASCII text escaped differently. Verify the token before any component rewrites it.
4. base64url and base64 are not the same encoding
JWT uses base64url: - and _ replace + and /, and padding is stripped. A verifier that decodes with the standard alphabet throws on a token that is perfectly valid. A token that travels through a query string can also lose a + if the receiver URL-decodes the value, because a decoded + becomes a space. Decode with a base64url decoder that tolerates missing padding.
5. The key was imported in the wrong format
PEM files carry their own type. BEGIN RSA PRIVATE KEY is PKCS#1 and BEGIN PRIVATE KEY is PKCS#8; a verifier that expects PKCS#8 rejects the first. Windows line endings break some parsers. A JWK needs kty, n and e for RSA, and crv, x and y for EC. ES256 signatures are the 64-byte pair r||s, not the DER sequence that OpenSSL prints, so a DER signature fails verification in a JWT library even though both sides use the same key.
"Wrong number of segments"
Split the token on . and expect exactly three parts. Four parts means an extra dot inside a segment. Two parts means a truncated copy. A trailing newline from a text file belongs to the last segment and breaks the digest. Trim the value before you split it.
Failures that are not signature failures
An expired token, a token used before nbf, a wrong iss or an unexpected aud all fail verification while the signature is valid. Check the error code: a signature error names the crypto step, a claims error names the claim. Reporting both as "invalid token" turns a five-minute fix into a debugging session.
Diagnose in this order
Split the token. Decode both segments with a base64url decoder and confirm the JSON parses. Recompute the digest with the same key bytes and compare it with the third segment byte for byte. Only then read the claims. LochBot's JWT verifier runs those steps in your browser with WebCrypto, so the key never leaves the tab. Use the JWT decoder when you only need to read the claims. For the choice between signing algorithms, see HMAC-SHA256 or RSA for JWT signatures.
Common questions
What does it mean when a JWT has the wrong number of segments?
The token did not split into exactly three parts on the dot. Four parts means an extra dot inside a header or payload value, and two parts means a truncated copy. Trim trailing whitespace and split on the dot before decoding.
Is an expired JWT a signature failure?
No. The signature can be valid while the exp, nbf, iss or aud claims fail. Check the error code: a signature error names the crypto step and a claims error names the claim.
Why does the same token verify in one library and fail in another?
The two libraries usually disagree about the secret bytes or the base64 alphabet. One may encode a string secret as UTF-8 while the other decodes it as base64, and one may decode segments with the standard base64 alphabet instead of base64url.
Scan your system prompt with LochBot — free, client-side, no data sent anywhere.