JWT Authentication: Complete Guide with Best Practices
JSON Web Tokens (JWT) are a popular method for authentication in modern web applications. Let’s explore how to implement JWT authentication securely.
What is JWT?
JWT is a compact, URL-safe token format for securely transmitting information between parties. A JWT consists of three parts:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header.Payload.Signature
JWT Structure
Header:
{
"alg": "HS256",
"typ": "JWT"
}
Payload:
{
"userId": "123",
"email": "user@example.com",
"exp": 1735689600
}
Signature:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Basic Implementation
Generate JWT (Node.js)
const jwt = require('jsonwebtoken');
function generateAccessToken(user) {
const payload = {
userId: user.id,
email: user.email,
role: user.role
};
return jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: '15m', // Short-lived access token
issuer: 'your-app',
audience: 'your-app-users'
});
}
function generateRefreshToken(user) {
const payload = {
userId: user.id,
tokenVersion: user.tokenVersion // For token revocation
};
return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET, {
expiresIn: '7d', // Long-lived refresh token
issuer: 'your-app'
});
}
Verify JWT
function verifyToken(token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'your-app',
audience: 'your-app-users'
});
return decoded;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new Error('Token expired');
}
if (error instanceof jwt.JsonWebTokenError) {
throw new Error('Invalid token');
}
throw error;
}
}
Authentication Flow
Login Endpoint
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
// 1. Find user
const user = await db.users.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// 2. Verify password
const validPassword = await bcrypt.compare(password, user.passwordHash);
if (!validPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// 3. Generate tokens
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// 4. Store refresh token (database or Redis)
await db.refreshTokens.insert({
userId: user.id,
token: refreshToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
// 5. Send tokens
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({ accessToken });
});
Protected Route Middleware
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
const user = verifyToken(token);
req.user = user;
next();
} catch (error) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
}
// Use middleware
app.get('/api/profile', authenticateToken, async (req, res) => {
const user = await db.users.findById(req.user.userId);
res.json(user);
});
Refresh Token Endpoint
app.post('/api/auth/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
try {
// 1. Verify refresh token
const payload = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
// 2. Check if token exists in database
const storedToken = await db.refreshTokens.findOne({
userId: payload.userId,
token: refreshToken
});
if (!storedToken) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
// 3. Get user and verify token version
const user = await db.users.findById(payload.userId);
if (user.tokenVersion !== payload.tokenVersion) {
return res.status(403).json({ error: 'Token revoked' });
}
// 4. Generate new access token
const accessToken = generateAccessToken(user);
res.json({ accessToken });
} catch (error) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
});
Logout Endpoint
app.post('/api/auth/logout', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (refreshToken) {
// Remove refresh token from database
await db.refreshTokens.deleteOne({ token: refreshToken });
}
res.clearCookie('refreshToken');
res.json({ message: 'Logged out successfully' });
});
Token Revocation
Increment Token Version
app.post('/api/auth/revoke-all', authenticateToken, async (req, res) => {
// Increment token version to invalidate all tokens
await db.users.updateOne(
{ _id: req.user.userId },
{ $inc: { tokenVersion: 1 } }
);
// Delete all refresh tokens
await db.refreshTokens.deleteMany({ userId: req.user.userId });
res.json({ message: 'All tokens revoked' });
});
Blacklist Tokens (Redis)
const redis = require('redis');
const client = redis.createClient();
async function blacklistToken(token, expiresIn) {
await client.setex(`blacklist:${token}`, expiresIn, 'true');
}
async function isTokenBlacklisted(token) {
const result = await client.get(`blacklist:${token}`);
return result === 'true';
}
// Modified middleware
function authenticateToken(req, res, next) {
const token = req.headers['authorization']?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
// Check blacklist
if (await isTokenBlacklisted(token)) {
return res.status(403).json({ error: 'Token revoked' });
}
try {
const user = verifyToken(token);
req.user = user;
next();
} catch (error) {
return res.status(403).json({ error: 'Invalid token' });
}
}
Frontend Integration
Store Tokens
// Store access token in memory (not localStorage!)
let accessToken = null;
async function login(email, password) {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
credentials: 'include' // Include cookies
});
const data = await response.json();
accessToken = data.accessToken;
}
Axios Interceptor
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
withCredentials: true
});
// Add access token to requests
api.interceptors.request.use((config) => {
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});
// Refresh token on 401
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Refresh token
const response = await axios.post('/api/auth/refresh', {}, {
withCredentials: true
});
accessToken = response.data.accessToken;
// Retry original request
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return api(originalRequest);
} catch (refreshError) {
// Redirect to login
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
Security Best Practices
1. Use Strong Secrets
// ❌ Bad
const JWT_SECRET = 'mysecret';
// ✅ Good - generate random secret
// Use: openssl rand -base64 64
const JWT_SECRET = process.env.JWT_SECRET;
2. Short Access Token Expiry
// ✅ Good - short-lived access tokens
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
// ✅ Good - long-lived refresh tokens
const refreshToken = jwt.sign(payload, secret, { expiresIn: '7d' });
3. Secure Cookie Settings
res.cookie('refreshToken', token, {
httpOnly: true, // Prevents XSS
secure: true, // HTTPS only
sameSite: 'strict', // Prevents CSRF
maxAge: 7 * 24 * 60 * 60 * 1000
});
4. Never Store Sensitive Data
// ❌ Bad - sensitive data in JWT
const token = jwt.sign({
userId: user.id,
password: user.password, // Never!
creditCard: user.cc // Never!
}, secret);
// ✅ Good - only necessary data
const token = jwt.sign({
userId: user.id,
email: user.email,
role: user.role
}, secret);
5. Validate All Claims
const decoded = jwt.verify(token, secret, {
issuer: 'your-app',
audience: 'your-app-users',
algorithms: ['HS256'] // Prevent algorithm confusion
});
Common Mistakes
❌ Storing JWT in localStorage
// ❌ Bad - vulnerable to XSS
localStorage.setItem('token', accessToken);
// ✅ Good - store in memory or httpOnly cookie
let accessToken = null; // Memory
// or
res.cookie('token', refreshToken, { httpOnly: true });
❌ Long-lived Access Tokens
// ❌ Bad - can't revoke easily
jwt.sign(payload, secret, { expiresIn: '30d' });
// ✅ Good - short-lived with refresh
jwt.sign(payload, secret, { expiresIn: '15m' });
❌ No Token Revocation
// ✅ Good - implement token versioning
const payload = {
userId: user.id,
tokenVersion: user.tokenVersion
};
Testing
const request = require('supertest');
describe('Authentication', () => {
it('should login and return tokens', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'user@example.com', password: 'password' });
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('accessToken');
});
it('should access protected route with valid token', async () => {
const token = generateAccessToken({ userId: '123' });
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
});
it('should reject invalid token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', 'Bearer invalid');
expect(response.status).toBe(403);
});
});
Conclusion
JWT authentication is powerful when implemented correctly. Remember to:
- Use short-lived access tokens
- Implement refresh token rotation
- Store tokens securely
- Validate all claims
- Implement token revocation
- Never store sensitive data in JWTs
Follow these best practices to build secure, scalable authentication systems.