Why Decoding a JWT Is Not the Same as Verifying It
JSON Web Tokens are everywhere in modern authentication โ API sessions, single sign-on, mobile app auth. A JWT looks cryptographically sealed because it's a long string of seemingly random characters. But here's the part that trips up even experienced developers: decoding a JWT reveals its contents to anyone, because the header and payload are only base64url-encoded, not encrypted. Encoding is not encryption, and it is definitely not a signature check.
Anyone can paste a JWT into a decoder and read the claims inside. The signature is the only part that actually proves the token wasn't tampered with โ and checking that signature requires the issuer's secret key (for HS256) or public key (for RS256/ES256), which never travels inside the token itself. If your application trusts a JWT's claims without verifying the signature server-side, you have no real security guarantee at all.
The alg:none Vulnerability You Should Know About
One of the most infamous JWT bugs in history involves the alg claim in the header. Some early JWT libraries accepted a header declaring "alg":"none" as valid, meaning the token claimed to have no signature whatsoever โ and the library trusted it anyway. An attacker could take any legitimate JWT, strip the signature, set alg to none, and forge arbitrary claims like {"role":"admin"} with zero cryptographic material required.
Most modern libraries reject alg:none by default now, but misconfigurations still happen โ especially in custom auth middleware or older dependencies that haven't been patched. Whenever you inspect a JWT, checking the alg claim should be a reflex. If you ever see none in production, treat it as a critical finding.
Reading Time Claims Correctly
JWTs commonly carry three time-related claims, all expressed as Unix timestamps in seconds:
- iat (issued at) โ when the token was created
- nbf (not before) โ the token is invalid before this time
- exp (expiration) โ the token is invalid after this time
A token that has already expired should never be accepted, no matter how valid its signature is. Similarly, a token with an nbf claim in the future is intentionally not yet active โ some systems issue tokens ahead of time for scheduled access windows. Misreading these fields, or ignoring them entirely, is a surprisingly common source of authentication bugs.
A Practical Checklist Before You Trust Any JWT
- Check the structure. A valid JWT has exactly three dot-separated segments: header, payload, and signature. Anything else is malformed.
- Decode the header first. Confirm the
algis one your backend expects (e.g. RS256), and immediately rejectnone. - Inspect the time claims. Convert
exp,iat, andnbfto your local time and confirm the token is currently valid. - Never trust claims without server-side signature verification. Decoding for debugging is fine; decoding as a substitute for verification is not.
- Rotate signing keys periodically and make sure your verification library is current โ old JWT libraries are where most of these bugs live.
A decoder tool is invaluable for debugging โ inspecting what's actually inside a token during development, checking why an integration is failing, or auditing a third-party API's tokens. Just remember that decoding is a diagnostic step, not a security boundary.
Debugging With a JWT Decoder in Practice
In day-to-day development, a decoder earns its keep in a handful of recurring situations. When an API integration suddenly starts rejecting requests with a 401, decoding the token you're sending is often the fastest way to confirm whether the problem is an expired token, a missing claim your backend expects, or a token that was never actually attached to the request in the first place. When a mobile app reports "random" logout behavior, checking the exp claim against your token refresh interval frequently reveals a race condition โ the refresh logic firing a few seconds too late relative to when the old token actually expires.
It's also useful during code review. If a teammate's pull request changes how tokens are issued, decoding a sample token from their branch and comparing the claim set against the previous implementation surfaces accidental regressions immediately โ a dropped scope claim, a shortened expiry window, or a stale user ID format left over from a migration.
One habit worth building: whenever you inspect a production token for debugging, immediately note its expiry and treat the token as disposable. Decoded claims should never be copy-pasted into scripts or shared in chat as if they were safe secrets โ while the payload isn't encrypted, an active token is still a live credential until it expires, and it deserves the same handling caution as a password.