JSON Web Tokens (JWT) are the most widely used standard for stateless authentication in modern web applications. This guide explains how JWT works, its structure, security best practices, and common pitfalls.
What is JWT?
A JWT is a compact, URL-safe token that carries claims (data) between two parties. It consists of three parts separated by dots: Header.Payload.Signature. The token is Base64URL-encoded, making it suitable for HTTP headers, cookies, and URLs.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header Payload Signature
(algorithm) (user claims) (verification hash)JWT Structure Decoded
Header
The header identifies the algorithm used to sign the token:
{
"alg": "HS256", // Algorithm: HMAC-SHA256
"typ": "JWT" // Token type
}Payload (Claims)
The payload contains claims — statements about the user and metadata:
{
"sub": "1234567890", // Subject (user ID)
"name": "John Doe", // Custom claim
"email": "john@example.com",
"role": "admin", // Custom claim
"iss": "https://api.example.com", // Issuer
"aud": "https://app.example.com", // Audience
"iat": 1516239022, // Issued at (Unix timestamp)
"exp": 1516242622 // Expiration (1 hour later)
}Signature
The signature ensures the token has not been tampered with:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
)How JWT Authentication Works
JWT vs Session-Based Auth
| Feature | JWT (Token-based) | Session (Server-side) |
|---|---|---|
| Storage | Client-side (cookie/memory) | Server-side (database/Redis) |
| Scalability | Stateless — scales easily | Requires shared session store |
| Server load | No DB lookup per request | DB/Redis lookup each request |
| Revocation | Hard (need blacklist) | Easy (delete session) |
| Size | Larger (contains claims) | Small session ID |
| Mobile-friendly | Yes (no cookies needed) | Harder without cookies |
| Security | Vulnerable if secret leaked | Vulnerable to session fixation |
Common JWT Claims
| Claim | Name | Description |
|---|---|---|
iss | Issuer | Who created the token (e.g., your auth server URL) |
sub | Subject | The user or entity the token represents (usually user ID) |
aud | Audience | Intended recipient (e.g., your API URL) |
exp | Expiration | Unix timestamp after which the token is invalid |
iat | Issued At | Unix timestamp when the token was created |
nbf | Not Before | Token is not valid before this timestamp |
jti | JWT ID | Unique identifier to prevent token replay |
Security Best Practices
- Always verify signatures — Never trust a JWT without verification. Use a well-tested library.
- Use short expiration times — Set exp to 15-60 minutes. Use refresh tokens for longer sessions.
- Store in httpOnly cookies — Prevents XSS attacks from reading the token. Add Secure and SameSite flags.
- Never store sensitive data in payload — JWT payloads are Base64-encoded, not encrypted. Anyone can decode them.
- Use strong secrets — For HMAC, use at least 256 bits of randomness. For RSA/EC, use proper key sizes.
- Implement token revocation — Use a blacklist or short-lived tokens with refresh tokens.
Common JWT Vulnerabilities
An attacker modifies the header to use no algorithm. Fix: Always validate the algorithm on the server side. Never accept "none".
Weak HMAC secrets can be brute-forced with tools like jwt-cracker. Fix: Use at least 256-bit random secrets.
A stolen token can be reused until it expires. Fix: Use short expiration, bind to IP/device, implement refresh rotation.
Implementation Example
// Node.js with jsonwebtoken library
const jwt = require('jsonwebtoken');
// Sign (create token)
const token = jwt.sign(
{ sub: user.id, name: user.name, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '1h', issuer: 'https://api.example.com' }
);
// Verify (validate token)
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'https://api.example.com',
algorithms: ['HS256'], // Prevent alg switching!
});
console.log(decoded.sub); // User ID
} catch (err) {
// Token is invalid or expired
console.error('JWT verification failed:', err.message);
}
// Express middleware
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
return res.status(401).json({ error: 'Invalid token' });
}
}Decode and inspect any JWT instantly
FAQ
Is JWT encrypted?
No. Standard JWT (JWS) is only signed, not encrypted. The payload is Base64URL-encoded and can be decoded by anyone. Use JWE (JSON Web Encryption) if you need to hide the payload content.
Should I store JWT in localStorage or cookies?
httpOnly cookies are more secure because JavaScript cannot access them, preventing XSS token theft. localStorage is vulnerable to XSS but easier to implement for SPAs.
How do I handle JWT expiration?
Use a short-lived access token (15-60 min) with a longer-lived refresh token. When the access token expires, use the refresh token to get a new one without re-authentication.
What is the difference between HS256 and RS256?
HS256 (HMAC-SHA256) uses a shared secret for both signing and verification — both parties need the same key. RS256 (RSA-SHA256) uses a private key to sign and a public key to verify — ideal for distributed systems.
Can I revoke a JWT?
JWTs are stateless, so they cannot be directly revoked. Common approaches: maintain a token blacklist, use very short expiration times, or implement a token version/generation system in the database.