JWT Authentication Flow
JWT Authentication Flow
How JWT Works
Client Server
│ │
│ POST /api/auth/login │
│ { email, password } │
│ ─────────────────────────► │
│ │ Verify credentials
│ │ Generate access token (15min)
│ │ Generate refresh token (7 days)
│ { accessToken, refreshToken} │
│ ◄───────────────────────── │
│ │
│ GET /api/data │
│ Authorization: Bearer <jwt> │
│ ─────────────────────────► │
│ │ Verify token
│ { data } │
│ ◄───────────────────────── │
Token Generation
import jwt from 'jsonwebtoken';
import { randomBytes } from 'crypto';
const config = {
accessTokenSecret: process.env.JWT_SECRET,
refreshTokenSecret: process.env.REFRESH_SECRET,
accessTokenExpiry: '15m',
refreshTokenExpiry: '7d',
};
export function generateTokens(user) {
const accessToken = jwt.sign(
{ sub: user.id, email: user.email, role: user.role },
config.accessTokenSecret,
{ expiresIn: config.accessTokenExpiry }
);
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
config.refreshTokenSecret,
{ expiresIn: config.refreshTokenExpiry }
);
return { accessToken, refreshToken };
}
export function verifyAccessToken(token) {
return jwt.verify(token, config.accessTokenSecret);
}
export function verifyRefreshToken(token) {
return jwt.verify(token, config.refreshTokenSecret);
}
Login Controller
import { comparePassword } from '../utils/crypto.js';
import { generateTokens } from '../utils/jwt.js';
import * as usersRepo from '../repositories/users.js';
import { AppError } from '../middleware/errorHandler.js';
export async function login(req, res, next) {
try {
const { email, password } = req.body;
const user = await usersRepo.findByEmail(email);
if (!user) throw new AppError('Invalid credentials', 401);
const valid = await comparePassword(password, user.password);
if (!valid) throw new AppError('Invalid credentials', 401);
const { accessToken, refreshToken } = generateTokens(user);
// Store refresh token hash in database
await usersRepo.storeRefreshToken(user.id, refreshToken);
res.json({ accessToken, refreshToken });
} catch (error) {
next(error);
}
}
Password Security and Hashing
Password Security and Hashing
bcrypt Hashing
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
export async function hashPassword(password) {
return bcrypt.hash(password, SALT_ROUNDS);
}
export async function comparePassword(password, hash) {
return bcrypt.compare(password, hash);
}
Registration with Validation
import { z } from 'zod';
import { hashPassword } from '../utils/crypto.js';
const registerSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
password: z.string().min(8).max(128)
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])/,
'Must include uppercase, lowercase, number, and special character'),
confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
});
export async function register(req, res, next) {
try {
const validated = registerSchema.parse(req.body);
const { confirmPassword, ...userData } = validated;
const existing = await usersRepo.findByEmail(userData.email);
if (existing) throw new AppError('Email already registered', 409);
const hashedPassword = await hashPassword(userData.password);
const user = await usersRepo.create({
...userData,
password: hashedPassword,
role: 'user',
});
const tokens = generateTokens(user);
res.status(201).json({ user: { id: user.id, name: user.name, email: user.email }, ...tokens });
} catch (error) {
next(error);
}
}
Password Reset Flow
import { randomBytes } from 'crypto';
export async function requestPasswordReset(req, res, next) {
try {
const user = await usersRepo.findByEmail(req.body.email);
if (!user) return res.json({ message: 'If email exists, reset link sent' });
const token = randomBytes(32).toString('hex');
const expires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
await usersRepo.storeResetToken(user.id, token, expires);
await emailService.sendPasswordReset(user.email, token);
res.json({ message: 'If email exists, reset link sent' });
} catch (error) {
next(error);
}
}
export async function resetPassword(req, res, next) {
try {
const { token, newPassword } = req.body;
const user = await usersRepo.findByResetToken(token);
if (!user || user.resetExpires < new Date()) {
throw new AppError('Invalid or expired token', 400);
}
const hashed = await hashPassword(newPassword);
await usersRepo.updatePassword(user.id, hashed);
await usersRepo.clearResetToken(user.id);
res.json({ message: 'Password updated' });
} catch (error) {
next(error);
}
}
Authentication Middleware
Authentication Middleware
JWT Verification Middleware
// middleware/auth.js
import { verifyAccessToken } from '../utils/jwt.js';
import { AppError } from './errorHandler.js';
export function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
throw new AppError('No token provided', 401, 'UNAUTHORIZED');
}
const token = authHeader.split(' ')[1];
try {
const payload = verifyAccessToken(token);
req.user = payload; // { sub, email, role, iat, exp }
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new AppError('Token expired', 401, 'TOKEN_EXPIRED');
}
throw new AppError('Invalid token', 401, 'INVALID_TOKEN');
}
}
Role-Based Access Control
export function authorize(...allowedRoles) {
return (req, res, next) => {
if (!req.user) {
throw new AppError('Not authenticated', 401);
}
if (!allowedRoles.includes(req.user.role)) {
throw new AppError('Insufficient permissions', 403, 'FORBIDDEN');
}
next();
};
}
// Usage
router.get('/admin/users', authenticate, authorize('admin'), adminController.listUsers);
router.patch('/posts/:id', authenticate, authorize('admin', 'editor'), postsController.update);
router.delete('/posts/:id', authenticate, postsController.remove); // owner check in controller
Resource Ownership Check
export async function remove(req, res, next) {
try {
const post = await postsService.findById(req.params.id);
if (!post) throw new AppError('Post not found', 404);
// Only author or admin can delete
if (post.author_id !== req.user.sub && req.user.role !== 'admin') {
throw new AppError('Not authorized', 403);
}
await postsService.remove(req.params.id);
res.status(204).end();
} catch (error) {
next(error);
}
}
Token Refresh Endpoint
export async function refreshToken(req, res, next) {
try {
const { refreshToken } = req.body;
if (!refreshToken) throw new AppError('Refresh token required', 400);
const payload = verifyRefreshToken(refreshToken);
const storedToken = await usersRepo.findRefreshToken(payload.sub, refreshToken);
if (!storedToken) throw new AppError('Invalid refresh token', 401));
// Rotate refresh token
await usersRepo.removeRefreshToken(payload.sub, refreshToken);
const user = await usersRepo.findById(payload.sub);
const tokens = generateTokens(user);
await usersRepo.storeRefreshToken(user.id, tokens.refreshToken);
res.json(tokens);
} catch (error) {
next(error);
}
}
Security Best Practices
Security Best Practices
Environment Configuration
// config/index.js
import { z } from 'zod';
const configSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.coerce.number().default(3000),
JWT_SECRET: z.string().min(32),
REFRESH_SECRET: z.string().min(32),
DATABASE_URL: z.string().url(),
CORS_ORIGINS: z.string().transform(s => s.split(',')),
RATE_LIMIT_WINDOW: z.coerce.number().default(900000),
RATE_LIMIT_MAX: z.coerce.number().default(100),
});
export const config = configSchema.parse(process.env);
Security Headers and Hardening
import helmet from 'helmet';
import hpp from 'hpp';
// Security middleware
app.use(helmet());
app.use(hpp()); // HTTP Parameter Pollution protection
// Disable X-Powered-By
app.disable('x-powered-by');
// Trust proxy (if behind load balancer)
app.set('trust proxy', 1);
// Content Security Policy
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
}));
Audit Logging
function auditLog(action) {
return async (req, res, next) => {
const start = Date.now();
await next();
const duration = Date.now() - start;
await db.query(
`INSERT INTO audit_logs (user_id, action, resource, resource_id, ip, duration_ms, status)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[
req.user?.sub || null,
action,
req.baseUrl,
req.params.id || null,
req.ip,
duration,
res.statusCode,
]
);
};
}
router.delete('/posts/:id', authenticate, auditLog('DELETE_POST'), postsController.remove);
Security Checklist
- Use HTTPS in production
- Store secrets in environment variables, never in code
- Hash passwords with bcrypt (12+ rounds)
- Set HttpOnly, Secure, SameSite cookies for refresh tokens
- Implement rate limiting on auth endpoints
- Validate and sanitize all input
- Use parameterized queries (prevent SQL injection)
- Set appropriate CORS origins
- Enable security headers (Helmet)
- Log security-relevant events for auditing