10 Common JWT Decoding Errors and How to Fix Them
Debug and fix the most common JWT token errors: malformed tokens, expired claims, algorithm mismatches, Base64url padding, and signature failures. Step-by-step solutions with code examples.
The Problem
JWT tokens silently fail in production, return cryptic errors like 'jwt malformed' or 'invalid signature', and debugging them requires understanding Base64url encoding, JSON structure, and cryptographic algorithms simultaneously.
You paste a JWT into your code and get a cryptic error. The token looks fine, the docs say it should work, but something is wrong. JWT decoding errors are frustratingly common because tokens combine three different encoding layers (JSON, Base64url, cryptographic signatures) and each layer has its own failure modes. This guide covers the 10 most frequent JWT issues developers hit in production, with exact error messages and copy-paste fixes.
Common errors covered
- 1 Token does not have three parts
- 2 Token has expired (exp claim in the past)
- 3 Algorithm mismatch between header and verifier
- 4 Base64 vs Base64url encoding confusion
- 5 Signature verification fails
- 6 Decoded payload is not valid JSON
- 7 Token not yet valid (nbf claim is in the future)
- 8 Audience (aud) claim rejected
- 9 Non-ASCII characters in claims cause encoding errors
- 10 Decoding without verification (security risk)
Token does not have three parts
JsonWebTokenError: jwt malformed
A valid JWT has exactly three dot-separated segments: header.payload.signature. Common causes: accidentally including the Bearer prefix, truncated tokens from copy-paste, or line breaks inserted by email clients.
Step-by-step fix
- 1 Paste the token into the JWT Decoder to see how many segments are detected.
- 2 Count the dots: a valid JWT has exactly two dots.
-
3
Remove any
Bearerprefix before decoding. - 4 Check for line breaks or spaces that split the token across lines.
Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Token has expired (exp claim in the past)
TokenExpiredError: jwt expired
The exp claim is a Unix timestamp. When the current server time exceeds this value, the token is rejected. Clock skew between servers and token issuers is a common hidden cause.
Step-by-step fix
-
1
Decode the token with the JWT Decoder and find the
expfield. - 2 Convert the timestamp using the Timestamp Converter to verify expiration time.
- 3 Compare with the current UTC time (not local time).
- 4 If clock skew is the issue, add a leeway of 30-60 seconds in your verification library.
// Token with exp in the past:
{"sub": "user_123", "exp": 1700000000}
// Current time: 1713600000 - token expired 157 days ago
// Verify with leeway for clock skew:
jwt.verify(token, secret, { clockTolerance: 30 }); // 30s leeway
// Or request a fresh token from the auth server
Algorithm mismatch between header and verifier
JsonWebTokenError: invalid algorithm
Error: Expected RS256, got HS256
The JWT header's alg field must match what your verification library expects. Attackers exploit this by changing RS256 to HS256 and signing with the public key as an HMAC secret.
Step-by-step fix
- 1 Decode the header (first segment) with the JWT Decoder.
-
2
Check the
algvalue - is it what your server expects? - 3 Always specify the expected algorithm explicitly in your verify call.
-
4
Never accept
"alg": "none"in production.
// INSECURE: accepts any algorithm jwt.verify(token, key); // attacker can forge with alg: 'none'
// SECURE: enforce specific algorithm
jwt.verify(token, key, { algorithms: ['RS256'] });
Base64 vs Base64url encoding confusion
Error: Invalid base64url string
SyntaxError: Unexpected token
JWT uses Base64url encoding (replacing + with -, / with _, and stripping = padding). Using standard Base64 decoders fails on JWT segments.
Step-by-step fix
- 1 Use the JWT Decoder which handles Base64url automatically.
-
2
If decoding manually, replace
-with+and_with/. -
3
Re-add padding: append
=until string length is a multiple of 4. - 4 Use the Base64 tool in URL-safe mode.
// Standard atob() fails on JWT segments:
atob('eyJhbGciOiJSUzI1NiJ9'); // may fail on - and _ chars
function decodeJwtSegment(seg) {
const base64 = seg.replace(/-/g, '+').replace(/_/g, '/');
const pad = base64 + '==='.slice((base64.length + 3) % 4);
return JSON.parse(atob(pad));
}
Signature verification fails
JsonWebTokenError: invalid signature
jwt.exceptions.InvalidSignatureError
The signature was computed with a different key than the one used for verification. Common causes: wrong secret/key, key rotation, or the token was tampered with.
Step-by-step fix
- 1 Decode the token header to confirm the algorithm.
- 2 Verify you are using the correct secret (HS256) or public key (RS256).
-
3
Check if key rotation happened - the
kid(key ID) header helps identify which key was used. - 4 For RS256, ensure you are using the public key for verification (not the private key).
// Using wrong key: jwt.verify(token, 'old-secret-key'); // key was rotated
// Use kid header to find correct key:
const header = jwt.decode(token, { complete: true }).header;
const key = keyStore.get(header.kid);
jwt.verify(token, key, { algorithms: ['RS256'] });
Decoded payload is not valid JSON
SyntaxError: Unexpected token in JSON at position 0
After Base64url-decoding the payload segment, the result must be valid JSON. Corruption during transmission, truncation, or manual editing can break the JSON structure.
Step-by-step fix
- 1 Decode just the middle segment with the Base64 tool (URL-safe mode).
- 2 Paste the result into the JSON Formatter to see the exact parse error.
- 3 Check for truncation - the original token may have been cut off.
-
4
Verify the token was not URL-encoded twice (look for
%2einstead of dots).
// Truncated payload produces invalid JSON: eyJzdWIiOiJ1c2VyXzEyMyIsIm // cut off mid-string
// Full payload decodes to valid JSON:
eyJzdWIiOiJ1c2VyXzEyMyIsImlhdCI6MTcxMzYwMDAwMH0
// -> {"sub": "user_123", "iat": 1713600000}
Token not yet valid (nbf claim is in the future)
NotBeforeError: jwt not active
jwt used before not-before claim
The nbf (not before) claim sets the earliest time the token can be used. If the client clock is behind the server, valid tokens appear premature.
Step-by-step fix
-
1
Decode the token and check the
nbffield. - 2 Convert the timestamp to verify it is in the future relative to your server.
- 3 Add clock tolerance (leeway) in your verification config.
- 4 Synchronize server clocks using NTP.
// Token issued for future use, client clock is behind:
{"sub": "user", "nbf": 1713700000, "iat": 1713600000}
// Client time: 1713650000 - before nbf
// Add leeway for clock differences:
jwt.verify(token, secret, {
clockTolerance: 60 // allow 60 seconds of clock skew
});
Audience (aud) claim rejected
JsonWebTokenError: jwt audience invalid
Expected: api.example.com, Got: web.example.com
The aud claim specifies which service the token is intended for. Using a token meant for one API on a different API triggers this error.
Step-by-step fix
-
1
Decode the payload and check the
audfield. - 2 Verify it matches the expected audience for your service.
- 3 If your service accepts multiple audiences, pass an array to the verifier.
- 4 Request a token with the correct audience from the auth server.
// Token for wrong service:
{"sub": "user", "aud": "web.example.com"}
// But API expects: "api.example.com"
// Accept multiple valid audiences:
jwt.verify(token, secret, {
audience: ['api.example.com', 'web.example.com']
});
Non-ASCII characters in claims cause encoding errors
UnicodeDecodeError: 'utf-8' codec can't decode byte
JWT claims containing non-ASCII characters (accented names, CJK text, emoji) must be properly UTF-8 encoded before Base64url encoding. Some libraries handle this incorrectly.
Step-by-step fix
- 1 Decode the payload and check for garbled characters.
- 2 Ensure the JWT library encodes claims as UTF-8 JSON before Base64url.
- 3 Test with the Base64 tool in UTF-8 mode to verify encoding.
- 4 Use ASCII-only values for critical claims; store display names separately.
# Latin-1 encoding of UTF-8 characters:
buffer = claims_json.encode('latin-1') # fails on non-ASCII
# Always use UTF-8:
import json
buffer = json.dumps(claims, ensure_ascii=False).encode('utf-8')
token_segment = base64url_encode(buffer)
Decoding without verification (security risk)
(No error - but application is vulnerable)
Decoding a JWT and trusting the claims without verifying the signature is a critical security vulnerability. Any user can craft a JWT with arbitrary claims.
Step-by-step fix
- 1 Use decode-only for debugging with the JWT Decoder tool.
-
2
In production code, always use
verify(), neverdecode(). -
3
Ensure your library validates the signature AND the
exp,iss,audclaims. - 4 Add tests that attempt to bypass authentication with forged tokens.
// INSECURE: decode without verification
const payload = JSON.parse(atob(token.split('.')[1]));
const userId = payload.sub; // attacker-controlled!
// SECURE: verify signature + claims
const payload = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
audience: 'api.myapp.com',
issuer: 'auth.myapp.com'
});
const userId = payload.sub; // trusted
Debugging Approach
- 1 Paste the full token into the JWT Decoder to see all three segments.
- 2 Check the header: Is the algorithm what you expect?
- 3 Check the payload: Are exp/nbf/iat timestamps valid? Is the audience correct?
- 4 Check the signature: Are you using the right key/secret?
- 5 Compare server time (UTC) with token timestamps to rule out clock skew.
Prevention Checklist
- Always specify the exact algorithm in your verify call - never rely on the token header.
- Set clock tolerance (30-60s leeway) to handle server clock skew.
-
Validate
exp,iss,aud, andnbfclaims server-side. -
Use
verify()in production,decode()only for debugging. -
Store key IDs (
kid) to support key rotation without downtime. -
Never accept
alg: nonetokens in production environments.
Frequently Asked Questions
Why does my JWT work locally but fail in production?
The most common causes are: (1) clock skew between your local machine and the production server, (2) different environment variables for the secret key, (3) the token issuer URL differs between environments. Decode the token and compare each claim against your production config.
Is it safe to paste JWT tokens into an online decoder?
The JWT Decoder on toolpilot.dev runs entirely in your browser - no data is sent to any server. For maximum safety, use test tokens for debugging and never paste production tokens containing sensitive PII.
How do I handle JWT key rotation without breaking existing tokens?
Use the kid (key ID) header claim. When rotating keys, keep the old key active for verification (but stop signing new tokens with it) until all old tokens expire. Your JWKS endpoint should serve both keys during the transition period.
Related Debug Guides
Related Tools
Still stuck? Try our free tools
All tools run in your browser, no signup required.