JSON Web Tokens are everywhere — login sessions, API auth, single sign-on. Yet a surprising number of developers treat them as opaque magic. They are not magic. A JWT is three pieces of Base64url text joined by dots, and once you decode them, there is nothing left to wonder about.
The three parts
A JWT looks like xxxxx.yyyyy.zzzzz. Each segment is separated by a dot:
- header — metadata about the token: which signing algorithm and which key id.
- payload — the claims: who the user is, when it expires, and anything else you put there.
- signature — a cryptographic signature over the first two parts.
Decode the header and you get JSON like:
{
"alg": "HS256",
"typ": "JWT",
"kid": "key-2026-09"
}
What the payload actually contains
The payload is the part you care about. A typical decoded payload:
{
"sub": "user_8821",
"name": "Ada Lovelace",
"role": "admin",
"exp": 1785000000
}
These fields are called claims. A few have reserved meanings:
| Claim | Meaning |
|---|---|
sub | Subject — who the token is about (usually a user id) |
exp | Expiration time — token is rejected after this (Unix seconds) |
iat | Issued at — when the token was created |
nbf | Not before — token is invalid before this time |
iss | Issuer — who created the token |
aud | Audience — who is allowed to accept it |
The payload is readable by anyone
This is the part newcomers get wrong. The header and payload are Base64url-encoded, not encrypted. No key is involved. Anyone who holds the token can decode it in a browser console in two lines:
const [, p] = token.split('.');
JSON.parse(atob(p.replace(/-/g,'+').replace(/_/g,'/')));
So the payload is an open book. Never put a password, a credit card number, or anything sensitive in a JWT payload. The encoding only exists to fit structured data into a text token, not to hide it.
What the signature is for
The signature is the only part that involves a secret or a private key. It is computed over the header and payload:
signature = sign(base64url(header) + "." + base64url(payload), secretOrPrivateKey)
Its job is twofold: prove the token was issued by someone holding the key, and prove the header and payload were not tampered with in transit. If an attacker changes "role":"user" to "role":"admin", the signature no longer matches and the server rejects it.
Crucially, the signature provides integrity and authenticity, not confidentiality. It stops tampering; it does not stop reading.
HS256 vs RS256
| Algorithm | Key | How verification works |
|---|---|---|
| HS256 | One shared secret | Both sides sign and verify with the same key |
| RS256 | Public/private keypair | Auth server signs with private key; others verify with public key |
HS256 is simpler — one secret, both issuing and verifying. The downside is that anyone who can verify can also forge, because they hold the same secret. RS256 splits the roles: the auth server keeps the private key, and every other service verifies with a public key it cannot use to sign. For many services trusting one issuer, RS256 scales better.
Why JWT is a poor session store
A JWT is stateless — the server needs no session table, because everything is in the token. That sounds like a win, but it has a sharp edge: you cannot easily revoke a JWT before it expires. Log a user out, and their still-valid token keeps working until exp hits. To fix that you end up building a revocation list, which puts the state back that you were trying to avoid.
For short-lived API access tokens, stateless is fine. For "remember me on this device for two weeks," a server-side session or a refresh-token flow is usually the better tool.
When not to use a JWT
- When you need to invalidate tokens the moment a user logs out — use a session.
- When the payload must stay secret — JWT payloads are readable by design.
- When you just need a random session id — a plain opaque token is simpler and smaller.
Debugging a JWT safely
Never paste a live production token into a random third-party website. Use a local, client-side decoder you trust. The JWT decoder on this site runs entirely in your browser, so the token never leaves your machine. Paste it in, read the header and payload, and confirm exp and aud are what you expect.