DEV Community

zhihu wu
zhihu wu

Posted on

Your JWT Works Locally but 401s in Production - Check These 5 Things First

You paste the token your API just issued into a decoder. The header parses, the payload parses, everything looks right - and the request still comes back 401 Unauthorized.

Signature bugs get all the attention, but in practice most "valid-looking token rejected" incidents are one of five boring mismatches. Decode the token first, then walk this list in order.

1. Clock skew between two servers

A token is minted with iat and validated against nbf and exp. If the machine that issued it is a few seconds behind the machine validating it, a token created "just now" can look like it is not valid yet. Most libraries default to 30-60 seconds of leeway - if someone tightened that, put it back. The tell: the token decodes, exp is clearly in the future, and the failure is intermittent.

2. The audience or issuer doesn't match

aud and iss are the two claims people skip. Your auth service may issue tokens for api.example.com while the service you are calling is configured to accept *.internal. Paste the token, read aud and iss, and compare them character by character with the validator's config. It is almost always one environment variable.

3. Key rotation and the kid header

When a provider rotates signing keys, the token's header carries a kid (key id) saying which key signed it. If your validator only loads the old key, every freshly issued token fails while older ones still work - or the exact reverse. Compare the kid in the header with the keys your verifier actually has loaded.

4. The algorithm is mis-declared

A token signed with RS256 but handed to a service pinned to HS256 fails even though the payload is perfect. Worse: if the service reads alg from the token itself, an "alg": "none" token lets an attacker strip the signature entirely. Pin the accepted algorithm server-side and never trust the header's alg field.

5. You are testing the wrong copy of the token

Embarrassingly common. The token in your clipboard came from a truncated log line, an earlier request, or a different user's session. Decode the exact string you are sending, not the one you think you are sending.

The point is that decoding is free and instant, and it removes guesswork. I use CodeToolbox's JWT Decoder - it runs entirely in the browser with atob and JSON.parse, so the token never leaves your machine, which matters because a JWT is a bearer credential. It renders exp/iat/nbf as readable dates and flags unsecured alg: none tokens.

Decode anywhere; verify the signature server-side with a real library.

Top comments (0)