What's Inside a JWT
By ToolzHive · · 4 min read
A JWT (JSON Web Token) is the long string of gibberish your app stores after you log in. It is not encrypted and not magic — it is three Base64 chunks with a signature. Knowing what is inside helps you use them correctly and avoid the classic security mistakes.
Three parts, separated by dots
Every JWT has the same shape: header.payload.signature. The two dots split it into three Base64URL-encoded sections.
- Header — a tiny JSON object naming the signing algorithm (for example
HS256orRS256) and the token type. - Payload — the claims: the actual data, such as the user ID, roles, and an expiry time.
- Signature — a cryptographic stamp over the header and payload, proving the token has not been altered.
Because the first two parts are only Base64-encoded, not encrypted, anyone can read them. Decode a JWT and you see the payload in plain JSON. This is the single most important fact about JWTs.
What the payload holds
Claims come in two kinds. Registered claims are standard short keys with agreed meanings:
sub— the subject (usually the user ID).iat— issued-at time.exp— expiry time. After this, the token should be rejected.issandaud— who issued the token and who it is for.
Alongside these you can add your own custom claims, like a username or permission list. Keep them small — the token travels on every request, and it is readable, so it is no place for secrets.
How the signature protects it
The signature is what makes a JWT trustworthy. The server combines the header, the payload and a secret key (or a private key) and runs them through the signing algorithm. On each request, the server recomputes the signature and checks it matches.
Change a single character of the payload — bump your own role from "user" to "admin" — and the signature no longer matches, so the server rejects it. You cannot forge a valid token without the secret. This is why the token can be readable yet still safe: readability is fine, tamperability is not, and the signature blocks tampering.
The classic mistakes
- Treating the payload as secret. It is not. Never put passwords, card numbers or anything private in a JWT. Assume the user can read every claim.
- Not checking expiry. A token without an enforced
exp, or a server that ignores it, is a token that lives forever. Always validateexp. - The
alg: nonetrap. Early libraries allowed an algorithm of "none", meaning no signature. A careless server would accept it, letting anyone forge tokens. Always pin the expected algorithm server-side. - Weak secrets. An
HS256token is only as strong as its shared secret. A short, guessable secret can be brute-forced offline, after which anyone can mint valid tokens. Use a long, random secret.
Readable is not the same as verified
A key habit: decoding a JWT tells you what it claims, not whether it is valid. Decoding just reverses the Base64. Verification means recomputing the signature with the secret and checking the expiry. A browser-based decoder is perfect for inspecting a token during development — seeing the claims, confirming the expiry, checking the algorithm — but the real validation always happens on the server that holds the key.
So when you are debugging an auth flow and a request is being rejected, decode the token first: is it expired? Is the sub what you expect? Is the algorithm the one your server expects? Inspecting the header and payload, and building a test token to try against your endpoint, both run locally in your browser, so you can take apart and assemble JWTs while developing without sending a live session token to anyone else.