DEV Community

康栋普
康栋普

Posted on Originally published at nulltools.dev

How JWTs Work, and Why Decoding Is Not Verification

JSON Web Tokens are everywhere: authentication, single sign-on, API authorization. They are also one of the most misunderstood pieces of the modern web — mostly because people confuse decoding a token with verifying it.

Here is what is actually inside a JWT, and the one mistake that quietly turns a "secure" auth system into an open door.

The three parts

A JWT is just a string with three dot-separated segments:

header.payload.signature
Enter fullscreen mode Exit fullscreen mode
  • Header — the token type and the signing algorithm (e.g. HS256, RS256).
  • Payload — the claims: user ID, roles, expiry (exp), issuer (iss), audience (aud).
  • Signature — what lets a trusted party confirm the token was not tampered with.

The first two segments are Base64URL-encoded JSON. That is all. They are not encrypted, and anyone holding the token can read them.

Decoding is not verification

This is the part that bites people.

Decoding a JWT reveals its contents — nothing more. It does not prove the token was issued by your server, or that it has not been modified.

To actually trust a token you must:

  1. Verify the signature with the correct key or secret.
  2. Check exp — the token has not expired.
  3. Check iss and aud match what you expect.

Skip any of those and you have effectively built an API where anyone can mint themselves an admin token.

Decoding a payload by hand

Sometimes you just want to see what is inside. The payload is Base64URL, so:

JavaScript

const [, payload] = token.split('.')
const json = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')))
console.log(json)
Enter fullscreen mode Exit fullscreen mode

Python

import base64, json

payload = token.split('.')[1]
padding = '=' * (-len(payload) % 4)
print(json.loads(base64.urlsafe_b64decode(payload + padding)))
Enter fullscreen mode Exit fullscreen mode

PHP

[$header, $payload, $sig] = explode('.', $token);
echo base64_decode(strtr($payload, '-_', '+/'));
Enter fullscreen mode Exit fullscreen mode

Note that none of these snippets verify anything — they only decode.

Common pitfalls

  • Putting secrets in the payload. Anyone can read it.
  • Trusting claims without verifying the signature.
  • Ignoring exp and aud. An expired token that never expires is a gift to an attacker.
  • Accepting the algorithm from the token itself. Always pin the expected alg on the server side.

FAQ

Can anyone read a JWT? Yes. The header and payload are encoded, not encrypted.

Does decoding prove the token is valid? No. Validity comes from signature and claim verification.

Is my token uploaded when I inspect it? It should not be. Some online decoders send the token to a server, which is a bad idea for a credential — decoding can happen entirely in the browser.


If you need to eyeball a token quickly, I built a free JWT Decoder that runs entirely client-side, so nothing leaves your browser. (It is my own project — the explanation above stands on its own.)

Top comments (0)