JWT explained
The anatomy of a JSON Web Token — standard claims, signature algorithms, and the security footguns (alg: none, algorithm confusion, missing exp) that ship real vulnerabilities.
A JSON Web Token (JWT) looks like a random string, but it's really three base64url-encoded chunks glued together with dots. Understanding what's in each chunk — and which parts you can and can't trust — is the difference between a secure auth setup and a public incident.
Anatomy
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← header (base64url JSON) .eyJzdWIiOiIxMjMiLCJleHAiOjE3MzU2ODk2MDB9 ← payload (base64url JSON) .abc123signaturebytes ← signature
Split on ., base64url-decode the first two segments, and you have plain JSON. The signature is opaque bytes computed overheader.payload using the algorithm named in the header. Try it live with the JWT decoder.
The header
Small object, usually two fields:
alg— signing algorithm (HS256,RS256,ES256, etc.).typ— almost always"JWT".
The payload — standard claims
Anything can go in the payload, but RFC 7519 reserves a handful of short names:
| Claim | Meaning |
|---|---|
iss | Issuer — who minted the token. |
sub | Subject — the user or entity the token is about. |
aud | Audience — who the token is intended for; verify this. |
exp | Expiration (Unix seconds). Reject if now > exp. |
nbf | Not before (Unix seconds). Reject if now < nbf. |
iat | Issued at (Unix seconds). |
jti | Unique token ID, useful for revocation lists. |
The signature — what actually makes a JWT secure
The payload is not encrypted. Anyone with the token can decode it. The signature only proves the token hasn't been modified since it was minted by someone holding the signing key.
- HS256 — HMAC with a shared secret. Both sides need the secret. Fine for a single service.
- RS256 / ES256 — asymmetric. Signer holds the private key; verifiers use the public key. Use this if more than one service verifies tokens.
The security footguns
alg: none— the spec allows an unsigned token. If your library accepts it, an attacker can strip the signature and forge any payload. Explicitly whitelist algorithms.- Algorithm confusion. A classic bug: a token signed with the RSA public key as an HMAC secret. Always pin the expected algorithm; don't infer it from the header.
- Missing
exp. A token with no expiry is valid forever. Always setexp, keep it short (minutes to hours), and rotate refresh tokens separately. - Storing secrets in the payload. The payload is base64 — that's encoding, not encryption. Never put passwords, private data, or anything you wouldn't email in plain text.
- Trusting a decoded token client-side. Decoding is not verification. The server is the only place you can trust a JWT, because it's the only place with the key.
- Not verifying
audandiss. A token minted for another service in the same auth ecosystem may verify cryptographically but shouldn't be accepted.
When to use JWTs vs sessions
JWTs shine when a token needs to be verified by many services without a shared session store — think microservices, mobile clients, third-party API access. They're worse when you need instant revocation, because there's no server-side state to invalidate. Traditional server sessions are simpler, safer, and revocable in one query; if a single monolith serves your app, stateful sessions are usually the better default.
A safe verification checklist
- Split on
.and confirm 3 segments. - Verify the signature with a library, pinned to the expected algorithm.
- Check
expand (if present)nbfagainst the current time, with a small clock-skew tolerance (~30 s). - Check
issandaudmatch your service. - Only then trust
sub, roles, and other claims.
Once verified, treat the payload like any other request input: don't embed it into SQL, don't eval it, and re-authorize each action against the current state of the user in your database.