Build secure authentication in your Node.js API using JSON Web Tokens (JWT) with refresh token rotation.
Things You'll Need
- Node.js project with Express
- MongoDB or another database
- Basic understanding of authentication concepts
- Postman or similar API testing tool
Requirements
- Express server set up
- Database connection configured
- User model created
Steps
Follow these 9 steps to complete this guide
Install Required Packages
Install jsonwebtoken for creating and verifying JWTs, and bcrypt for password hashing.
npm install jsonwebtoken bcryptjs
npm install -D @types/jsonwebtoken @types/bcryptjs Set Up Environment Variables
Create a .env file with your JWT secrets. Never commit these to version control!
# .env
JWT_SECRET=your_super_secret_key_min_32_characters_long
JWT_REFRESH_SECRET=your_refresh_secret_key_min_32_characters
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d Warnings
- Use strong, random secrets in production
- Never commit .env files to Git
- Rotate secrets regularly in production
Create Token Utility Functions
Build helper functions for generating and verifying JWT tokens.
// utils/jwt.ts
import jwt from 'jsonwebtoken';
interface TokenPayload {
userId: string;
email: string;
}
export const generateAccessToken = (payload: TokenPayload): string => {
return jwt.sign(payload, process.env.JWT_SECRET!, {
expiresIn: process.env.JWT_EXPIRES_IN,
});
};
export const generateRefreshToken = (payload: TokenPayload): string => {
return jwt.sign(payload, process.env.JWT_REFRESH_SECRET!, {
expiresIn: process.env.JWT_REFRESH_EXPIRES_IN,
});
};
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;
}; Create Authentication Middleware
Build middleware to protect routes and verify tokens.
// middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import { verifyAccessToken } from '../utils/jwt';
export interface AuthRequest extends Request {
user?: {
userId: string;
email: string;
};
}
export const authenticateToken = (
req: AuthRequest,
res: Response,
next: NextFunction
) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ message: 'Access token required' });
}
try {
const payload = verifyAccessToken(token);
req.user = payload;
next();
} catch (error) {
return res.status(403).json({ message: 'Invalid or expired token' });
}
}; Tips
- Return 401 for missing tokens (Unauthorized)
- Return 403 for invalid tokens (Forbidden)
Implement Registration Endpoint
Create an endpoint for user registration with password hashing.
// routes/auth.ts
import { Router } from 'express';
import bcrypt from 'bcryptjs';
import { User } from '../models/User';
import { generateAccessToken, generateRefreshToken } from '../utils/jwt';
const router = Router();
router.post('/register', async (req, res) => {
try {
const { email, password, name } = req.body;
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create user
const user = await User.create({
email,
password: hashedPassword,
name,
});
// Generate tokens
const accessToken = generateAccessToken({
userId: user._id.toString(),
email: user.email,
});
const refreshToken = generateRefreshToken({
userId: user._id.toString(),
email: user.email,
});
res.status(201).json({
message: 'User created successfully',
accessToken,
refreshToken,
});
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});
export default router; Warnings
- Always validate input data
- Use bcrypt salt rounds of at least 10
Implement Login Endpoint
Create a login endpoint that verifies credentials and issues tokens.
// routes/auth.ts (continued)
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Verify password
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Generate tokens
const accessToken = generateAccessToken({
userId: user._id.toString(),
email: user.email,
});
const refreshToken = generateRefreshToken({
userId: user._id.toString(),
email: user.email,
});
res.json({
message: 'Login successful',
accessToken,
refreshToken,
});
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
}); Tips
- Use the same error message for user not found and wrong password to prevent user enumeration
Implement Token Refresh Endpoint
Allow users to get new access tokens using their refresh token.
// routes/auth.ts (continued)
router.post('/refresh', async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ message: 'Refresh token required' });
}
// Verify refresh token
const payload = verifyRefreshToken(refreshToken);
// Generate new access token
const newAccessToken = generateAccessToken({
userId: payload.userId,
email: payload.email,
});
res.json({
accessToken: newAccessToken,
});
} catch (error) {
res.status(403).json({ message: 'Invalid refresh token' });
}
}); Protect Routes with Middleware
Use the authentication middleware to protect your API routes.
// routes/protected.ts
import { Router } from 'express';
import { authenticateToken, AuthRequest } from '../middleware/auth';
const router = Router();
router.get('/profile', authenticateToken, async (req: AuthRequest, res) => {
try {
const user = await User.findById(req.user!.userId).select('-password');
res.json({ user });
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});
export default router; Test Your Authentication
Use Postman or curl to test registration, login, and protected routes.
# Register
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password123","name":"Test User"}'
# Login
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password123"}'
# Access protected route
curl http://localhost:3000/profile \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" Tips
- Save the tokens returned from login for testing
- Test with expired tokens to verify error handling
- Test the refresh token flow
Was this guide helpful?
Check out more step-by-step guides for development, deployment, debugging, and configuration.