JSON Web Tokens (JWT): Authentication Guide
Complete guide to JWT authentication. Learn JWT structure, implementation, security best practices, and token refresh strategies.
Michael Rodriguez
• API & Security EngineerMichael is an API engineer and security specialist with over 7 years of experience building RESTful services, data conversion pipelines, and authentication systems. He writes practical guides on JSON Web Tokens, API debugging strategies, data science applications of JSON, and modern AI tooling workflows including MCP and JSON-RPC.
What is JWT?
JSON Web Token (JWT) is a compact, URL-safe token format for securely transmitting information between parties.
JWT Structure
A JWT has three parts separated by dots:
header.payload.signature
Header
{
"alg": "HS256",
"typ": "JWT"
}
Payload (Claims)
{
"sub": "1234567890",
"name": "John",
"iat": 1516239022,
"exp": 1516242622
}
Signature
Created by signing header + payload with secret key.
Standard Claims
- sub: Subject (user ID)
- iss: Issuer
- aud: Audience
- exp: Expiration time
- iat: Issued at time
- nbf: Not before
Implementation
Node.js
import jwt from 'jsonwebtoken';
const SECRET = process.env.JWT_SECRET;
// Create token
function createToken(user) {
return jwt.sign(
{ sub: user.id, email: user.email },
SECRET,
{ expiresIn: '1h' }
);
}
// Verify token
function verifyToken(token) {
try {
return jwt.verify(token, SECRET);
} catch (error) {
return null;
}
}
Python
import jwt
from datetime import datetime, timedelta
SECRET = os.environ['JWT_SECRET']
def create_token(user):
payload = {
'sub': user['id'],
'exp': datetime.utcnow() + timedelta(hours=1)
}
return jwt.encode(payload, SECRET, algorithm='HS256')
def verify_token(token):
try:
return jwt.decode(token, SECRET, algorithms=['HS256'])
except jwt.InvalidTokenError:
return None
Express Middleware
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token' });
}
try {
req.user = jwt.verify(token, SECRET);
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
}
app.get('/protected', authMiddleware, (req, res) => {
res.json({ user: req.user });
});
Token Refresh Strategy
// Short-lived access token
function createAccessToken(user) {
return jwt.sign(
{ sub: user.id, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
}
// Long-lived refresh token
function createRefreshToken(user) {
return jwt.sign(
{ sub: user.id, type: 'refresh' },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
}
Security Best Practices
alg: none attack)Common Security Mistakes
1. Don't Store Sensitive Data
// ❌ Bad - payload is base64, not encrypted!
const token = jwt.sign({
userId: 123,
password: 'secret123', // NEVER!
creditCard: '4532...', // NEVER!
ssn: '123-45-6789' // NEVER!
}, secret);
// ✅ Good - only non-sensitive identifiers
const token = jwt.sign({
sub: 123,
email: 'user@example.com',
role: 'user',
permissions: ['read', 'write']
}, secret, { expiresIn: '15m' });
2. Always Validate Algorithm (Prevent alg:none Attack)
// ❌ Vulnerable to algorithm confusion attack
jwt.verify(token, secret);
// ✅ Secure - explicitly allow only specific algorithms
jwt.verify(token, secret, {
algorithms: ['HS256'], // Only allow HMAC SHA-256
issuer: 'myapp.com',
audience: 'myapp-users'
});
3. Use HttpOnly Cookies (Prevent XSS)
// ❌ Vulnerable to XSS attacks
res.json({ token }); // Client stores in localStorage
// ✅ Secure - HttpOnly prevents JavaScript access
res.cookie('accessToken', token, {
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // Prevent CSRF
maxAge: 15 60 1000 // 15 minutes
});
Token Refresh Strategy
Dual Token Pattern (Access + Refresh)
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET;
// Short-lived access token (15 minutes)
function createAccessToken(user) {
return jwt.sign(
{
sub: user.id,
email: user.email,
role: user.role,
type: 'access'
},
ACCESS_TOKEN_SECRET,
{ expiresIn: '15m', issuer: 'myapp.com' }
);
}
// Long-lived refresh token (7 days)
function createRefreshToken(user) {
return jwt.sign(
{
sub: user.id,
type: 'refresh',
tokenVersion: user.tokenVersion // For revocation
},
REFRESH_TOKEN_SECRET,
{ expiresIn: '7d', issuer: 'myapp.com' }
);
}
// Login endpoint
app.post('/api/login', async (req, res) => {
const user = await authenticateUser(req.body.email, req.body.password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const accessToken = createAccessToken(user);
const refreshToken = createRefreshToken(user);
// Store refresh token in database for revocation capability
await db.refreshTokens.create({
userId: user.id,
token: refreshToken,
expiresAt: new Date(Date.now() + 7 24 60 60 1000)
});
// Send tokens via HttpOnly cookies
res.cookie('accessToken', accessToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 15 60 1000 });
res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 7 24 60 60 1000 });
res.json({ user: { id: user.id, email: user.email, role: user.role } });
});
// Refresh endpoint
app.post('/api/refresh', async (req, res) => {
const { refreshToken } = req.cookies;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
try {
const payload = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, { algorithms: ['HS256'] });
// Check if token is in database (not revoked)
const tokenExists = await db.refreshTokens.findOne({
userId: payload.sub,
token: refreshToken
});
if (!tokenExists) {
return res.status(401).json({ error: 'Token revoked' });
}
// Get user and check token version
const user = await db.users.findById(payload.sub);
if (user.tokenVersion !== payload.tokenVersion) {
return res.status(401).json({ error: 'Token invalidated' });
}
// Issue new access token
const newAccessToken = createAccessToken(user);
res.cookie('accessToken', newAccessToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 15 60 1000 });
res.json({ success: true });
} catch (error) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
Token Revocation Strategies
Database Blacklist
// Logout: add token to blacklist
app.post('/api/logout', authenticate, async (req, res) => {
const token = req.cookies.accessToken;
const decoded = jwt.decode(token);
// Store in blacklist until expiration
await db.tokenBlacklist.create({
token,
expiresAt: new Date(decoded.exp 1000)
});
// Delete refresh token
await db.refreshTokens.deleteMany({ userId: req.user.sub });
res.clearCookie('accessToken');
res.clearCookie('refreshToken');
res.json({ success: true });
});
// Middleware to check blacklist
function authenticate(req, res, next) {
const token = req.cookies.accessToken;
if (!token) {
return res.status(401).json({ error: 'No token' });
}
// Check blacklist first
const isBlacklisted = await db.tokenBlacklist.findOne({ token });
if (isBlacklisted) {
return res.status(401).json({ error: 'Token revoked' });
}
try {
req.user = jwt.verify(token, ACCESS_TOKEN_SECRET, { algorithms: ['HS256'] });
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
}
Token Version Increment
// Force logout all devices by incrementing user's token version
app.post('/api/logout-all-devices', authenticate, async (req, res) => {
await db.users.update(
{ id: req.user.sub },
{ $inc: { tokenVersion: 1 } } // Increment version
);
await db.refreshTokens.deleteMany({ userId: req.user.sub });
res.json({ success: true });
});
Client-Side Token Handling
Automatic Token Refresh
class ApiClient {
constructor() {
this.isRefreshing = false;
this.failedQueue = [];
}
async fetch(url, options = {}) {
try {
const response = await fetch(url, {
...options,
credentials: 'include' // Send cookies
});
if (response.status === 401) {
// Token expired, try refresh
return await this.handleTokenExpired(url, options);
}
return response;
} catch (error) {
throw error;
}
}
async handleTokenExpired(url, options) {
if (this.isRefreshing) {
// Queue request until refresh completes
return new Promise((resolve, reject) => {
this.failedQueue.push({ resolve, reject, url, options });
});
}
this.isRefreshing = true;
try {
// Refresh token
await fetch('/api/refresh', {
method: 'POST',
credentials: 'include'
});
// Retry original request
const response = await fetch(url, options);
// Process queued requests
this.failedQueue.forEach(({ resolve, url, options }) => {
resolve(fetch(url, options));
});
this.failedQueue = [];
return response;
} catch (error) {
// Refresh failed, logout
window.location.href = '/login';
throw error;
} finally {
this.isRefreshing = false;
}
}
}
const api = new ApiClient();
// Usage
const response = await api.fetch('/api/protected-data');
const data = await response.json();
Production Best Practices
Environment-Based Secrets
# .env (NEVER commit to git)
JWT_ACCESS_SECRET=a1b2c3d4e5f6... # 64+ random chars
JWT_REFRESH_SECRET=x9y8z7w6v5u4... # Different secret
JWT_ISSUER=myapp.com
JWT_AUDIENCE=myapp-users
import crypto from 'crypto';
// Generate secure secret
const secret = crypto.randomBytes(64).toString('hex');
console.log(secret); // Use this in production
Rate Limiting Auth Endpoints
import rateLimit from 'express-rate-limit';
const authLimiter = rateLimit({
windowMs: 15 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: { error: 'Too many login attempts, try again later' },
standardHeaders: true
});
app.post('/api/login', authLimiter, loginHandler);
app.post('/api/refresh', authLimiter, refreshHandler);
Audit Logging
app.post('/api/login', async (req, res) => {
const user = await authenticateUser(req.body.email, req.body.password);
// Log authentication attempt
await db.auditLog.create({
userId: user?.id,
email: req.body.email,
action: user ? 'LOGIN_SUCCESS' : 'LOGIN_FAILED',
ip: req.ip,
userAgent: req.get('user-agent'),
timestamp: new Date()
});
// ...
});
Testing JWT Implementation
import request from 'supertest';
import app from './app';
describe('JWT Authentication', () => {
it('should issue tokens on login', async () => {
const response = await request(app)
.post('/api/login')
.send({ email: 'test@example.com', password: 'password123' })
.expect(200);
const cookies = response.headers['set-cookie'];
expect(cookies.some(c => c.startsWith('accessToken='))).toBe(true);
expect(cookies.some(c => c.startsWith('refreshToken='))).toBe(true);
});
it('should reject expired tokens', async () => {
const expiredToken = jwt.sign(
{ sub: 123 },
ACCESS_TOKEN_SECRET,
{ expiresIn: '-1h' } // Already expired
);
await request(app)
.get('/api/protected')
.set('Cookie', accessToken=${expiredToken})
.expect(401);
});
it('should refresh access token', async () => {
// Login first
const loginRes = await request(app)
.post('/api/login')
.send({ email: 'test@example.com', password: 'password123' });
const refreshToken = loginRes.headers['set-cookie']
.find(c => c.startsWith('refreshToken='))
.split(';')[0]
.split('=')[1];
// Refresh
const refreshRes = await request(app)
.post('/api/refresh')
.set('Cookie', refreshToken=${refreshToken})
.expect(200);
expect(refreshRes.headers['set-cookie']).toBeDefined();
});
});
Conclusion
JWT is powerful for stateless authentication but requires careful implementation. Use short-lived access tokens (15m), long-lived refresh tokens (7d), HttpOnly cookies (not localStorage), and explicit algorithm validation. Always validate all claims (exp, iss, aud), implement token revocation via blacklist or token versioning, and never store sensitive data in the payload. For production, use strong random secrets (64+ chars), rate limit auth endpoints, audit all auth events, and rotate secrets regularly.
Related Articles
What is JSON? Complete Guide for Beginners 2026
Learn what JSON is, its syntax, data types, and use cases. A comprehensive beginner-friendly guide to understanding JavaScript Object Notation.
JSON APIs and REST Services: Complete Development Guide
Learn to build and consume JSON-based REST APIs. Covers HTTP methods, authentication, best practices, and real-world implementation examples.
JavaScript JSON: Parse, Stringify, and Best Practices
Complete guide to JSON in JavaScript. Learn JSON.parse(), JSON.stringify(), error handling, and advanced techniques for web development.