Skip to main content

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:

  1. Intercepting requests to specific routes
  2. Checking if the user is authenticated (usually via tokens or session)
  3. 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:

javascript
// 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:

javascript
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:

javascript
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

  1. User logs in with valid credentials
  2. Server generates a signed JWT token
  3. Client stores the token (typically in localStorage or cookies)
  4. For protected routes, client includes the token in the Authorization header
  5. 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:

javascript
// 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:

  1. First authenticates the user using the JWT token
  2. Then checks if the authenticated user has the required role
  3. Either allows access or returns a 403 Forbidden response

Practical Example: Complete Express Application

Let's put everything together in a more complete example:

javascript
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:

bash
# 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:

  1. Send a POST request to /login with username and password
  2. Copy the token from the response
  3. Set an Authorization header with value Bearer YOUR_TOKEN_HERE
  4. Send a GET request to a protected route

Best Practices for Protected Routes

  1. Always use HTTPS in production to encrypt tokens and credentials
  2. Set appropriate token expiration times - shorter times are more secure
  3. Implement token refresh mechanisms for better user experience
  4. Store tokens securely on the client-side (HttpOnly cookies are recommended)
  5. Use environment variables for secrets and keys
  6. Validate and sanitize input on both client and server sides
  7. Implement rate limiting to prevent brute force attacks
  8. 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:

  1. How to create basic authentication middleware
  2. How to protect Express.js routes using middleware
  3. JWT-based authentication implementation
  4. Role-based access control
  5. How to implement protected routes in a complete Express application
  6. 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

Exercises

  1. Implement a password reset system with protected routes
  2. Create a multi-level permission system (e.g., user, moderator, admin)
  3. Add two-factor authentication to your login process
  4. Implement token refreshing to extend user sessions
  5. 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! :)