JWT Debugging Guide: Common Issues and Fixes from Production
Troubleshoot JWT problems: expired tokens, invalid signatures, missing claims, and more with battle-tested debugging strategies.
How to approach JWT debugging systematically
JWT issues can be frustrating because the error messages are often vague. I've spent hours debugging JWT problems that ended up being simple fixes once I knew where to look. Let me save you that pain.
The key to effective JWT debugging is to be systematic. Don't just guess — check one thing at a time, in order. Here's the approach I use every time:
1. **Can you decode the token?** Start with the basics — is it even a valid JWT?
2. **Are the timestamps valid?** Check `exp` and `nbf` first — this is the most common issue.
3. **Does the signature verify?** This is the second most common issue.
4. **Are the claims correct?** Check `iss`, `aud`, and any custom claims.
5. **Is the token being sent correctly?** Check headers, formatting, etc.
Let's walk through each of these in detail, with the common problems I've seen in production and how to fix them.
JWT problem #1: "Token expired" (and how to fix it)
This is the single most common JWT issue I encounter. The error message might say "Token expired", "JWT expired", "Expired token", or something similar.
**How to diagnose**: First, decode the token and look at the `exp` claim. Remember, it's a Unix timestamp in seconds (not milliseconds!) since January 1, 1970 UTC. Convert it to a readable date:
const exp = 1718894400; // Example timestamp
const expirationDate = new Date(exp * 1000); // Multiply by 1000 for JavaScript
console.log(expirationDate.toString()); **Common causes and fixes**:
- **The token is actually expired** — This is the most common case. The user needs to get a new token using a refresh token or by re-authenticating.
- **Server clock is wrong** — I've seen this happen when server clocks drift. Always use NTP to keep server clocks synchronized. Remember, timestamps should always be UTC, not local time.
- **Client and server time mismatch** — If the client's clock is significantly wrong, tokens might seem expired before they actually are. This is rare but can happen.
- **You forgot to multiply by 1000** — If you're working in JavaScript, Python uses seconds, JavaScript uses milliseconds. This is a common gotcha.
- **`exp` is in milliseconds instead of seconds** — Some systems accidentally generate timestamps in milliseconds. Double-check the source.
**Pro tip**: When building your own JWT system, include a clock skew tolerance (usually 1-5 minutes) to handle minor clock differences between systems.
JWT problem #2: "Invalid signature" (the tricky one)
Invalid signature errors are the second most common issue, and they can be trickier to debug because there are so many potential causes.
**This usually means one of these things**:
- **The secret key is wrong** — The most common cause. Double-check that you're using the exact same secret or public key that was used to sign the token. Even a single character difference will break verification.
- **Algorithm mismatch** — HS256 vs RS256 is a common mix-up. The algorithm in the header must match what the verifier expects.
- **The token was modified** — Even a single character change in the header or payload will invalidate the signature. This is exactly what signatures are for!
- **Wrong public/private key pair** — With asymmetric algorithms (RS256, ES256), you must use the private key to sign and the corresponding public key to verify.
- **Key ID (`kid`) mismatch** — If you're using key rotation with `kid`, make sure you're using the right key.
- **The token includes extra characters** — Sometimes when copying tokens from logs or chat, you get extra spaces or newlines. Make sure you're verifying the exact token string.
**How to debug**: Start with the simplest possibility first — is the secret/key correct? Then check the algorithm. Then verify that the token hasn't been modified or truncated.
**Pro tip**: If you're using HS256, you can use our HMAC generator tool to compute the expected signature and compare it to what's in the token.
JWT problem #3: "Invalid audience" or "Invalid issuer"
These errors mean the `aud` (audience) or `iss` (issuer) claim doesn't match what the server expects. These are important for security — they ensure the token was meant for your application.
**Common causes and fixes**:
- **Trailing slash mismatch** — `"https://your-app.com"` vs `"https://your-app.com/"`. These are different strings! Be consistent.
- **Environment mismatch** — Using staging issuer in production or vice versa. `"https://staging-auth.your-app.com"` vs `"https://auth.your-app.com"`.
- **Typo in the URL** — It happens! Check the exact string.
- **Forgot to configure the values** — Many JWT libraries require you to explicitly set the expected issuer and audience. Make sure these are configured.
**How to diagnose**: Decode the token and look at the `iss` and `aud` claims. Compare them exactly (case-sensitive, including slashes) to what your server expects. Even a single character difference will cause verification to fail.
**Pro tip**: Document the expected issuer and audience values in your development docs so everyone on the team knows what they should be.
JWT problem #4: Missing expected claims
Sometimes the token verifies correctly, but it doesn't have the claims your application expects — like `email`, `role`, or `permissions`.
**Common causes and fixes**:
- **Auth provider changed their format** — This happens more often than you think. Auth providers sometimes change which claims are included by default.
- **You forgot to request the right scopes** — In OAuth2 and OpenID Connect, you need to request specific scopes to get certain claims. For example, you might need the `profile` scope to get the user's name, or the `email` scope to get their email.
- **Custom claims are in a different place** — Sometimes custom claims are nested under a `namespace` or another property. Check the full payload.
- **The token is from the wrong flow** — Different OAuth flows return different tokens with different claims. Client credentials flow tokens might not have the same claims as authorization code flow tokens.
**How to diagnose**: Decode the token and look at what claims are actually present. Don't just assume claims exist — check! Then go back to your auth provider's documentation to see if you need to configure something differently.
**Pro tip**: When integrating a new auth provider, print or log the full decoded payload early on so you can see exactly what claims you're getting.
JWT problem #5: The token won't decode at all
If you can't even decode the token, start with the basics.
**Common causes and fixes**:
- **The token is truncated** — JWTs can be long. Make sure you have the complete string with all three parts.
- **Extra whitespace or characters** — When copying from logs, chat, or documents, you might get extra spaces, newlines, or quotes. Make sure you have just the token string.
- **It's not a JWT at all** — If it has five parts instead of three, it might be a JWE (encrypted JWT). If it's XML, it might be a SAML token.
- **Base64URL vs regular Base64** — Make sure you're using Base64URL decoding, not regular Base64. JWT uses URL-safe characters (- instead of +, _ instead of /).
- **Corrupted token** — Sometimes tokens get corrupted in transit. Ask the user to get a new token.
**How to diagnose**: First, check that the token has three parts separated by dots. If it does, make sure you're using Base64URL decoding. If it still doesn't decode, the token might be corrupted.
JWT problem #6: "Not before" (nbf) issues
The `nbf` (not before) claim is optional, but when present, it specifies the time before which the token must not be accepted for processing.
**Common causes and fixes**:
- **Clock skew** — The token says it's not valid yet, but according to your server's clock, it should be. Again, use NTP to keep clocks synchronized.
- **The `nbf` value is wrong** — Maybe the token was generated with an incorrect `nbf` timestamp.
- **You're checking it too soon** — Sometimes tokens are generated with an `nbf` a few seconds in the future to account for clock drift. Wait a few seconds and try again.
As with `exp`, include a small clock skew tolerance when checking `nbf`.
My go-to JWT debugging checklist
This is the exact checklist I use every time I debug a JWT issue. It's saved me countless hours:
**1. Can you decode the token at all?** If not, check for truncation or extra characters. Make sure it has three dot-separated parts.
**2. Is the header valid JSON?** Does it have an `alg` claim? Is the algorithm what you expect?
**3. Is the payload valid JSON?** Are there any syntax errors?
**4. Is the token expired?** Check `exp` against current time. Remember: `exp` is in seconds, multiply by 1000 for JavaScript dates.
**5. Is the token not yet valid?** Check `nbf` if present.
**6. Are `iss` and `aud` exactly what the server expects?** Check for trailing slashes, environment mismatches, and typos.
**7. Can you verify the signature with the correct key?** Double-check the secret or public key. Make sure the algorithm matches.
**8. Are you sending the token correctly?** Is it in the `Authorization` header with the `Bearer ` prefix (note the space after Bearer)? Is it missing, or is there a typo in "Bearer"?
**9. Are the custom claims you expect present?** Don't assume — check the actual payload.
**10. Are you using a reputable JWT library?** Don't implement JWT verification yourself. Use a well-audited library.
Go through this checklist step by step, and you'll find the issue 99% of the time.
Advanced JWT debugging tips
Here are some pro tips I've learned over the years:
**Log what you're verifying**: When debugging, log the expected issuer, audience, and the claims you're getting. Sometimes the difference is subtle.
**Use the same library for signing and verifying**: If possible, use the same JWT library for both signing and verifying. Different libraries can have slightly different defaults.
**Try a known-good token**: If you have a token that you know works, compare it side by side with the problematic token.
**Check for signature algorithm confusion**: Make sure your library isn't accepting the `none` algorithm, and that it's not confusing HS256 with RS256.
**Use `kid` for key rotation**: If you're rotating keys, include the `kid` header so you know which key to use for verification.
**Test with JWT libraries in a REPL**: When debugging, you can use a Node.js REPL or a browser console to test verification interactively. This is much faster than changing code and reloading.
**Remember: decoding ≠ verification**: Just because you can read the claims doesn't mean you should trust them. Always verify the signature and the claims.
Try These Tools
Open the tools that go with this tutorial and practice what you learned.
JWT Decoder
Decode JWT headers and payloads locally so you can inspect claims and timestamps.
Base64URL Encode / Decode
Encode text as Base64URL or decode URL-safe Base64 back to UTF-8 text.
JSON Formatter
Format JSON with clean indentation so logs, payloads, and fixtures are easier to read.
More Tutorials in This Category
Keep Learning
How to Decode a JWT: A Comprehensive Step-by-Step Tutorial
Learn what a JWT is, how to decode one, what each part means, and real-world debugging tips from production experience.
Base64 Encoding: A Comprehensive Developer Guide with Examples
Learn how Base64 works, when to use it, common pitfalls, and the crucial difference between Base64 and Base64URL with practical examples.