Authentication 2026-04-20

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.

how to fix JWT JWT decoding error jwt malformed jwt expired JWT common mistakes debug JWT token

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. 1 Token does not have three parts
  2. 2 Token has expired (exp claim in the past)
  3. 3 Algorithm mismatch between header and verifier
  4. 4 Base64 vs Base64url encoding confusion
  5. 5 Signature verification fails
  6. 6 Decoded payload is not valid JSON
  7. 7 Token not yet valid (nbf claim is in the future)
  8. 8 Audience (aud) claim rejected
  9. 9 Non-ASCII characters in claims cause encoding errors
  10. 10 Decoding without verification (security risk)
1

Token does not have three parts

Error message
JsonWebTokenError: jwt malformed
Root cause

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. 1 Paste the token into the JWT Decoder to see how many segments are detected.
  2. 2 Count the dots: a valid JWT has exactly two dots.
  3. 3 Remove any Bearer prefix before decoding.
  4. 4 Check for line breaks or spaces that split the token across lines.
Wrong
Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0
Correct
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

2

Token has expired (exp claim in the past)

Error message
TokenExpiredError: jwt expired
Root cause

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. 1 Decode the token with the JWT Decoder and find the exp field.
  2. 2 Convert the timestamp using the Timestamp Converter to verify expiration time.
  3. 3 Compare with the current UTC time (not local time).
  4. 4 If clock skew is the issue, add a leeway of 30-60 seconds in your verification library.
Wrong
// Token with exp in the past:
{"sub": "user_123", "exp": 1700000000}
// Current time: 1713600000 - token expired 157 days ago
Correct
// Verify with leeway for clock skew:
jwt.verify(token, secret, { clockTolerance: 30 }); // 30s leeway
// Or request a fresh token from the auth server

3

Algorithm mismatch between header and verifier

Error message
JsonWebTokenError: invalid algorithm Error: Expected RS256, got HS256
Root cause

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. 1 Decode the header (first segment) with the JWT Decoder.
  2. 2 Check the alg value - is it what your server expects?
  3. 3 Always specify the expected algorithm explicitly in your verify call.
  4. 4 Never accept "alg": "none" in production.
Wrong
// INSECURE: accepts any algorithm
jwt.verify(token, key); // attacker can forge with alg: 'none'
Correct
// SECURE: enforce specific algorithm
jwt.verify(token, key, { algorithms: ['RS256'] });

4

Base64 vs Base64url encoding confusion

Error message
Error: Invalid base64url string SyntaxError: Unexpected token
Root cause

JWT uses Base64url encoding (replacing + with -, / with _, and stripping = padding). Using standard Base64 decoders fails on JWT segments.

Step-by-step fix

  1. 1 Use the JWT Decoder which handles Base64url automatically.
  2. 2 If decoding manually, replace - with + and _ with /.
  3. 3 Re-add padding: append = until string length is a multiple of 4.
  4. 4 Use the Base64 tool in URL-safe mode.
Wrong
// Standard atob() fails on JWT segments:
atob('eyJhbGciOiJSUzI1NiJ9');  // may fail on - and _ chars
Correct
function decodeJwtSegment(seg) {
  const base64 = seg.replace(/-/g, '+').replace(/_/g, '/');
  const pad = base64 + '==='.slice((base64.length + 3) % 4);
  return JSON.parse(atob(pad));
}

5

Signature verification fails

Error message
JsonWebTokenError: invalid signature jwt.exceptions.InvalidSignatureError
Root cause

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. 1 Decode the token header to confirm the algorithm.
  2. 2 Verify you are using the correct secret (HS256) or public key (RS256).
  3. 3 Check if key rotation happened - the kid (key ID) header helps identify which key was used.
  4. 4 For RS256, ensure you are using the public key for verification (not the private key).
Wrong
// Using wrong key:
jwt.verify(token, 'old-secret-key'); // key was rotated
Correct
// 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'] });

6

