Express Cookie Authentication
Introduction
Cookie authentication is one of the most common methods for managing user sessions in web applications. In this guide, you'll learn how to implement cookie-based authentication in Express.js applications from the ground up.
Unlike token-based approaches like JWT, cookie authentication stores session information on the server-side and gives the client a cookie containing a session identifier. This approach offers several security advantages and is relatively simple to implement using Express middleware.
What are Cookies?
Cookies are small pieces of data stored in the user's browser. When a user visits a website, the server can send cookies which the browser will store and send back with subsequent requests to the same site.
Cookies are perfect for authentication because:
- They're automatically sent with every request to the same domain
- They can be configured to be secure and accessible only by the server
- They can persist across browser sessions (for "remember me" functionality)
Setting Up Cookie Authentication
Prerequisites
Before we begin, make sure you have the following installed:
npm install express express-session cookie-parser bcrypt
Here's what each package does:
express
: Our web frameworkexpress-session
: Middleware for session managementcookie-parser
: Middleware for parsing cookiesbcrypt
: For password hashing
Basic Setup
Let's start with a basic Express server setup:
const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const bcrypt = require('bcrypt');
const app = express();
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
// Configure session middleware
app.use(session({
secret: 'your-secret-key', // Used to sign the session ID cookie
resave: false, // Don't save session if unmodified
saveUninitialized: false, // Don't create session until something stored
cookie: {
httpOnly: true, // Prevents client-side JS from reading the cookie
secure: process.env.NODE_ENV === 'production', // Cookie only works over HTTPS in production
maxAge: 24 * 60 * 60 * 1000 // Cookie expiry (1 day in this example)
}
}));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Implementing User Authentication
For a basic authentication system, we need to:
- Register users
- Log in users
- Protect routes
- Log out users
For this example, we'll use a simple in-memory user store. In a real application, you'd use a database.
// Mock user database
const users = [];
// User registration
app.post('/register', async (req, res) => {
try {
const { username, password } = req.body;
// Check if user exists
if (users.find(user => user.username === username)) {
return res.status(400).json({ message: 'User already exists' });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create new user
const newUser = {
id: users.length + 1,
username,
password: hashedPassword
};
users.push(newUser);
res.status(201).json({ message: 'User created successfully' });
} catch (error) {
res.status(500).json({ message: 'Error creating user', error: error.message });
}
});
// User login
app.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
// Find user
const user = users.find(user => user.username === username);
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Compare password
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Store user info in session
req.session.userId = user.id;
req.session.username = user.username;
res.json({ message: 'Login successful' });
} catch (error) {
res.status(500).json({ message: 'Error logging in', error: error.message });
}
});
Protecting Routes
Now that we can register and log in users, let's create a middleware to protect routes that require authentication:
// Authentication middleware
const requireAuth = (req, res, next) => {
if (!req.session.userId) {
return res.status(401).json({ message: 'Unauthorized - Please login' });
}
next();
};
// Protected route example
app.get('/profile', requireAuth, (req, res) => {
res.json({
message: 'This is a protected route',
user: {
id: req.session.userId,
username: req.session.username
}
});
});
Logging Out
To log out a user, we simply destroy their session:
app.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).json({ message: 'Could not log out, please try again' });
}
res.clearCookie('connect.sid'); // Clear the session cookie
res.json({ message: 'Logged out successfully' });
});
});
Security Considerations
Cookie authentication requires careful attention to security:
1. Cross-Site Request Forgery (CSRF) Protection
To protect against CSRF attacks, you should implement CSRF tokens:
const csrf = require('csurf');
// Setup CSRF protection
const csrfProtection = csrf({ cookie: true });
// Apply to routes that change state
app.post('/login', csrfProtection, async (req, res) => {
// Login logic here
});
// Generate CSRF token for forms
app.get('/form', csrfProtection, (req, res) => {
// Send form with CSRF token
res.send(`
<form action="/login" method="POST">
<input type="hidden" name="_csrf" value="${req.csrfToken()}">
<!-- Other form fields -->
</form>
`);
});
2. Cookie Settings
Make your cookies more secure with these settings:
app.use(session({
// other options...
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000, // 1 day
sameSite: 'strict', // Prevents the cookie from being sent in cross-site requests
path: '/' // Scope your cookies to a specific path
}
}));
Complete Example
Here's a complete example combining all the concepts:
const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const bcrypt = require('bcrypt');
const csrf = require('csurf');
const app = express();
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
// Configure session
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000,
sameSite: 'strict'
}
}));
// Setup CSRF protection
const csrfProtection = csrf({ cookie: true });
// Mock user database
const users = [];
// Authentication middleware
const requireAuth = (req, res, next) => {
if (!req.session.userId) {
return res.status(401).json({ message: 'Unauthorized' });
}
next();
};
// User registration
app.post('/register', csrfProtection, async (req, res) => {
try {
const { username, password } = req.body;
// Check if user exists
if (users.find(user => user.username === username)) {
return res.status(400).json({ message: 'User already exists' });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create new user
const newUser = {
id: users.length + 1,
username,
password: hashedPassword
};
users.push(newUser);
res.status(201).json({ message: 'User created successfully' });
} catch (error) {
res.status(500).json({ message: 'Error creating user', error: error.message });
}
});
// User login
app.post('/login', csrfProtection, async (req, res) => {
try {
const { username, password } = req.body;
// Find user
const user = users.find(user => user.username === username);
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Compare password
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Store user info in session
req.session.userId = user.id;
req.session.username = user.username;
res.json({ message: 'Login successful' });
} catch (error) {
res.status(500).json({ message: 'Error logging in', error: error.message });
}
});
// Protected route
app.get('/profile', requireAuth, (req, res) => {
res.json({
message: 'You are viewing your profile',
user: {
id: req.session.userId,
username: req.session.username
}
});
});
// Logout
app.post('/logout', requireAuth, (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).json({ message: 'Could not log out' });
}
res.clearCookie('connect.sid');
res.json({ message: 'Logged out successfully' });
});
});
// Get CSRF token for frontend forms
app.get('/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Real-World Use Case: Authentication for a Todo App
Let's see how cookie authentication fits into a simple todo application:
// Add todo item (protected route)
app.post('/todos', requireAuth, csrfProtection, (req, res) => {
const { title, description } = req.body;
// In a real app, you'd save this to a database
const newTodo = {
id: Math.random().toString(36).substring(2),
title,
description,
userId: req.session.userId,
completed: false,
createdAt: new Date()
};
// Mock database operation
todos.push(newTodo);
res.status(201).json(newTodo);
});
// Get user's todos (protected route)
app.get('/todos', requireAuth, (req, res) => {
// In a real app, you'd query the database
const userTodos = todos.filter(todo => todo.userId === req.session.userId);
res.json(userTodos);
});
Testing Your Authentication System
You can test your authentication system using tools like curl, Postman or writing simple HTML forms. Here's a sequence to test:
- Register a new user
- Log in with the new user credentials
- Access a protected route
- Log out
- Try to access the protected route again (should fail)
Summary
In this guide, you've learned how to implement cookie-based authentication in Express applications:
- Set up Express with session middleware
- Implement user registration and login
- Create middleware to protect routes
- Handle user logout securely
- Add security enhancements like CSRF protection
- Apply authentication to a real-world example
Cookie authentication provides a robust solution for managing user sessions while maintaining good security practices. Remember that in production applications, you'll want to store sessions in a more persistent store like Redis, MongoDB, or a database rather than in-memory as we did in these examples.
Further Learning
To continue your learning about Express authentication, consider exploring:
- Session stores (Redis, MongoDB, etc.) for production
- Implementing "remember me" functionality
- Account recovery and password reset flows
- Multi-factor authentication
- OAuth integration for "Sign in with Google/Facebook/etc."
Exercises
- Enhance the code to use a real database like MongoDB
- Add a "remember me" option that extends the session lifetime
- Implement password reset functionality
- Add account confirmation via email
- Create a simple frontend that works with your authentication system
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)