Reading a JWT Without Trusting It: Claims, Expiry, and the Signature Trap
A JWT looks like three chunks of gibberish separated by dots. Two of them are not gibberish at all — they are Base64url-encoded JSON, readable by anyone who bothers to decode them. That single fact explains most of the mistakes people make with tokens.
The three parts
- Header — the signing algorithm and, often, a key id. JSON, encoded, not encrypted.
- Payload — the claims. Also JSON, also encoded, also not encrypted.
- Signature — the only part that provides any security, computed over the first two.
Read that middle point again: anyone holding the token can read its contents. Do not put anything in a JWT that you would not put in a URL — no internal ids you consider secret, no PII you would not want logged, certainly no passwords.
Decoding is not verifying
Decoding a token means Base64-decoding two strings. It tells you what the token claims. It tells you nothing about whether those claims are true, because you have not checked the signature.
The historic version of this bug: an application decodes the payload, trusts role: "admin", and never verifies. An attacker edits the payload, re-encodes it, and is now an admin. A related variant is the alg: "none" attack, where a library is talked into accepting an unsigned token as valid. Any decent library refuses this now, but only if you are actually calling the verify path rather than the decode path.
The claims worth checking every time
exp— expiry. Verify it, and remember it is in seconds since the epoch, not milliseconds. Mixing the two produces tokens that expire in 1970 or in the year 56000.nbf— not before. A token from a clock-skewed issuer can be legitimately rejected; allow a small tolerance, on the order of a minute.issandaud— issuer and audience. A valid token issued for a different service is still a valid token. Check that it was meant for you.iat— issued at. Useful for deciding a token is too old to honour even if it has not formally expired.
Debugging a token safely
Pasting a production token into a random site hands a live credential to a stranger — it is bearer authentication, so possession is enough. If you must inspect one:
- Use a tool that decodes locally in your browser and does not transmit the token anywhere.
- Prefer a token from a test environment, or one belonging to a throwaway account.
- If you do paste a live token somewhere questionable, treat it as leaked and rotate the signing key — not just that session.
- Never paste a signing secret anywhere to "check the signature". That is the one value that actually matters.
When a JWT is the wrong tool
JWTs are hard to revoke. That is the whole trade — statelessness buys you scale and costs you the ability to instantly kill a session. If you need immediate logout, forced sign-out on password change, or a per-session kill switch, you need server-side session state, a short-lived access token paired with a revocable refresh token, or a deny list. Long-lived JWTs with no revocation path are a decision, and usually a quiet one nobody remembers making.