Node.js & Express

Authentication with JWT

19 min Lesson 14 of 40

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:

  1. Create user registration endpoint with password hashing
  2. Implement login endpoint that returns JWT token
  3. Create authentication middleware to protect routes
  4. Add role-based authorization (user, admin)
  5. Implement refresh token mechanism
  6. Create logout endpoint that clears tokens
  7. Add password reset functionality with JWT