Express Protected Routes
Introduction
Protected routes are a fundamental concept in web application security. They are specific paths in your application that require authentication before a user can access them. For instance, user profiles, admin dashboards, or personal data should only be accessible to authenticated users with the appropriate permissions.
In this tutorial, we'll learn how to implement protected routes in an Express.js application using authentication middleware. We'll cover both basic authentication patterns and more advanced techniques for route protection.
Prerequisites
Before we begin, make sure you have:
- Basic knowledge of JavaScript and Express.js
- Node.js installed on your machine
- A code editor
- Understanding of HTTP requests and responses
Understanding Protected Routes
Protected routes work by:
- Intercepting requests to specific routes
- Checking if the user is authenticated (usually via tokens or session)
- Either allowing access or redirecting/rejecting the request
This process is typically handled by middleware functions in Express.js.
Basic Authentication Middleware
Let's start by creating a simple authentication middleware that checks if a user is logged in:
// authMiddleware.js
function isAuthenticated(req, res, next) {
// Check if user is authenticated
if (req.session && req.session.user) {
// User is authenticated, proceed to the next middleware or route handler
return next();
}
// User is not authenticated
return res.status(401).json({ message: "Unauthorized - Please log in" });
}
module.exports = { isAuthenticated };
In this example:
- We check if the session exists and contains user data
- If authenticated, we call
next()
to continue to the next middleware or route handler - If not authenticated, we return a 401 Unauthorized response
Implementing Protected Routes
Now, let's use our middleware to protect specific routes:
const express = require('express');
const { isAuthenticated } = require('./authMiddleware');
const router = express.Router();
// Public routes - accessible to everyone
router.get('/', (req, res) => {
res.send('Welcome to our homepage!');
});
router.get('/about', (req, res) => {
res.send('About us page');
});
// Protected routes - only accessible to authenticated users
router.get('/profile', isAuthenticated, (req, res) => {
res.send(`Welcome to your profile, ${req.session.user.username}!`);
});
router.get('/dashboard', isAuthenticated, (req, res) => {
res.send('Your personal dashboard');
});
module.exports = router;
In this example, /profile
and /dashboard
are protected routes that can only be accessed by authenticated users. The isAuthenticated
middleware will check if the user is logged in before allowing access to these routes.
JWT Authentication for Protected Routes
For modern web applications, JWT (JSON Web Tokens) is a common authentication method. Let's implement protected routes using JWT:
const express = require('express');
const jwt = require('jsonwebtoken');
const router = express.Router();
// Secret key for JWT signing (in production, use environment variables)
const JWT_SECRET = 'your-secret-key';
// JWT authentication middleware
function authenticateToken(req, res, next) {
// Get the token from the Authorization header
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ message: 'Access token required' });
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ message: 'Invalid or expired token' });
}
// Token is valid, save user info in request
req.user = user;
next();
});
}
// Example login route (generates a token)
router.post('/login', (req, res) => {
// In a real app, validate username and password against DB
const { username, password } = req.body;
// Simplified authentication (replace with actual validation)
if (username === 'user' && password === 'pass') {
// User data to embed in token
const user = { id: 1, username: username, role: 'user' };
// Create and sign the token
const token = jwt.sign(user, JWT_SECRET, { expiresIn: '1h' });
res.json({ message: 'Login successful', token });
} else {
res.status(401).json({ message: 'Invalid credentials' });
}
});
// Protected route example
router.get('/protected-data', authenticateToken, (req, res) => {
// Access is granted if we reach here
res.json({
message: 'Protected data accessed successfully',
user: req.user,
data: { sensitive: 'This is private information' }
});
});
module.exports = router;
How JWT Authentication Works
- User logs in with valid credentials
- Server generates a signed JWT token
- Client stores the token (typically in localStorage or cookies)
- For protected routes, client includes the token in the Authorization header
- Server middleware verifies the token before allowing access
Role-Based Access Control
Often, you'll want to restrict certain routes based on user roles (e.g., admin vs. regular user). Let's implement role-based middleware:
// Role-based authorization middleware
function checkRole(role) {
return (req, res, next) => {
// First ensure user is authenticated
authenticateToken(req, res, () => {
// Check if user has the required role
if (req.user && req.user.role === role) {
next(); // User has the required role, proceed
} else {
res.status(403).json({ message: "Access forbidden - Insufficient permissions" });
}
});
};
}
// Admin-only route
router.get('/admin/settings', checkRole('admin'), (req, res) => {
res.json({ message: 'Admin settings page', adminControls: true });
});
// Regular user route
router.get('/user/settings', checkRole('user'), (req, res) => {
res.json({ message: 'User settings page' });
});
This middleware:
- First authenticates the user using the JWT token
- Then checks if the authenticated user has the required role
- Either allows access or returns a 403 Forbidden response
Practical Example: Complete Express Application
Let's put everything together in a more complete example:
const express = require('express');
const jwt = require('jsonwebtoken');
const cookieParser = require('cookie-parser');
const app = express();
// Middleware setup
app.use(express.json());
app.use(cookieParser());
// Secret key (use environment variables in production)
const JWT_SECRET = 'your-secret-key';
// Authentication middleware
function authenticateToken(req, res, next) {
// Check for token in cookies or Authorization header
const token = req.cookies.token ||
(req.headers.authorization && req.headers.authorization.split(' ')[1]);
if (!token) {
return res.status(401).json({ message: 'Authentication required' });
}
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(403).json({ message: 'Invalid or expired token' });
}
}
// Role checker middleware
function checkRole(roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ message: 'Authentication required' });
}
if (roles.includes(req.user.role)) {
return next();
}
return res.status(403).json({ message: 'Insufficient permissions' });
};
}
// Public routes
app.get('/', (req, res) => {
res.send('Welcome to our API! 👋');
});
// Login route
app.post('/login', (req, res) => {
const { username, password } = req.body;
// In a real app, validate against a database
let user;
if (username === 'admin' && password === 'admin123') {
user = { id: 1, username: 'admin', role: 'admin' };
} else if (username === 'user' && password === 'user123') {
user = { id: 2, username: 'user', role: 'user' };
} else {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Generate token
const token = jwt.sign(user, JWT_SECRET, { expiresIn: '1h' });
// Set token as cookie and also return in response
res.cookie('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 3600000 // 1 hour
});
res.json({ message: 'Login successful', token, user: { id: user.id, username: user.username, role: user.role } });
});
// Protected route - all authenticated users
app.get('/profile', authenticateToken, (req, res) => {
res.json({
message: 'Profile accessed successfully',
user: req.user
});
});
// Protected route - only for admins
app.get('/admin/dashboard', authenticateToken, checkRole(['admin']), (req, res) => {
res.json({
message: 'Admin dashboard',
adminStats: {
users: 150,
activeUsers: 65,
revenue: "$13,500"
}
});
});
// Protected route - for both users and editors
app.get('/content', authenticateToken, checkRole(['user', 'editor', 'admin']), (req, res) => {
res.json({
message: 'Content accessed',
articles: [
{ id: 1, title: 'Getting Started with Protected Routes' },
{ id: 2, title: 'Advanced Express Authentication' }
]
});
});
// Logout route
app.post('/logout', (req, res) => {
res.clearCookie('token');
res.json({ message: 'Logged out successfully' });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Testing Protected Routes
You can test your protected routes using tools like Postman, curl, or even a simple front-end application.
With curl:
# Login and get token
curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
# Access protected route with token
curl http://localhost:3000/admin/dashboard \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
With Postman:
- Send a POST request to
/login
with username and password - Copy the token from the response
- Set an Authorization header with value
Bearer YOUR_TOKEN_HERE
- Send a GET request to a protected route
Best Practices for Protected Routes
- Always use HTTPS in production to encrypt tokens and credentials
- Set appropriate token expiration times - shorter times are more secure
- Implement token refresh mechanisms for better user experience
- Store tokens securely on the client-side (HttpOnly cookies are recommended)
- Use environment variables for secrets and keys
- Validate and sanitize input on both client and server sides
- Implement rate limiting to prevent brute force attacks
- Log authentication events for security auditing
Common Pitfalls to Avoid
- Storing sensitive information in JWTs - Tokens should contain minimal user data
- Not validating tokens properly - Always check expiration and signature
- Using the same middleware for all protected routes - Different routes may need different security levels
- Hard-coding secrets in your application code
- Insufficient error handling that might leak sensitive information
Summary
In this tutorial, we've learned:
- How to create basic authentication middleware
- How to protect Express.js routes using middleware
- JWT-based authentication implementation
- Role-based access control
- How to implement protected routes in a complete Express application
- Best practices for secure authentication
Protected routes are essential for any application that deals with user data or has restricted functionality. By implementing proper authentication and authorization mechanisms, you ensure that your application's sensitive endpoints remain secure.
Additional Resources
- Express.js Documentation
- JWT.io - Learn more about JSON Web Tokens
- OWASP Authentication Cheat Sheet
- Express JWT Documentation
Exercises
- Implement a password reset system with protected routes
- Create a multi-level permission system (e.g., user, moderator, admin)
- Add two-factor authentication to your login process
- Implement token refreshing to extend user sessions
- Create a route that's only accessible to users who have verified their email address
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)