Node.js & Express
Authentication with JWT
Authentication with JWT
JSON Web Tokens (JWT) provide a secure, stateless authentication mechanism for modern web applications. In this lesson, we'll implement JWT-based authentication in Express.js applications.
Authentication vs Authorization
Understanding the difference between these concepts is crucial:
- Authentication: Verifying who the user is (login with credentials)
- Authorization: Determining what the user can access (permissions and roles)
Remember: Authentication answers "Who are you?" while Authorization answers "What can you do?"
JWT Structure
A JWT consists of three parts separated by dots (.):
// JWT Format: header.payload.signature
// Example JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
// 1. Header (Algorithm & Token Type)
{
"alg": "HS256",
"typ": "JWT"
}
// 2. Payload (Claims/Data)
{
"userId": "1234567890",
"name": "John Doe",
"iat": 1516239022, // Issued at
"exp": 1516242622 // Expiration
}
// 3. Signature (Verification)
// HMACSHA256(
// base64UrlEncode(header) + "." + base64UrlEncode(payload),
// secret
// )
Security Note: JWT payloads are Base64-encoded, NOT encrypted. Never store sensitive information like passwords in JWT tokens.
Installing jsonwebtoken Library
Install the required package:
npm install jsonwebtoken bcryptjs
Creating JWT Tokens
Generate tokens after successful login:
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const User = require('../models/User');
// Environment variable for secret key
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
const JWT_EXPIRE = process.env.JWT_EXPIRE || '7d';
// Register new user
exports.register = async (req, res) => {
try {
const { name, email, password } = req.body;
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({
success: false,
message: 'User already exists'
});
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create user
const user = await User.create({
name,
email,
password: hashedPassword
});
// Generate JWT token
const token = jwt.sign(
{ userId: user._id, email: user.email },
JWT_SECRET,
{ expiresIn: JWT_EXPIRE }
);
res.status(201).json({
success: true,
token,
user: {
id: user._id,
name: user.name,
email: user.email
}
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
// Login user
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
// Validate input
if (!email || !password) {
return res.status(400).json({
success: false,
message: 'Please provide email and password'
});
}
// Find user (include password field)
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Check password
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Generate token
const token = jwt.sign(
{
userId: user._id,
email: user.email,
role: user.role
},
JWT_SECRET,
{ expiresIn: JWT_EXPIRE }
);
res.json({
success: true,
token,
user: {
id: user._id,
name: user.name,
email: user.email,
role: user.role
}
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
Verifying JWT Tokens
Create middleware to verify tokens on protected routes:
// middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
// Protect routes - verify JWT
exports.protect = async (req, res, next) => {
try {
let token;
// Check if token exists in headers
if (
req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')
) {
// Extract token from "Bearer TOKEN"
token = req.headers.authorization.split(' ')[1];
}
// Check if token exists
if (!token) {
return res.status(401).json({
success: false,
message: 'Not authorized to access this route'
});
}
try {
// Verify token
const decoded = jwt.verify(token, JWT_SECRET);
// Add user to request object
req.user = await User.findById(decoded.userId).select('-password');
if (!req.user) {
return res.status(401).json({
success: false,
message: 'User not found'
});
}
next();
} catch (error) {
return res.status(401).json({
success: false,
message: 'Not authorized, token failed'
});
}
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
// Authorize roles
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: `User role ${req.user.role} is not authorized to access this route`
});
}
next();
};
};
Best Practice: Always store JWT_SECRET in environment variables and use a strong, random string. Never commit secrets to version control.
Protecting Routes
Apply authentication middleware to protect routes:
// routes/users.js
const express = require('express');
const router = express.Router();
const { protect, authorize } = require('../middleware/auth');
const userController = require('../controllers/userController');
// Public routes
router.post('/register', userController.register);
router.post('/login', userController.login);
// Protected routes (require authentication)
router.get('/profile', protect, userController.getProfile);
router.put('/profile', protect, userController.updateProfile);
// Admin only routes (require authentication + admin role)
router.get('/admin/users',
protect,
authorize('admin'),
userController.getAllUsers
);
router.delete('/admin/users/:id',
protect,
authorize('admin'),
userController.deleteUser
);
// Multiple roles allowed
router.get('/dashboard',
protect,
authorize('admin', 'moderator'),
userController.getDashboard
);
module.exports = router;
Refresh Tokens
Implement refresh tokens for better security:
// Generate both access and refresh tokens
const generateTokens = (userId) => {
// Short-lived access token (15 minutes)
const accessToken = jwt.sign(
{ userId },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
// Long-lived refresh token (7 days)
const refreshToken = jwt.sign(
{ userId },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
};
// Login with refresh token
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email }).select('+password');
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
const { accessToken, refreshToken } = generateTokens(user._id);
// Store refresh token in database
user.refreshToken = refreshToken;
await user.save();
// Send refresh token as httpOnly cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.json({
success: true,
accessToken,
user: {
id: user._id,
name: user.name,
email: user.email
}
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
// Refresh access token
exports.refreshToken = async (req, res) => {
try {
const { refreshToken } = req.cookies;
if (!refreshToken) {
return res.status(401).json({
success: false,
message: 'Refresh token not found'
});
}
// Verify refresh token
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
// Find user and verify refresh token matches
const user = await User.findById(decoded.userId);
if (!user || user.refreshToken !== refreshToken) {
return res.status(401).json({
success: false,
message: 'Invalid refresh token'
});
}
// Generate new access token
const accessToken = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({
success: true,
accessToken
});
} catch (error) {
res.status(401).json({
success: false,
message: 'Invalid or expired refresh token'
});
}
};
// Logout
exports.logout = async (req, res) => {
try {
// Clear refresh token from database
await User.findByIdAndUpdate(req.user._id, {
refreshToken: null
});
// Clear cookie
res.clearCookie('refreshToken');
res.json({
success: true,
message: 'Logged out successfully'
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
Refresh Token Strategy:
- Access Token: Short-lived (15 minutes), sent in Authorization header
- Refresh Token: Long-lived (7 days), stored in httpOnly cookie
- When access token expires, use refresh token to get a new access token
- Store refresh tokens in database to enable revocation
Complete Authentication Flow
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required']
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true
},
password: {
type: String,
required: [true, 'Password is required'],
minlength: 8,
select: false // Don't return password by default
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
refreshToken: {
type: String,
select: false
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('User', userSchema);
// app.js setup
const express = require('express');
const cookieParser = require('cookie-parser');
const authRoutes = require('./routes/auth');
const app = express();
// Middleware
app.use(express.json());
app.use(cookieParser());
// Routes
app.use('/api/auth', authRoutes);
// Error handler
app.use(errorHandler);
module.exports = app;
Security Considerations:
- Use HTTPS in production to prevent token interception
- Set httpOnly flag on cookies to prevent XSS attacks
- Implement rate limiting on login endpoints
- Use strong, random secrets for signing tokens
- Validate and sanitize all user inputs
- Implement token blacklisting for logout
Practice Exercise
Implement a complete JWT authentication system:
- Create user registration endpoint with password hashing
- Implement login endpoint that returns JWT token
- Create authentication middleware to protect routes
- Add role-based authorization (user, admin)
- Implement refresh token mechanism
- Create logout endpoint that clears tokens
- Add password reset functionality with JWT