Decoded payload is not valid JSON

Error message
SyntaxError: Unexpected token in JSON at position 0
Root cause

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. 1 Decode just the middle segment with the Base64 tool (URL-safe mode).
  2. 2 Paste the result into the JSON Formatter to see the exact parse error.
  3. 3 Check for truncation - the original token may have been cut off.
  4. 4 Verify the token was not URL-encoded twice (look for %2e instead of dots).
Wrong
// Truncated payload produces invalid JSON:
eyJzdWIiOiJ1c2VyXzEyMyIsIm  // cut off mid-string
Correct
// Full payload decodes to valid JSON:
eyJzdWIiOiJ1c2VyXzEyMyIsImlhdCI6MTcxMzYwMDAwMH0
// -> {"sub": "user_123", "iat": 1713600000}

7

Token not yet valid (nbf claim is in the future)

Error message
NotBeforeError: jwt not active jwt used before not-before claim
Root cause

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. 1 Decode the token and check the nbf field.
  2. 2 Convert the timestamp to verify it is in the future relative to your server.
  3. 3 Add clock tolerance (leeway) in your verification config.
  4. 4 Synchronize server clocks using NTP.
Wrong
// Token issued for future use, client clock is behind:
{"sub": "user", "nbf": 1713700000, "iat": 1713600000}
// Client time: 1713650000 - before nbf
Correct
// Add leeway for clock differences:
jwt.verify(token, secret, {
  clockTolerance: 60  // allow 60 seconds of clock skew
});

8

Audience (aud) claim rejected

Error message
JsonWebTokenError: jwt audience invalid Expected: api.example.com, Got: web.example.com
Root cause

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. 1 Decode the payload and check the aud field.
  2. 2 Verify it matches the expected audience for your service.
  3. 3 If your service accepts multiple audiences, pass an array to the verifier.
  4. 4 Request a token with the correct audience from the auth server.
Wrong
// Token for wrong service:
{"sub": "user", "aud": "web.example.com"}
// But API expects: "api.example.com"
Correct
// Accept multiple valid audiences:
jwt.verify(token, secret, {
  audience: ['api.example.com', 'web.example.com']
});

9

Non-ASCII characters in claims cause encoding errors

Error message
UnicodeDecodeError: 'utf-8' codec can't decode byte
Root cause

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. 1 Decode the payload and check for garbled characters.
  2. 2 Ensure the JWT library encodes claims as UTF-8 JSON before Base64url.
  3. 3 Test with the Base64 tool in UTF-8 mode to verify encoding.
  4. 4 Use ASCII-only values for critical claims; store display names separately.
Wrong
# Latin-1 encoding of UTF-8 characters:
buffer = claims_json.encode('latin-1')  # fails on non-ASCII
Correct
# Always use UTF-8:
import json
buffer = json.dumps(claims, ensure_ascii=False).encode('utf-8')
token_segment = base64url_encode(buffer)

10

Decoding without verification (security risk)

Error message
(No error - but application is vulnerable)
Root cause

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. 1 Use decode-only for debugging with the JWT Decoder tool.
  2. 2 In production code, always use verify(), never decode().
  3. 3 Ensure your library validates the signature AND the exp, iss, aud claims.
  4. 4 Add tests that attempt to bypass authentication with forged tokens.
Wrong
// INSECURE: decode without verification
const payload = JSON.parse(atob(token.split('.')[1]));
const userId = payload.sub;  // attacker-controlled!
Correct
// 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. 1 Paste the full token into the JWT Decoder to see all three segments.
  2. 2 Check the header: Is the algorithm what you expect?
  3. 3 Check the payload: Are exp/nbf/iat timestamps valid? Is the audience correct?
  4. 4 Check the signature: Are you using the right key/secret?
  5. 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, and nbf claims server-side.
  • Use verify() in production, decode() only for debugging.
  • Store key IDs (kid) to support key rotation without downtime.
  • Never accept alg: none tokens 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.