JWT Authentication & Authorization Guide
Introduction
JSON Web Tokens (JWT) provide a stateless authentication mechanism perfect for modern APIs and microservices. This guide covers JWT implementation, refresh tokens, role-based access control (RBAC), and security best practices.
1. JWT Basics
JWT Structure
JWT Format: header.payload.signature
Header (Algorithm & Type):
{
"alg": "HS256",
"typ": "JWT"
}
Payload (Claims):
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}
Signature:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
2. Generate & Verify JWT
// Install jsonwebtoken
npm install jsonwebtoken
npm install --save-dev @types/jsonwebtoken
// src/utils/jwt.ts
import jwt from 'jsonwebtoken';
interface TokenPayload {
userId: string;
email: string;
role: string;
}
export const generateAccessToken = (payload: TokenPayload): string => {
return jwt.sign(
payload,
process.env.JWT_SECRET!,
{ expiresIn: '15m' } // Short-lived
);
};
export const generateRefreshToken = (payload: TokenPayload): string => {
return jwt.sign(
payload,
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '7d' } // Long-lived
);
};
export const verifyAccessToken = (token: string): TokenPayload => {
return jwt.verify(token, process.env.JWT_SECRET!) as TokenPayload;
};
export const verifyRefreshToken = (token: string): TokenPayload => {
return jwt.verify(token, process.env.JWT_REFRESH_SECRET!) as TokenPayload;
};
3. Authentication Flow
Login Endpoint
// src/controllers/auth.controller.ts
import { Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import User from '../models/user.model';
import { generateAccessToken, generateRefreshToken } from '../utils/jwt';
export const login = async (req: Request, res: Response) => {
try {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Verify password
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate tokens
const payload = {
userId: user.id,
email: user.email,
role: user.role,
};
const accessToken = generateAccessToken(payload);
const refreshToken = generateRefreshToken(payload);
// Store refresh token in DB
await User.findByIdAndUpdate(user.id, {
refreshToken,
lastLogin: new Date(),
});
// Set refresh token in httpOnly cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});
res.json({
accessToken,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
},
});
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
};
Refresh Token Endpoint
export const refreshToken = async (req: Request, res: Response) => {
try {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token not provided' });
}
// Verify refresh token
const decoded = verifyRefreshToken(refreshToken);
// Check if token exists in DB
const user = await User.findOne({
_id: decoded.userId,
refreshToken,
});
if (!user) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Generate new tokens
const payload = {
userId: user.id,
email: user.email,
role: user.role,
};
const newAccessToken = generateAccessToken(payload);
const newRefreshToken = generateRefreshToken(payload);
// Update refresh token in DB
await User.findByIdAndUpdate(user.id, {
refreshToken: newRefreshToken,
});
// Update cookie
res.cookie('refreshToken', newRefreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken: newAccessToken });
} catch (error) {
res.status(401).json({ error: 'Invalid refresh token' });
}
};
Logout Endpoint
export const logout = async (req: Request, res: Response) => {
try {
const userId = req.user.id; // From auth middleware
// Remove refresh token from DB
await User.findByIdAndUpdate(userId, {
$unset: { refreshToken: 1 },
});
// Clear cookie
res.clearCookie('refreshToken');
res.json({ message: 'Logged out successfully' });
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
};
4. Authentication Middleware
// src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { verifyAccessToken } from '../utils/jwt';
import User from '../models/user.model';
declare global {
namespace Express {
interface Request {
user?: any;
}
}
}
export const authenticate = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
// Get token from header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
// Verify token
const decoded = verifyAccessToken(token);
// Get user from database
const user = await User.findById(decoded.userId).select('-password');
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
// Attach user to request
req.user = user;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Token expired',
code: 'TOKEN_EXPIRED'
});
}
res.status(401).json({ error: 'Invalid token' });
}
};
5. Role-Based Access Control (RBAC)
Authorization Middleware
// src/middleware/authorize.middleware.ts
import { Request, Response, NextFunction } from 'express';
export const authorize = (...allowedRoles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
error: 'Access denied',
message: `Requires one of: ${allowedRoles.join(', ')}`
});
}
next();
};
};
// Usage in routes
import { authenticate } from './middleware/auth.middleware';
import { authorize } from './middleware/authorize.middleware';
// Only admins can access
router.delete('/users/:id',
authenticate,
authorize('admin'),
deleteUser
);
// Admins and moderators
router.patch('/posts/:id/approve',
authenticate,
authorize('admin', 'moderator'),
approvePost
);
Permission-Based Authorization
// More granular permissions
enum Permission {
CREATE_USER = 'create:user',
READ_USER = 'read:user',
UPDATE_USER = 'update:user',
DELETE_USER = 'delete:user',
MANAGE_POSTS = 'manage:posts',
}
interface Role {
name: string;
permissions: Permission[];
}
const roles: Record = {
admin: {
name: 'admin',
permissions: [
Permission.CREATE_USER,
Permission.READ_USER,
Permission.UPDATE_USER,
Permission.DELETE_USER,
Permission.MANAGE_POSTS,
],
},
moderator: {
name: 'moderator',
permissions: [
Permission.READ_USER,
Permission.MANAGE_POSTS,
],
},
user: {
name: 'user',
permissions: [
Permission.READ_USER,
],
},
};
export const requirePermission = (...permissions: Permission[]) => {
return (req: Request, res: Response, next: NextFunction) => {
const userRole = roles[req.user.role];
const hasPermission = permissions.every(permission =>
userRole.permissions.includes(permission)
);
if (!hasPermission) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
// Usage
router.delete('/users/:id',
authenticate,
requirePermission(Permission.DELETE_USER),
deleteUser
);
6. Token Blacklisting
// Using Redis for token blacklist
import Redis from 'ioredis';
const redis = new Redis();
export const blacklistToken = async (token: string, expiresIn: number) => {
await redis.setex(`blacklist:${token}`, expiresIn, '1');
};
export const isTokenBlacklisted = async (token: string): Promise => {
const result = await redis.get(`blacklist:${token}`);
return result === '1';
};
// Update authenticate middleware
export const authenticate = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const token = req.headers.authorization?.substring(7);
// Check blacklist
if (await isTokenBlacklisted(token!)) {
return res.status(401).json({ error: 'Token has been revoked' });
}
const decoded = verifyAccessToken(token!);
req.user = await User.findById(decoded.userId);
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
// Logout with blacklist
export const logout = async (req: Request, res: Response) => {
const token = req.headers.authorization?.substring(7);
if (token) {
const decoded = verifyAccessToken(token);
const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);
await blacklistToken(token, expiresIn);
}
res.json({ message: 'Logged out successfully' });
};
7. Security Best Practices
✓ JWT Security Checklist:
- Use strong, random secrets (256 bits minimum)
- Store secrets in environment variables
- Keep access tokens short-lived (15 minutes)
- Use refresh tokens for extended sessions
- Store refresh tokens in httpOnly cookies
- Implement token rotation on refresh
- Never store sensitive data in JWT payload
- Validate all claims (exp, iat, iss)
- Use HTTPS in production
- Implement rate limiting on auth endpoints
- Log suspicious authentication attempts
- Implement token blacklisting for logout
- Use strong password hashing (bcrypt/argon2)
- Implement multi-factor authentication
- Monitor for brute force attacks
8. Client-Side Implementation
// React auth context
import { createContext, useState, useEffect } from 'react';
import axios from 'axios';
const AuthContext = createContext(null);
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [accessToken, setAccessToken] = useState(null);
// Axios interceptor for token refresh
useEffect(() => {
const interceptor = axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const { data } = await axios.post('/api/auth/refresh', {}, {
withCredentials: true
});
setAccessToken(data.accessToken);
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
return axios(originalRequest);
} catch (refreshError) {
setUser(null);
setAccessToken(null);
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
return () => axios.interceptors.response.eject(interceptor);
}, []);
const login = async (email, password) => {
const { data } = await axios.post('/api/auth/login', { email, password });
setUser(data.user);
setAccessToken(data.accessToken);
};
const logout = async () => {
await axios.post('/api/auth/logout', {}, {
headers: { Authorization: `Bearer ${accessToken}` }
});
setUser(null);
setAccessToken(null);
};
return (
{children}
);
};
Conclusion
JWT provides a scalable, stateless authentication mechanism ideal for modern APIs. By implementing refresh tokens, proper token rotation, role-based access control, and following security best practices, you can build secure authentication systems that protect your users and scale effectively.
💡 Pro Tip: Always use short-lived access tokens (15 minutes or less) combined with refresh tokens. Implement token rotation where each refresh generates a new refresh token, making stolen tokens useless after one use.