jwt
How JWTs Actually Work: Header, Payload, Signature & Common Mistakes
A developer-friendly explainer of JSON Web Tokens β what the three parts really are, what signing does and does NOT do, how validation works, and the security mistakes that cause real breaches.
JWTs are everywhere in modern auth, and almost everywhere slightly misunderstood. The two biggest myths: that theyβre encrypted (theyβre not), and that having a valid signature means the token is safe to trust blindly (it isnβt). Letβs open one up and see whatβs really going on.
A JWT is three Base64url strings joined by dots
A JSON Web Token looks like one long opaque string, but itβs three parts separated by .:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3...Qssw5c.SflKxwRJ...
βββββββββββ header βββββββββββ βββββββββββ payload βββββββββββ ββ signature ββ
Each part is Base64url-encoded (not encrypted). Decode the first two and you get plain JSON.
1. Header β how itβs signed
{ "alg": "HS256", "typ": "JWT" }
alg is the signing algorithm; typ is the token type.
2. Payload β the claims
{
"sub": "1234567890",
"name": "Ada Lovelace",
"role": "admin",
"iat": 1735689600,
"exp": 1735693200
}
These are claims β statements about the user/session. Standard registered claims include:
| Claim | Meaning |
|---|---|
iss | Issuer (who made the token) |
sub | Subject (usually the user ID) |
aud | Audience (who itβs intended for) |
exp | Expiration time (Unix seconds) |
nbf | Not valid before |
iat | Issued at |
jti | Unique token ID |
3. Signature β the tamper seal
The signature is computed over the encoded header and payload:
HMAC-SHA256( base64url(header) + "." + base64url(payload), secret )
The single most important fact: a JWT is signed, not encrypted
Anyone holding the token can Base64url-decode the payload and read every claim β role, email, everything. The signature doesnβt hide the data; it only proves the data hasnβt been changed since the issuer signed it.
So two rules follow immediately:
- Never put secrets in a JWT payload (passwords, API keys, PII you donβt want exposed). Treat it as public, readable JSON.
- The value of the signature is integrity, not confidentiality. If you need the contents hidden, you need encryption (JWE) or just donβt put it in the token.
How signing actually works
There are two families of algorithms:
- HMAC (HS256/384/512) β symmetric. The same secret signs and verifies. Simple, fast, but every service that verifies must hold the shared secret.
- RSA / ECDSA (RS256, ES256) β asymmetric. A private key signs; a public key verifies. Your auth server keeps the private key; any number of services can verify with the public key without being able to mint tokens. This is what you want for distributed systems and third-party verification.
Verification recomputes the signature over the received header+payload and checks it matches. If even one byte of the payload was altered, the signatures wonβt match and the token is rejected.
Validating a token β the full checklist
A correct verifier does all of these, not just the signature:
- Signature is valid for the expected algorithm and key.
expis in the future (the token hasnβt expired).nbf(if present) is in the past.issmatches the issuer you trust.audmatches your service.- The
algis one you explicitly allow.
Skipping any of these is how tokens get misused.
The security mistakes that cause real breaches
1. The alg: none attack. The JWT spec allows an βunsignedβ token with alg: none. Naive libraries once accepted these β an attacker just sets alg to none, strips the signature, and forges any payload. Always pin the expected algorithm on the verification side; never trust the alg value from the incoming header.
2. Algorithm confusion (RS256 β HS256). If your verifier is told βthe key is Xβ but lets the token pick the algorithm, an attacker can take your public RSA key, sign a token with it using HMAC, and your server β using that public key as the HMAC secret β validates it. Fix: hard-code which algorithm goes with which key type.
3. Not checking expiration. A signed token with no exp check is a permanent credential. Always set exp, and always verify it.
4. Storing JWTs in localStorage. localStorage is readable by any JavaScript on the page, so an XSS bug hands your tokens to the attacker. Prefer HttpOnly, Secure, SameSite cookies for session tokens so scripts canβt read them.
5. Treating JWTs as revocable. A signed token is valid until it expires β you canβt βlog it outβ server-side without extra machinery (a denylist, short lifetimes + refresh tokens, or rotating keys). Keep access-token lifetimes short.
When to actually use one
JWTs shine for stateless, cross-service authorization: an API gateway or microservice can verify a token with a public key and trust the claims without a database round-trip. For a classic single-server web app with a session, a plain server-side session cookie is often simpler and gives you easy revocation. Pick the tool for the topology.
Inspect and experiment
The fastest way to internalize all of this is to take a token apart:
- Paste any token into the JWT Decoder to see the header, payload, and claims in plain JSON β and confirm for yourself that the payload is readable without any secret.
- Mint test tokens with chosen claims and algorithms in the JWT Generator to see how the signature changes when the payload does.
Both run entirely in your browser β no token data leaves your machine.