Implementing JWT Authentication in Node.js Applications

Introduction to JWT
JSON Web Tokens (JWT) have become the standard for implementing stateless authentication in modern web applications. This guide will walk through implementing secure JWT authentication in Node.js, including refresh tokens, cookie security, and best practices.
Project Setup
Initialize a new Node.js project and install required dependencies:
npm init -y
npm install express mongoose bcryptjs jsonwebtoken cookie-parser dotenv
npm install --save-dev nodemon
User Model
Create a Mongoose user model with password hashing:
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true }
});
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
module.exports = mongoose.model('User', userSchema);
Authentication Controller
Registration
// controllers/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const signToken = id => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN
});
};
exports.register = async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.create({ email, password });
const token = signToken(user._id);
res.status(201).json({
status: 'success',
token,
data: { user }
});
} catch (err) {
res.status(400).json({
status: 'fail',
message: err.message
});
}
};
Login
exports.login = async (req, res) => {
const { email, password } = req.body;
// 1) Check if email and password exist
if (!email || !password) {
return res.status(400).json({
status: 'fail',
message: 'Please provide email and password'
});
}
// 2) Check if user exists and password is correct
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.correctPassword(password, user.password))) {
return res.status(401).json({
status: 'fail',
message: 'Incorrect email or password'
});
}
// 3) Generate JWT
const token = signToken(user._id);
// 4) Send response
res.status(200).json({
status: 'success',
token
});
};
Route Protection Middleware
exports.protect = async (req, res, next) => {
try {
// 1) Get token from headers/cookies
let token;
if (
req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')
) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return res.status(401).json({
status: 'fail',
message: 'You are not logged in! Please log in to get access'
});
}
// 2) Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// 3) Check if user still exists
const currentUser = await User.findById(decoded.id);
if (!currentUser) {
return res.status(401).json({
status: 'fail',
message: 'The user belonging to this token no longer exists'
});
}
// Grant access to protected route
req.user = currentUser;
next();
} catch (err) {
res.status(401).json({
status: 'fail',
message: 'Invalid or expired token'
});
}
};
Implementing Refresh Tokens
// Generate refresh token
const createRefreshToken = (id) => {
return jwt.sign({ id }, process.env.REFRESH_TOKEN_SECRET, {
expiresIn: '7d'
});
};
// Token refresh endpoint
exports.refreshToken = async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ message: 'Refresh token required' });
}
try {
const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
const accessToken = signToken(decoded.id);
res.json({ accessToken });
} catch (err) {
res.status(403).json({ message: 'Invalid refresh token' });
}
};
Security Best Practices
- Store tokens in HTTPOnly cookies
- Implement CSRF protection
- Use short expiration times for access tokens (15-30 mins)
- Rotate refresh tokens
- Use secure cookies in production
// Secure cookie settings
res.cookie('jwt', token, {
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'Strict'
});
Comments
Leave a Comment