Developer

JWTs: what is inside one, and why decoding is not verifying

Anyone holding a JWT can read every claim in it. That is by design, and it is the single most misunderstood thing about the format.

3 minute read

A JSON Web Token is three Base64url-encoded segments separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3MzAwMDAwMDB9.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
   header                                payload                                  signature

The three parts

Header, a small JSON object naming the signing algorithm, e.g. {"alg":"HS256","typ":"JWT"}.

Payload, the claims. Registered claim names are short by convention: sub (subject), iss (issuer), aud (audience), exp (expiry, as a Unix timestamp), iat (issued at), nbf (not before). You can add your own.

Signature, computed over base64url(header) + "." + base64url(payload) using the algorithm from the header and a key.

The part people get wrong

Base64url is encoding, not encryption. The payload is readable by anyone who has the token, paste it into any decoder, or run it through atob in a browser console.

So:

  • Never put anything confidential in a JWT payload. No passwords, no personal data you would not put in a URL, no internal identifiers you would rather not leak.
  • The signature guarantees the token has not been modified. It does nothing to stop it being read.

If you need an encrypted token, that is JWE, a different specification. JWT alone is signed, not encrypted.

Decoding versus verifying

Decoding splits on dots and Base64url-decodes. It requires no key and proves nothing.

Verifying recomputes the signature using the key and compares. Only that step establishes the token is authentic.

A decoder, including ours, shows you what is inside a token. It cannot tell you the token is valid, because it does not have your signing key. That is expected behaviour, not a limitation to work around.

The alg: none problem

Early JWT libraries trusted the alg field in the header. An attacker could set "alg":"none", strip the signature, and some libraries would accept it as valid.

A related attack: a server expecting RS256 (asymmetric) receives a token claiming HS256 (symmetric), and a naive library verifies the HMAC using the public key as the shared secret, a key the attacker also has.

Both are fixed in maintained libraries, and both are still worth knowing, because the underlying lesson generalises: the server must decide which algorithm it accepts. Never take that instruction from the token itself.

Checking a token properly

A verifier should confirm all of the following:

  1. The signature is valid, using an algorithm the server chose in advance.
  2. exp is in the future.
  3. nbf, if present, is in the past.
  4. iss matches the expected issuer.
  5. aud includes this service.

Skipping the expiry check is depressingly common and turns a short-lived token into a permanent credential.

The revocation problem

JWTs are stateless, that is their appeal. The server validates the signature without a database lookup, which scales beautifully.

It also means you cannot revoke one. A stolen token is valid until it expires, full stop. Logging out clears it from the client, and does nothing about a copy an attacker took.

Practical mitigations: keep access tokens short-lived (minutes, not days), use refresh tokens that are stored server-side and can be revoked, and maintain a deny-list for emergencies, accepting that you have reintroduced the state you were trying to avoid.

Where to store one

  • localStorage: readable by any JavaScript on the page. One XSS vulnerability and the token is exfiltrated.
  • httpOnly cookie: unreadable by JavaScript, so XSS cannot steal it directly. Requires CSRF protection, and SameSite handles most of that.

The general recommendation is an httpOnly, Secure, SameSite cookie.

Inspecting tokens safely

Session tokens end up pasted into online decoders during debugging all the time. A live token is a live credential, if the decoder's server logs it, that log is now a set of working keys to someone's account.

Decoding is pure string manipulation, so there is no reason for it to happen anywhere but your own browser. That is how the JWT decoder here works. Whatever tool you use, check that before pasting a production token into it.

Further reading

Do it hereJWT Token InspectorDecode JWT header, payload, verify expiry and inspect claimsOpen

Written for NextTool. Every statement about a tool is checked against that tool's code, and corrections are dated above. How guides are written

jwtauthsecurity