← Back to DevDecoder

How to debug a JWT that won't validate

A checklist for the nine things that commonly break JWT verification.

You're holding a JWT in your hand. You paste it into a decoder — the payload looks right, the expiration is in the future, the claims match what you expect. But when the client sends it to the server, verification fails. This is one of those situations where the error message is almost always unhelpful: libraries log "invalid token" or "signature verification failed" and stop there.

The good news: there are about nine things that go wrong, and they're nearly always one of them. This is a working checklist. Go through it in order — the causes are ranked roughly by how often they're the culprit.

1. The wrong key, full stop

The single most common cause. HS256 tokens are signed with a shared secret; RS256 and ES256 tokens are signed with a private key and verified with the corresponding public key. If the verifier doesn't have the exact right key, the signature check will fail — even if everything else is correct.

Concrete checks:

A fast sanity check: sign a known payload with your key on both sides, compare the output. If they differ, the key isn't the same.

2. Clock skew

JWTs carry iat (issued at), nbf (not before), and exp (expiration). Most libraries compare these against the verifier's local clock with zero tolerance. If the verifier's clock is even a few seconds ahead of the issuer's, a freshly-issued token can fail with "token used before issued" or "not yet valid." If the verifier's clock is behind, an expired token might silently validate.

Fix: configure a clock skew tolerance on your verifier — 30 to 60 seconds is standard. Most libraries expose this as leeway, clockTolerance, or clock_skew. Also run NTP on every machine involved.

3. Algorithm mismatch

The token's header has an alg field. Your verifier should pin the expected algorithm, not trust whatever the token declares. If the issuer signs with RS256 and your verifier is configured for HS256, verification will fail — possibly with a misleading error about invalid key format.

Related: if you only ever accept HS256, make sure your library isn't willing to fall through to RS256 with a public key that got accidentally passed as the secret — this is the root of the classic "HS256/RS256 confusion" vulnerability. Pin the algorithm explicitly.

4. The alg: none bug

If your verifier accepts alg: none, an attacker can forge any token they like — no key required. This is a library bug that has recurred multiple times since 2015. Audit for it.

The JWT spec defines a none algorithm for unsigned tokens. A broken verifier that naively trusts the alg header will happily accept a token with the signature part left empty. The defense is what was mentioned above: always pin the expected algorithm at the verifier, and make sure your library rejects none by default (modern libraries do).

5. Audience or issuer mismatch

If you issue JWTs for multiple services, you should be checking:

These are often the difference between a token that validates in staging and one that fails in production. Staging and prod typically have different issuer URLs, and a token minted for staging will be structurally valid but will fail the iss check on prod.

6. Claims you forgot to check

A valid signature only proves the token came from someone who held the signing key. It doesn't prove the token was meant for your endpoint, or for the action being attempted. Common claims that should be checked but often aren't:

The JWT library's built-in validation usually covers signature, exp, nbf, and optionally iss/aud. Everything else is application logic you have to write.

7. Base64URL vs. Base64 confusion

JWT parts are Base64URL-encoded, not standard Base64. If something in your pipeline — a proxy, a logging layer, a custom middleware — has re-encoded the token through a standard Base64 decoder and re-encoder, the +// characters will have been swapped for -/_ (or vice versa), and the signature won't match.

A quick test: count the +, /, -, and _ characters in the incoming token. If you see any + or /, the token has been re-encoded at some point. Find where and fix it.

8. Character encoding drift

HMAC signatures are computed over the exact byte sequence of header.payload. If anything in transit or storage converts between UTF-8 and another encoding (UTF-16, Windows-1252, Latin-1), bytes can shift and the HMAC will fail to verify. This is rare in pure HTTP flows but common when tokens transit through message queues, database columns with the wrong collation, or legacy SOAP gateways.

Ensure every layer from token generation to verification treats the token as an opaque ASCII byte sequence. Don't "normalize" it.

9. Key rotation lag

If your issuer rotates signing keys (which it should), and your verifier caches the public key, there's a window where the issuer is signing with a new key but the verifier still has the old one. Fix with a short cache TTL on the JWKS endpoint (5–15 minutes), and make sure the verifier will re-fetch on an unrecognized kid.

A simple debugging order

When a token fails, work through this list top to bottom:

  1. Decode the token (e.g. on DevDecoder's JWT page). Is the header sane? Is the payload what you expect?
  2. Is the exp in the future? Is the nbf in the past? Is the clock on the verifier correct?
  3. Does the iss match the trusted issuer string exactly? Does aud contain your service?
  4. Is the alg what your verifier expects? Is alg: none being rejected?
  5. Is the kid (if present) a key your verifier currently knows about?
  6. Sign a test token with your verifier's configured key and compare the signature bytes against the failing token's signature bytes (for HMAC).
  7. Check for Base64URL vs. standard Base64 corruption anywhere in the pipeline.

Eight out of ten JWT-won't-validate incidents resolve at step two or three.

What a decoder can and can't tell you

A pure-browser JWT decoder (like the one on DevDecoder) can show you the header, the payload, and whether the token is expired based on your local clock. It can't verify the signature — that requires the secret key (HMAC) or the public key (RSA/ECDSA), which would have to be pasted in. The safer pattern is to inspect the structure client-side, then run signature verification in a trusted environment: your backend, a CLI tool like jwt-cli, or a local script.

When debugging, the inspector is usually enough: most JWT failures are not signature failures in the cryptographic sense — they're claim mismatches that a good look at the payload will reveal.