JWT errors often look specific, but the message is only the starting point. The fastest way to debug them is to separate token shape, signature verification, and claim validation instead of changing keys or expiration settings at random.
Here is the checklist I use.
1. Start with the token shape
A compact JWT normally contains three Base64URL-encoded segments:
header.payload.signature
If your library reports jwt malformed, check the input before checking cryptography:
- Remove the
Bearerprefix. - Make sure the value contains exactly two dots.
- Check for quotes, whitespace, line breaks, or a truncated environment variable.
- Confirm you did not pass a refresh token or an opaque session token to a JWT verifier.
A quick JavaScript check:
const raw = authorizationHeader?.replace(/^Bearer\s+/i, "").trim();
const parts = raw?.split(".");
if (parts?.length !== 3) {
throw new Error("Expected a compact JWT with three segments");
}
Do not log production tokens while debugging. A JWT payload is encoded, not encrypted, and may contain user information.
2. Decode before you verify, but do not trust decoded data
Decoding is useful for inspecting alg, kid, iss, aud, exp, and nbf. It does not prove that the token came from your issuer.
Use decoded values as clues only. Authorization decisions must happen after signature and claim verification.
3. For invalid signature, compare the verification contract
This error usually means the verifier and issuer disagree about one of these:
- Algorithm: HS256 uses a shared secret; RS256/ES256 use a public key for verification.
- Key: development and production credentials may be different.
-
Key ID: with JWKS, the token's
kidmust match a currently published key. - Token bytes: copying, URL handling, or storage may have changed the token.
- Secret encoding: one service may treat a value as plain text while another expects Base64-decoded bytes.
Do not fix this by disabling signature verification or accepting every algorithm. Explicitly allow only the algorithms your issuer uses.
4. For jwt expired, inspect Unix time
The exp claim is measured in seconds since the Unix epoch, while JavaScript's Date.now() returns milliseconds.
const now = Math.floor(Date.now() / 1000);
if (payload.exp && payload.exp <= now) {
// Refresh through the trusted auth flow or require sign-in.
}
An expired access token should normally be refreshed through your authentication flow. Changing exp inside the payload does not create a valid token because the signature will no longer match.
5. Check audience, issuer, and not-before separately
A valid signature is not enough.
-
issshould identify the expected token issuer. -
audshould include your API or application. -
nbfmeans the token must not be accepted before that timestamp. - Small clock differences can be handled with a narrow tolerance, but a large tolerance hides configuration problems.
Keep expected issuer and audience values in configuration, and verify them explicitly in the backend.
A reliable debugging order
- Confirm the raw value has JWT shape.
- Decode the header and payload locally.
- Identify the algorithm and key source.
- Verify the signature with an explicit algorithm allowlist.
- Validate
exp,nbf,iss, andaud. - Compare development and production configuration.
- Reproduce with a newly issued token.
I turned this checklist into a browser-based reference covering the common messages, including malformed tokens, invalid signatures, expiration, audience, issuer, and not-before failures:
https://www.easyjwt.top/jwt-error-decoder
The accompanying decoder runs locally in the browser:
What JWT error has taken you the longest to diagnose? I would like to add more real failure cases to the guide.
Top comments (0)