How to Secure Your Node.js API with JWT Authentication
Introduction
Security is paramount in modern web applications, and authentication is the first line of defense. JSON Web Tokens (JWT) have become the gold standard for securing APIs due to their stateless nature, scalability, and ease of implementation.
In this comprehensive guide, we'll build a complete JWT authentication system for a Node.js API that includes:
- User registration and login
- JWT token generation and validation
- Secure middleware implementation
- Token refresh mechanisms
- Password hashing and security best practices
- Error handling and security considerations
By the end of this tutorial, you'll have a production-ready authentication system that secures your Node.js API endpoints effectively.
Understanding JWT Authentication
What is JWT?
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.
A JWT consists of three parts separated by dots:
- Header: Contains the token type and signing algorithm
- Payload: Contains the claims (user data)
- Signature: Used to verify the token's authenticity
// JWT Structure
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c;
Why Use JWT for API Authentication?
Advantages:
- Stateless: No need to store sessions server-side
- Scalable: Works perfectly with microservices
- Cross-domain: Can be used across different domains
- Mobile-friendly: Perfect for mobile applications
- Performance: No database lookups for each request
Use Cases:
- RESTful APIs
- Single Page Applications (SPAs)
- Mobile applications
- Microservices architecture
Project Setup
Let's start by setting up our Node.js project with the necessary dependencies.
Initialize the Project
mkdir jwt-auth-api
cd jwt-auth-api
npm init -y
Install Dependencies
# Production dependencies
npm install express mongoose bcryptjs jsonwebtoken dotenv cors helmet express-rate-limit
npm install express-validator
# Development dependencies
npm install -D nodemon @types/node
Dependencies explained:
express
: Web frameworkmongoose
: MongoDB ODMbcryptjs
: Password hashingjsonwebtoken
: JWT implementationdotenv
: Environment variablescors
: Cross-origin resource sharinghelmet
: Security headersexpress-rate-limit
: Rate limitingexpress-validator
: Input validation
Project Structure
jwt-auth-api/
├── src/
│ ├── controllers/
│ │ ├── authController.js
│ │ └── userController.js
│ ├── middleware/
│ │ ├── auth.js
│ │ ├── rateLimiter.js
│ │ └── validation.js
│ ├── models/
│ │ └── User.js
│ ├── routes/
│ │ ├── auth.js
│ │ └── users.js
│ ├── utils/
│ │ └── tokenUtils.js
│ └── app.js
├── .env
├── .gitignore
├── package.json
└── server.js
Database Setup
User Model
First, let's create our User model with Mongoose:
// src/models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema(
{
username: {
type: String,
required: [true, 'Username is required'],
unique: true,
trim: true,
minlength: [3, 'Username must be at least 3 characters long'],
maxlength: [30, 'Username cannot exceed 30 characters'],
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
trim: true,
lowercase: true,
match: [
/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
'Please enter a valid email',
],
},
password: {
type: String,
required: [true, 'Password is required'],
minlength: [6, 'Password must be at least 6 characters long'],
select: false, // Don't include password in queries by default
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user',
},
isActive: {
type: Boolean,
default: true,
},
refreshTokens: [
{
token: String,
createdAt: {
type: Date,
default: Date.now,
expires: 604800, // 7 days
},
},
],
},
{
timestamps: true,
}
);
// Hash password before saving
userSchema.pre('save', async function (next) {
if (!this.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(12);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
// Compare password method
userSchema.methods.comparePassword = async function (candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
// Remove password from JSON output
userSchema.methods.toJSON = function () {
const user = this.toObject();
delete user.password;
delete user.refreshTokens;
return user;
};
module.exports = mongoose.model('User', userSchema);
Token Utilities
Create utility functions for JWT operations:
// src/utils/tokenUtils.js
const jwt = require('jsonwebtoken');
class TokenUtils {
// Generate access token (short-lived)
static generateAccessToken(payload) {
return jwt.sign(payload, process.env.JWT_ACCESS_SECRET, {
expiresIn: process.env.JWT_ACCESS_EXPIRE || '15m',
issuer: 'your-app-name',
audience: 'your-app-users',
});
}
// Generate refresh token (long-lived)
static generateRefreshToken(payload) {
return jwt.sign(payload, process.env.JWT_REFRESH_SECRET, {
expiresIn: process.env.JWT_REFRESH_EXPIRE || '7d',
issuer: 'your-app-name',
audience: 'your-app-users',
});
}
// Verify access token
static verifyAccessToken(token) {
try {
return jwt.verify(token, process.env.JWT_ACCESS_SECRET);
} catch (error) {
throw new Error('Invalid access token');
}
}
// Verify refresh token
static verifyRefreshToken(token) {
try {
return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
} catch (error) {
throw new Error('Invalid refresh token');
}
}
// Extract token from Authorization header
static extractTokenFromHeader(authHeader) {
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new Error('No token provided');
}
return authHeader.substring(7);
}
// Generate token pair
static generateTokenPair(payload) {
const accessToken = this.generateAccessToken(payload);
const refreshToken = this.generateRefreshToken(payload);
return {
accessToken,
refreshToken,
expiresIn: process.env.JWT_ACCESS_EXPIRE || '15m',
};
}
}
module.exports = TokenUtils;
Authentication Middleware
Create middleware to protect routes:
// src/middleware/auth.js
const TokenUtils = require('../utils/tokenUtils');
const User = require('../models/User');
// Middleware to verify JWT token
const authenticateToken = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({
success: false,
message: 'Access token required',
});
}
const token = TokenUtils.extractTokenFromHeader(authHeader);
const decoded = TokenUtils.verifyAccessToken(token);
// Check if user still exists and is active
const user = await User.findById(decoded.userId);
if (!user || !user.isActive) {
return res.status(401).json({
success: false,
message: 'User not found or inactive',
});
}
// Add user to request object
req.user = {
userId: user._id,
username: user.username,
email: user.email,
role: user.role,
};
next();
} catch (error) {
console.error('Authentication error:', error.message);
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Access token expired',
code: 'TOKEN_EXPIRED',
});
}
return res.status(403).json({
success: false,
message: 'Invalid access token',
});
}
};
// Middleware to check user roles
const authorizeRoles = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Authentication required',
});
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: 'Insufficient permissions',
});
}
next();
};
};
// Optional authentication (doesn't fail if no token)
const optionalAuth = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (authHeader) {
const token = TokenUtils.extractTokenFromHeader(authHeader);
const decoded = TokenUtils.verifyAccessToken(token);
const user = await User.findById(decoded.userId);
if (user && user.isActive) {
req.user = {
userId: user._id,
username: user.username,
email: user.email,
role: user.role,
};
}
}
next();
} catch (error) {
// Continue without authentication
next();
}
};
module.exports = {
authenticateToken,
authorizeRoles,
optionalAuth,
};
Input Validation Middleware
// src/middleware/validation.js
const { body, validationResult } = require('express-validator');
// Validation rules for registration
const validateRegistration = [
body('username')
.trim()
.isLength({ min: 3, max: 30 })
.withMessage('Username must be between 3 and 30 characters')
.matches(/^[a-zA-Z0-9_]+$/)
.withMessage('Username can only contain letters, numbers, and underscores'),
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Please provide a valid email address'),
body('password')
.isLength({ min: 6 })
.withMessage('Password must be at least 6 characters long')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.withMessage(
'Password must contain at least one uppercase letter, one lowercase letter, and one number'
),
];
// Validation rules for login
const validateLogin = [
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Please provide a valid email address'),
body('password').notEmpty().withMessage('Password is required'),
];
// Validation rules for password change
const validatePasswordChange = [
body('currentPassword')
.notEmpty()
.withMessage('Current password is required'),
body('newPassword')
.isLength({ min: 6 })
.withMessage('New password must be at least 6 characters long')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.withMessage(
'New password must contain at least one uppercase letter, one lowercase letter, and one number'
),
];
// Handle validation errors
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
const extractedErrors = errors.array().map((error) => ({
field: error.param,
message: error.msg,
}));
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: extractedErrors,
});
}
next();
};
module.exports = {
validateRegistration,
validateLogin,
validatePasswordChange,
handleValidationErrors,
};
Rate Limiting Middleware
// src/middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
// General API rate limiter
const createRateLimiter = (windowMs, max, message) => {
return rateLimit({
windowMs,
max,
message: {
success: false,
message,
retryAfter: Math.ceil(windowMs / 1000),
},
standardHeaders: true,
legacyHeaders: false,
// Custom key generator (can be used for user-specific limits)
keyGenerator: (req) => {
return req.user?.userId || req.ip;
},
});
};
// Strict rate limiter for auth endpoints
const authLimiter = createRateLimiter(
15 * 60 * 1000, // 15 minutes
5, // 5 attempts
'Too many authentication attempts, please try again later'
);
// General API rate limiter
const apiLimiter = createRateLimiter(
15 * 60 * 1000, // 15 minutes
100, // 100 requests
'Too many API requests, please try again later'
);
// Password reset limiter
const passwordResetLimiter = createRateLimiter(
60 * 60 * 1000, // 1 hour
3, // 3 attempts
'Too many password reset attempts, please try again later'
);
module.exports = {
authLimiter,
apiLimiter,
passwordResetLimiter,
};
Authentication Controller
Now let's implement the authentication logic:
// src/controllers/authController.js
const User = require('../models/User');
const TokenUtils = require('../utils/tokenUtils');
class AuthController {
// User registration
static async register(req, res) {
try {
const { username, email, password } = req.body;
// Check if user already exists
const existingUser = await User.findOne({
$or: [{ email }, { username }],
});
if (existingUser) {
return res.status(400).json({
success: false,
message:
existingUser.email === email
? 'Email already registered'
: 'Username already taken',
});
}
// Create new user
const user = new User({
username,
email,
password,
});
await user.save();
// Generate tokens
const tokenPayload = {
userId: user._id,
username: user.username,
email: user.email,
role: user.role,
};
const tokens = TokenUtils.generateTokenPair(tokenPayload);
// Store refresh token
user.refreshTokens.push({
token: tokens.refreshToken,
});
await user.save();
res.status(201).json({
success: true,
message: 'User registered successfully',
data: {
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role,
},
tokens,
},
});
} catch (error) {
console.error('Registration error:', error);
if (error.code === 11000) {
return res.status(400).json({
success: false,
message: 'Email or username already exists',
});
}
res.status(500).json({
success: false,
message: 'Registration failed',
error:
process.env.NODE_ENV === 'development' ? error.message : undefined,
});
}
}
// User login
static async login(req, res) {
try {
const { email, password } = req.body;
// Find user and include password field
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid credentials',
});
}
if (!user.isActive) {
return res.status(401).json({
success: false,
message: 'Account is deactivated',
});
}
// Check password
const isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: 'Invalid credentials',
});
}
// Generate tokens
const tokenPayload = {
userId: user._id,
username: user.username,
email: user.email,
role: user.role,
};
const tokens = TokenUtils.generateTokenPair(tokenPayload);
// Store refresh token (limit to 5 active tokens)
user.refreshTokens.push({
token: tokens.refreshToken,
});
// Keep only the 5 most recent refresh tokens
if (user.refreshTokens.length > 5) {
user.refreshTokens = user.refreshTokens.slice(-5);
}
await user.save();
res.json({
success: true,
message: 'Login successful',
data: {
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role,
},
tokens,
},
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
message: 'Login failed',
error:
process.env.NODE_ENV === 'development' ? error.message : undefined,
});
}
}
// Refresh access token
static async refreshToken(req, res) {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({
success: false,
message: 'Refresh token required',
});
}
// Verify refresh token
const decoded = TokenUtils.verifyRefreshToken(refreshToken);
// Find user and check if refresh token exists
const user = await User.findById(decoded.userId);
if (!user || !user.isActive) {
return res.status(401).json({
success: false,
message: 'Invalid refresh token',
});
}
// Check if refresh token exists in user's tokens
const tokenExists = user.refreshTokens.some(
(tokenObj) => tokenObj.token === refreshToken
);
if (!tokenExists) {
return res.status(401).json({
success: false,
message: 'Invalid refresh token',
});
}
// Generate new access token
const tokenPayload = {
userId: user._id,
username: user.username,
email: user.email,
role: user.role,
};
const accessToken = TokenUtils.generateAccessToken(tokenPayload);
res.json({
success: true,
message: 'Token refreshed successfully',
data: {
accessToken,
expiresIn: process.env.JWT_ACCESS_EXPIRE || '15m',
},
});
} catch (error) {
console.error('Token refresh error:', error);
res.status(401).json({
success: false,
message: 'Invalid refresh token',
});
}
}
// Logout (invalidate refresh token)
static async logout(req, res) {
try {
const { refreshToken } = req.body;
const userId = req.user?.userId;
if (userId && refreshToken) {
// Remove the specific refresh token
await User.updateOne(
{ _id: userId },
{ $pull: { refreshTokens: { token: refreshToken } } }
);
}
res.json({
success: true,
message: 'Logged out successfully',
});
} catch (error) {
console.error('Logout error:', error);
res.status(500).json({
success: false,
message: 'Logout failed',
});
}
}
// Logout from all devices
static async logoutAll(req, res) {
try {
const userId = req.user.userId;
// Remove all refresh tokens
await User.updateOne({ _id: userId }, { $set: { refreshTokens: [] } });
res.json({
success: true,
message: 'Logged out from all devices successfully',
});
} catch (error) {
console.error('Logout all error:', error);
res.status(500).json({
success: false,
message: 'Logout failed',
});
}
}
// Get current user profile
static async getProfile(req, res) {
try {
const user = await User.findById(req.user.userId);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found',
});
}
res.json({
success: true,
data: {
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
},
},
});
} catch (error) {
console.error('Get profile error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch profile',
});
}
}
// Change password
static async changePassword(req, res) {
try {
const { currentPassword, newPassword } = req.body;
const userId = req.user.userId;
// Find user with password
const user = await User.findById(userId).select('+password');
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found',
});
}
// Verify current password
const isCurrentPasswordValid = await user.comparePassword(
currentPassword
);
if (!isCurrentPasswordValid) {
return res.status(400).json({
success: false,
message: 'Current password is incorrect',
});
}
// Update password
user.password = newPassword;
await user.save();
// Invalidate all refresh tokens (force re-login)
user.refreshTokens = [];
await user.save();
res.json({
success: true,
message: 'Password changed successfully. Please login again.',
});
} catch (error) {
console.error('Change password error:', error);
res.status(500).json({
success: false,
message: 'Failed to change password',
});
}
}
}
module.exports = AuthController;
Routes Setup
Create the authentication routes:
// src/routes/auth.js
const express = require('express');
const AuthController = require('../controllers/authController');
const { authenticateToken } = require('../middleware/auth');
const { authLimiter } = require('../middleware/rateLimiter');
const {
validateRegistration,
validateLogin,
validatePasswordChange,
handleValidationErrors,
} = require('../middleware/validation');
const router = express.Router();
// Public routes (with rate limiting)
router.post(
'/register',
authLimiter,
validateRegistration,
handleValidationErrors,
AuthController.register
);
router.post(
'/login',
authLimiter,
validateLogin,
handleValidationErrors,
AuthController.login
);
router.post('/refresh-token', authLimiter, AuthController.refreshToken);
// Protected routes
router.use(authenticateToken);
router.get('/profile', AuthController.getProfile);
router.post('/logout', AuthController.logout);
router.post('/logout-all', AuthController.logoutAll);
router.post(
'/change-password',
validatePasswordChange,
handleValidationErrors,
AuthController.changePassword
);
module.exports = router;
Create protected user routes:
// src/routes/users.js
const express = require('express');
const User = require('../models/User');
const { authenticateToken, authorizeRoles } = require('../middleware/auth');
const router = express.Router();
// All routes require authentication
router.use(authenticateToken);
// Get all users (admin only)
router.get('/', authorizeRoles('admin'), async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const users = await User.find({ isActive: true })
.select('-refreshTokens')
.skip(skip)
.limit(limit)
.sort({ createdAt: -1 });
const total = await User.countDocuments({ isActive: true });
res.json({
success: true,
data: {
users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
},
});
} catch (error) {
console.error('Get users error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch users',
});
}
});
// Get user by ID (admin only)
router.get('/:id', authorizeRoles('admin'), async (req, res) => {
try {
const user = await User.findById(req.params.id).select('-refreshTokens');
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found',
});
}
res.json({
success: true,
data: { user },
});
} catch (error) {
console.error('Get user error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch user',
});
}
});
// Deactivate user (admin only)
router.patch('/:id/deactivate', authorizeRoles('admin'), async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found',
});
}
user.isActive = false;
user.refreshTokens = []; // Invalidate all tokens
await user.save();
res.json({
success: true,
message: 'User deactivated successfully',
});
} catch (error) {
console.error('Deactivate user error:', error);
res.status(500).json({
success: false,
message: 'Failed to deactivate user',
});
}
});
module.exports = router;
Application Setup
Create the main application file:
// src/app.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
require('dotenv').config();
// Import routes
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');
// Import middleware
const { apiLimiter } = require('./middleware/rateLimiter');
const app = express();
// Security middleware
app.use(helmet());
app.use(
cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || [
'http://localhost:3000',
],
credentials: true,
})
);
// Rate limiting
app.use(apiLimiter);
// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Database connection
mongoose
.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/jwt-auth', {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('Connected to MongoDB'))
.catch((err) => console.error('MongoDB connection error:', err));
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
// Health check endpoint
app.get('/api/health', (req, res) => {
res.json({
success: true,
message: 'API is running',
timestamp: new Date().toISOString(),
});
});
// 404 handler
app.use('*', (req, res) => {
res.status(404).json({
success: false,
message: 'Route not found',
});
});
// Global error handler
app.use((error, req, res, next) => {
console.error('Unhandled error:', error);
res.status(error.status || 500).json({
success: false,
message: error.message || 'Internal server error',
...(process.env.NODE_ENV === 'development' && { stack: error.stack }),
});
});
module.exports = app;
Server entry point:
// server.js
const app = require('./src/app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
});
Environment Configuration
Create your environment configuration:
# .env
# Server Configuration
PORT=3000
NODE_ENV=development
# Database
MONGODB_URI=mongodb://localhost:27017/jwt-auth
# JWT Secrets (use strong, random strings in production)
JWT_ACCESS_SECRET=your-super-secret-access-key-change-this-in-production
JWT_REFRESH_SECRET=your-super-secret-refresh-key-change-this-in-production
# JWT Expiration
JWT_ACCESS_EXPIRE=15m
JWT_REFRESH_EXPIRE=7d
# CORS
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001
# Rate Limiting (requests per window)
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
Testing the API
Package.json Scripts
Add these scripts to your package.json
:
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
}
}
Testing with cURL
Start your server:
npm run dev
Register a new user:
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"username": "johndoe",
"email": "john@example.com",
"password": "Password123"
}'
Login:
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"password": "Password123"
}'
Access protected route:
curl -X GET http://localhost:3000/api/auth/profile \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Refresh token:
curl -X POST http://localhost:3000/api/auth/refresh-token \
-H "Content-Type: application/json" \
-d '{
"refreshToken": "YOUR_REFRESH_TOKEN"
}'
Frontend Integration
React Example
Here's how to integrate with a React frontend:
// authService.js
class AuthService {
constructor() {
this.baseURL = 'http://localhost:3000/api';
this.accessToken = localStorage.getItem('accessToken');
this.refreshToken = localStorage.getItem('refreshToken');
}
// Set authentication headers
getAuthHeaders() {
return {
'Content-Type': 'application/json',
...(this.accessToken && { Authorization: `Bearer ${this.accessToken}` }),
};
}
// Register user
async register(userData) {
const response = await fetch(`${this.baseURL}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
});
const data = await response.json();
if (data.success) {
this.setTokens(data.data.tokens);
}
return data;
}
// Login user
async login(credentials) {
const response = await fetch(`${this.baseURL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
});
const data = await response.json();
if (data.success) {
this.setTokens(data.data.tokens);
}
return data;
}
// Refresh access token
async refreshAccessToken() {
if (!this.refreshToken) {
throw new Error('No refresh token available');
}
const response = await fetch(`${this.baseURL}/auth/refresh-token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: this.refreshToken }),
});
const data = await response.json();
if (data.success) {
this.accessToken = data.data.accessToken;
localStorage.setItem('accessToken', this.accessToken);
} else {
this.logout();
throw new Error('Token refresh failed');
}
return data;
}
// Make authenticated API call
async authenticatedFetch(url, options = {}) {
let response = await fetch(`${this.baseURL}${url}`, {
...options,
headers: {
...this.getAuthHeaders(),
...options.headers,
},
});
// If access token expired, try to refresh
if (response.status === 401) {
const errorData = await response.json();
if (errorData.code === 'TOKEN_EXPIRED') {
try {
await this.refreshAccessToken();
// Retry the original request
response = await fetch(`${this.baseURL}${url}`, {
...options,
headers: {
...this.getAuthHeaders(),
...options.headers,
},
});
} catch (error) {
this.logout();
throw new Error('Authentication failed');
}
}
}
return response;
}
// Logout
async logout() {
if (this.refreshToken) {
try {
await fetch(`${this.baseURL}/auth/logout`, {
method: 'POST',
headers: this.getAuthHeaders(),
body: JSON.stringify({ refreshToken: this.refreshToken }),
});
} catch (error) {
console.error('Logout request failed:', error);
}
}
this.clearTokens();
}
// Set tokens
setTokens(tokens) {
this.accessToken = tokens.accessToken;
this.refreshToken = tokens.refreshToken;
localStorage.setItem('accessToken', tokens.accessToken);
localStorage.setItem('refreshToken', tokens.refreshToken);
}
// Clear tokens
clearTokens() {
this.accessToken = null;
this.refreshToken = null;
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
// Check if user is authenticated
isAuthenticated() {
return !!this.accessToken;
}
}
export default new AuthService();
React Hook Example
// useAuth.js
import { useState, useEffect, useContext, createContext } from 'react';
import authService from './authService';
const AuthContext = createContext();
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const initAuth = async () => {
if (authService.isAuthenticated()) {
try {
const response = await authService.authenticatedFetch(
'/auth/profile'
);
const data = await response.json();
if (data.success) {
setUser(data.data.user);
} else {
authService.logout();
}
} catch (error) {
console.error('Auth initialization failed:', error);
authService.logout();
}
}
setLoading(false);
};
initAuth();
}, []);
const login = async (credentials) => {
const result = await authService.login(credentials);
if (result.success) {
setUser(result.data.user);
}
return result;
};
const register = async (userData) => {
const result = await authService.register(userData);
if (result.success) {
setUser(result.data.user);
}
return result;
};
const logout = async () => {
await authService.logout();
setUser(null);
};
const value = {
user,
login,
register,
logout,
loading,
isAuthenticated: !!user,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
Security Best Practices
1. Token Security
Use Strong Secrets:
# Generate strong secrets
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
Token Expiration:
- Access tokens: Short-lived (15-30 minutes)
- Refresh tokens: Longer-lived (7 days to 30 days)
2. Password Security
Password Requirements:
- Minimum 8 characters
- Mix of uppercase, lowercase, numbers, and symbols
- Not commonly used passwords
Hashing:
// Use high salt rounds (12+ for production)
const salt = await bcrypt.genSalt(12);
const hashedPassword = await bcrypt.hash(password, salt);
3. Rate Limiting
Implementation Levels:
- Global API rate limiting
- Endpoint-specific limits
- User-specific limits
- IP-based limits
4. Input Validation
Always Validate:
- Email format
- Username format
- Password strength
- Request body size
- File uploads (if applicable)
5. HTTPS Only
Production Setup:
// Force HTTPS in production
if (process.env.NODE_ENV === 'production') {
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
return res.redirect(`https://${req.header('host')}${req.url}`);
}
next();
});
}
6. Security Headers
// Enhanced security headers
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
})
);
7. Token Blacklisting
For enhanced security, implement token blacklisting:
// Token blacklist (use Redis in production)
const tokenBlacklist = new Set();
// Middleware to check blacklisted tokens
const checkBlacklist = (req, res, next) => {
const token = TokenUtils.extractTokenFromHeader(req.headers.authorization);
if (tokenBlacklist.has(token)) {
return res.status(401).json({
success: false,
message: 'Token has been revoked',
});
}
next();
};
// Add token to blacklist on logout
const addToBlacklist = (token) => {
tokenBlacklist.add(token);
// Clean up expired tokens periodically
setTimeout(() => {
tokenBlacklist.delete(token);
}, 15 * 60 * 1000); // 15 minutes
};
Production Deployment
Environment Variables
# Production .env
NODE_ENV=production
PORT=3000
# Database
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/proddb
# JWT Secrets (use crypto.randomBytes(64).toString('hex'))
JWT_ACCESS_SECRET=your-production-access-secret
JWT_REFRESH_SECRET=your-production-refresh-secret
# Shorter expiration for production
JWT_ACCESS_EXPIRE=15m
JWT_REFRESH_EXPIRE=7d
# Production domains
ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
PM2 Configuration
// ecosystem.config.js
module.exports = {
apps: [
{
name: 'jwt-auth-api',
script: 'server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
time: true,
},
],
};
Docker Configuration
# Dockerfile
FROM node:16-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodeuser -u 1001
# Change ownership
RUN chown -R nodeuser:nodejs /app
USER nodeuser
EXPOSE 3000
CMD ["node", "server.js"]
Conclusion
You now have a comprehensive JWT authentication system for your Node.js API that includes:
✅ Secure user registration and login
✅ JWT token generation and validation
✅ Refresh token mechanism
✅ Password hashing with bcrypt
✅ Rate limiting and security middleware
✅ Input validation and sanitization
✅ Role-based authorization
✅ Frontend integration examples
✅ Production-ready security practices
Key Takeaways
- Security First: Always prioritize security in authentication systems
- Token Strategy: Use short-lived access tokens with refresh tokens
- Validation: Validate all inputs and implement proper error handling
- Rate Limiting: Protect against brute force attacks
- Monitoring: Log security events and monitor for suspicious activity
Next Steps
- Implement password reset functionality
- Add two-factor authentication (2FA)
- Implement OAuth integration
- Add comprehensive logging and monitoring
- Set up automated security testing
- Consider implementing WebAuthn for passwordless authentication
This authentication system provides a solid foundation for securing your Node.js APIs while maintaining scalability and user experience. Remember to keep your dependencies updated and regularly review security practices as your application grows.