The Three-Part Structure
A JWT is three Base64Url-encoded strings joined by dots: header.payload.signature. The encoding is not encryption — the header and payload are fully readable by anyone.
Part 1: Header
Decoded, the header is a small JSON object specifying the signing algorithm and token type:
{
"alg": "HS256", // HMAC-SHA256 (symmetric)
"typ": "JWT"
}Common algorithms: HS256 (HMAC with SHA-256, shared secret), RS256 (RSA with SHA-256, public/private key pair), ES256 (ECDSA, smaller and faster than RSA). For public APIs, prefer RS256 or ES256 — they allow signature verification without sharing the private key.
Part 2: Payload (Claims)
The payload contains "claims" — statements about the user and the token itself. Standard registered claims defined in RFC 7519:
{
"sub": "user_42", // Subject: who the token is about
"iss": "auth.example.com", // Issuer: who created the token
"aud": "api.example.com", // Audience: intended recipient
"iat": 1749254400, // Issued at (Unix timestamp)
"exp": 1749258000, // Expiry (Unix timestamp)
"jti": "tok_8a3f2b1c", // JWT ID (unique token identifier)
"roles": ["admin"], // Custom claim
"name": "Alice Chen" // Custom claim
}The Base64Url encoding is trivially reversible. Anyone who intercepts a JWT can read the payload. Never put sensitive data (passwords, credit card numbers, PII beyond a user ID) in the payload. If you need a confidential payload, use JWE (JSON Web Encryption, RFC 7516) instead.
Part 3: Signature
The signature is how the server verifies the token hasn't been tampered with. For HS256:
signature = HMAC-SHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)The server that issued the token signs it with a secret (HS256) or private key (RS256). When a request arrives with a JWT, the server recomputes the signature from the received header and payload, then compares it to the token's signature. If they match — and the exp claim is in the future — the token is valid. The server doesn't need a database lookup. This is the key advantage of JWTs over session tokens.
The Most Dangerous JWT Vulnerabilities
1. Algorithm Confusion (the alg: none attack)
Early JWT libraries trusted the alg field in the header. An attacker could change it to "none", strip the signature, and the library would accept the unsigned token as valid. Always explicitly specify the expected algorithm in your verification code — never accept whatever the token declares.
2. HS256 with an RS256 Public Key
If a server uses RS256 and the public key is known (it often is), an attacker can switch the algorithm to HS256 and sign the token with the public key as the HMAC secret. Vulnerable libraries will accept this. Again: always enforce the expected algorithm server-side.
3. No Expiry Claim
A JWT without an exp claim is valid forever. Always set expiry. Short-lived tokens (15–60 minutes) with refresh tokens are the standard pattern.
4. Weak Secrets
HS256 tokens can be brute-forced if the secret is short. Use at minimum 256 bits (32 bytes) of cryptographically random entropy for HMAC secrets. Never use passwords or predictable values.
JWT vs Session Tokens: When to Use Which
| Dimension | JWT (Stateless) | Session Token (Stateful) |
|---|---|---|
| Server storage | None — self-contained | Session store (Redis, DB) required |
| Revocation | Hard — need a blocklist or short expiry | Easy — delete from session store |
| Scalability | Excellent — any server can verify | Requires sticky sessions or shared store |
| Payload size | Grows with claims (sent every request) | Tiny (opaque token only) |
| Best for | Microservices, APIs, cross-domain auth | Traditional web apps, single-server setups |
The Refresh Token Pattern
Short-lived access tokens (15 min) paired with long-lived refresh tokens (7–30 days) is the industry standard:
- User logs in → server issues access token (JWT, 15 min) + refresh token (opaque, 30 days, stored in DB).
- Client sends access token with every API request.
- When access token expires, client sends refresh token to
/auth/refresh. - Server validates refresh token in DB, issues new access token.
- On logout, server deletes refresh token from DB — effectively revoking all future access.
Store JWTs in httpOnly, Secure, SameSite=Strict cookies — not localStorage. Cookies are inaccessible to JavaScript, which eliminates XSS token theft. localStorage is accessible to any script on the page — a single XSS vulnerability exposes every user's token.