JWT Authentication in Express.js
Access/refresh token pattern with middleware guards and secure cookie storage
What You’ll Build
After following this guide, you will have a working implementation of jwt authentication in express.js in your project. Build a robust JWT authentication system for Express.js APIs. Uses short-lived access tokens (15 min) and long-lived refresh tokens (7 days) stored in HTTP-only cookies. Includes middleware for protecting routes and automatic token refresh.
Use Cases & Problems Solved
- Protect routes so only authenticated users can access sensitive pages
- Allow users to sign up, log in, and manage their accounts securely
- Avoid storing raw passwords or building session management from scratch
Prerequisites
- Node.js 18+
- Express.js project
- A user database (any)
Step-by-Step Implementation
Install dependencies
The following snippet shows how to install dependencies. Copy this into your project and adjust the values for your environment.
npm install jsonwebtoken bcryptjs cookie-parser
Token generation utilities
The following snippet shows how to token generation utilities. Copy this into your project and adjust the values for your environment.
// utils/tokens.js
const jwt = require('jsonwebtoken');
function generateAccessToken(user) {
return jwt.sign(
{ id: user.id, email: user.email },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
}
function generateRefreshToken(user) {
return jwt.sign(
{ id: user.id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
}
module.exports = { generateAccessToken, generateRefreshToken };
Auth middleware
The following snippet shows how to auth middleware. Copy this into your project and adjust the values for your environment.
// middleware/auth.js
const jwt = require('jsonwebtoken');
function authenticate(req, res, next) {
const token = req.cookies.accessToken ||
req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token provided' });
try {
req.user = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET);
next();
} catch (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
}
module.exports = { authenticate };
Login and refresh endpoints
The following snippet shows how to login and refresh endpoints. Copy this into your project and adjust the values for your environment.
// routes/auth.js
const bcrypt = require('bcryptjs');
const { generateAccessToken, generateRefreshToken } = require('../utils/tokens');
router.post('/login', async (req, res) => {
const user = await User.findByEmail(req.body.email);
if (!user || !await bcrypt.compare(req.body.password, user.passwordHash)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
res.cookie('refreshToken', refreshToken, {
httpOnly: true, secure: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({ accessToken });
});
⚠️ Don’t Do This
❌ Storing JWTs in localStorage (XSS vulnerable)
// XSS attack can steal this!
localStorage.setItem('token', response.accessToken);
// Anyone with XSS can read it
fetch('/api/data', {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
✅ Store tokens in HTTP-only cookies (immune to XSS)
// Server sets HTTP-only cookie — JS cannot access it
res.cookie('accessToken', token, {
httpOnly: true, // Cannot be read by JavaScript
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 15 * 60 * 1000,
});
Testing
Add these tests to verify your jwt authentication in express.js implementation works correctly:
// __tests__/express.test.ts
import { describe, it, expect } from 'vitest';
describe('JWT Authentication in Express.js', () => {
it('should handle successful authentication', async () => {
// Test the happy path
const result = await authenticate({ email: 'test@example.com', password: 'valid' });
expect(result.user).toBeDefined();
expect(result.error).toBeNull();
});
it('should reject invalid credentials', async () => {
const result = await authenticate({ email: 'test@example.com', password: 'wrong' });
expect(result.user).toBeNull();
expect(result.error).toBeDefined();
});
it('should handle missing fields', async () => {
const result = await authenticate({ email: '', password: '' });
expect(result.error).toBeDefined();
});
});
Verification
# Test login
curl -X POST http://localhost:3000/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"test@test.com","password":"pass123"}' -c cookies.txt
# Test protected route
curl http://localhost:3000/api/profile -b cookies.txt Related Specs
Secure Session Management with Redis
Express sessions with Redis store, expiration, and fingerprinting
Role-Based Access Control (RBAC)
Role/permission system with middleware, database schema, and route guards