JWT Explained: How JSON Web Tokens Work
April 14, 2026 · 8 min read
JSON Web Tokens (JWTs) are the most common way to represent authentication claims in modern web applications. They appear in Authorization headers, cookies, and query strings. Understanding their structure — and their weaknesses — is essential for any developer building authenticated APIs.
The Structure of a JWT
A JWT is three Base64URL-encoded JSON objects joined by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 .eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxMTg0MzIwMH0 .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The three parts are: header, payload, and signature.
Header
Specifies the token type and the signing algorithm used:
{
"alg": "HS256", // HMAC-SHA256
"typ": "JWT"
}Payload (Claims)
Contains the actual data. Standard claims are defined by RFC 7519:
{
"sub": "user_123", // subject (user ID)
"iss": "https://api.io9.me",// issuer
"aud": "https://app.io9.me",// audience
"iat": 1711756800, // issued at (Unix timestamp)
"exp": 1711843200, // expiry (Unix timestamp)
"role": "admin" // custom claim
}Important: the payload is Base64URL-encoded, not encrypted. Anyone can decode it. Never put passwords, secrets, or sensitive PII in a JWT payload.
Signature
The signature proves the token was issued by a trusted party and has not been tampered with:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
When your server receives a JWT, it recomputes this signature using the same secret (or public key for asymmetric algorithms). If the result matches, the token is valid. If anything in the header or payload was changed, the signature will not match.
Signing Algorithms
| Algorithm | Type | Use when |
|---|---|---|
| HS256 | Symmetric (shared secret) | Single service owns both issuing and verification |
| RS256 | Asymmetric (RSA) | Different services verify tokens (private key signs, public key verifies) |
| ES256 | Asymmetric (ECDSA) | Same as RS256 but with smaller keys and faster verification |
For microservices, prefer RS256 or ES256: the auth service holds the private key and signs tokens, while downstream services only need the public key to verify. This way, a compromised downstream service cannot forge tokens.
Using JWTs in Practice
Issuing a token (Node.js)
import jwt from "jsonwebtoken";
const token = jwt.sign(
{ sub: "user_123", role: "admin" },
process.env.JWT_SECRET,
{ expiresIn: "1h", issuer: "https://api.example.com" }
);Verifying a token
try {
const payload = jwt.verify(token, process.env.JWT_SECRET, {
issuer: "https://api.example.com",
});
console.log(payload.sub); // "user_123"
} catch (err) {
// TokenExpiredError, JsonWebTokenError, etc.
return res.status(401).json({ error: "Invalid token" });
}Sending a token
// Authorization header (most common for APIs) Authorization: Bearer <token> // HTTP-only cookie (more secure for web apps) Set-Cookie: token=<jwt>; HttpOnly; Secure; SameSite=Strict
Common Security Mistakes
1. The "alg: none" attack
Early JWT libraries accepted tokens with "alg": "none" and no signature. Always explicitly specify which algorithms your server accepts during verification — never trust the algorithm declared in the header.
2. Weak secrets
For HS256, use a secret of at least 256 bits (32 random bytes). Short or guessable secrets can be brute-forced offline against a captured token. Generate them with openssl rand -base64 32.
3. Missing expiry
Always set exp. JWTs cannot be revoked server-side (unless you maintain a blocklist), so a short lifespan limits the damage from a stolen token. Access tokens should typically expire in 15 minutes to 1 hour; use refresh tokens for longer sessions.
4. Storing in localStorage
Storing JWTs in localStorage makes them accessible to any JavaScript on the page, including injected scripts (XSS). HTTP-only cookies are immune to XSS. Use cookies with HttpOnly; Secure; SameSite=Strict for web applications.
JWT vs Sessions
JWTs are stateless — the server does not need to look anything up. This makes them well-suited for distributed systems and microservices. Traditional sessions require a server-side store (Redis, database) but are easy to invalidate instantly. Neither is universally better; choose based on your architecture.
Use the Text Tools on io9.me to Base64-decode a JWT payload and inspect its claims directly in your browser.