Why Your JWT Isn't Working: A Debugging Checklist
A JWT-based login stops working, or a request that should be authenticated comes back 401 Unauthorized, and the error message is unhelpfully generic. This is one of the more frustrating categories of bug precisely because the token itself often looks fine when you glance at it. Here's a systematic way to actually find the cause, instead of guessing.
Step 1: decode the token and actually look at it
Before anything else, decode the token and read what's actually inside it. A JWT Decoder splits the token into its header and payload and shows you the real, decoded JSON — not what you assume is in there. This step alone catches a surprising number of bugs: a token from the wrong environment, a payload missing a claim your code expects, or a token that's technically well-formed but simply isn't the one you thought you were testing with.
Step 2: check the expiration claim
The exp claim is a Unix timestamp marking when the token expires. This is the single most common reason a previously-working token suddenly stops authenticating — it simply timed out. A few things to check specifically:
- Is
expactually in the future, relative to the current time, in seconds (not milliseconds)? A surprisingly common bug is a token generator that setsexpin milliseconds instead of seconds, which either creates a token that expires almost instantly or one that appears to expire thousands of years in the future — both wrong, for different reasons. - Is the server's clock correct? JWT expiration checks compare against server time. A server with a wrong system clock will reject valid, non-expired tokens or accept ones that should have expired. This is rare but real, especially on containers or VMs with clock drift.
- Is there a
nbf(not before) claim that hasn't been reached yet? Less common thanexp, but a token with a futurenbfwill be rejected as "not yet valid," which produces a similar-looking failure to expiration.
Step 3: verify the algorithm matches on both ends
The JWT header specifies which algorithm was used to sign the token (commonly HS256 or RS256). If the issuing service signs with one algorithm and the verifying service is configured to expect a different one, verification fails — and the error is often a generic "invalid signature" that doesn't hint at the actual mismatch. This is especially easy to get wrong when switching a system from symmetric signing (HS256, one shared secret) to asymmetric signing (RS256, a public/private key pair) and not updating every service that verifies tokens.
Step 4: confirm you're checking the right secret or key
For HS256 tokens, both the signing side and verifying side need the exact same secret string. A trailing space, a secret loaded from the wrong environment variable, or a secret that got rotated on one service but not the other will all produce a token that decodes fine (remember: decoding doesn't require the secret) but fails signature verification.
For RS256 tokens, confirm the verifying service actually has the current public key — this bites teams especially after a key rotation, when old services keep the previous public key cached and reject tokens signed with the new private key.
Step 5: check for a stripped or mangled "Bearer " prefix
Tokens are typically sent in an Authorization header formatted as Bearer <token>. If your code passes the entire header value (including "Bearer ") into a JWT verification function that expects just the token, verification fails — not because the token is invalid, but because the "Bearer " prefix and space are still stuck to the front of the string. This is a common, easy-to-miss bug, especially after refactoring how a header gets parsed.
Step 6: rule out a decoded-but-unverified token being trusted incorrectly
This one is a security bug, not just a functional one, and it's worth explicitly checking your code for: decoding a JWT is not the same as verifying it. Reading the payload out of a token (to display a username, for example) doesn't confirm the token's signature is valid. If any code path decodes a token and trusts its contents without going through a proper signature verification step first, that's a real vulnerability, not just a debugging inconvenience — an attacker could hand-craft a token with any payload they want, since the payload segment is only Base64URL-encoded, not encrypted or otherwise protected on its own.
A fast checklist for next time
When a JWT issue comes up again, work through this in order — it resolves the large majority of real cases:
- Decode it and confirm the payload actually contains what you expect
- Check
expagainst the current time, in the right unit (seconds) - Confirm the signing and verifying algorithms match
- Confirm the secret or public key is current on the verifying side
- Confirm the raw token (not the full
Authorizationheader value) is what's being passed to verification - Confirm nothing trusts a decoded-but-unverified payload as authenticated
Frequently asked questions
Why does my JWT say it's expired when I just generated it?
The most common cause is a units mismatch — the exp claim should be a Unix timestamp in seconds, but some code accidentally sets it using a milliseconds-based timestamp. This produces a token that either expires almost instantly or appears to expire far in the future, depending on which direction the mismatch goes.
Can I fix a JWT signature error by re-encoding the token?
No — a signature error means the token's signature doesn't match what the verifying service computes for the given header and payload using its configured secret or key, which points to a mismatched secret, mismatched algorithm, or an outdated key after rotation. Re-encoding the same payload doesn't fix a fundamental mismatch in what secret or key is being used.
Is it safe to decode a JWT on the frontend to read the user's info?
Reading non-sensitive claims (like a display name) from a decoded token on the frontend is common and generally fine, since the frontend already received the token from a trusted source. The critical rule is that verification must happen server-side before the token's claims are trusted for any authorization decision — never treat a client-side decode as proof the token is valid.
What's the difference between decoding and verifying a JWT?
Decoding just reads the Base64URL-encoded header and payload back into readable JSON — anyone can do this without any secret, since it's just an encoding, not encryption. Verifying checks the token's signature against the expected secret or public key, confirming the token was actually issued by a trusted source and hasn't been tampered with. Only a verified token should be trusted for authentication.