Session Management with Redis
Session Management with Redis
Session management is a critical component of web applications that require user authentication and state persistence. Redis is an excellent choice for storing sessions due to its speed, built-in expiration, and ability to handle high concurrency. This lesson covers implementing secure, scalable session management with Redis in Node.js.
Why Redis for Sessions?
Traditional session storage methods have limitations:
- Memory Store: Not scalable across multiple servers, lost on restart
- File System: Slow, difficult to clean up expired sessions
- Database: Slower than Redis, adds load to your primary database
Redis advantages:
- In-memory speed (sub-millisecond access)
- Built-in TTL (automatic session expiration)
- Horizontal scalability (multiple app servers can share sessions)
- Persistence options (AOF/RDB for durability)
- Atomic operations (prevent race conditions)
Installing Dependencies
Install required packages for session management:
Basic Session Setup
Configure Express with Redis session store:
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const Redis = require('ioredis');
const app = express();
const redisClient = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD
});
// Configure session middleware
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only in prod
httpOnly: true, // Prevent XSS
maxAge: 1000 * 60 * 60 * 24 // 24 hours
}
}));
// Test endpoint
app.get('/', (req, res) => {
if (req.session.views) {
req.session.views++;
} else {
req.session.views = 1;
}
res.json({
message: 'Session working',
views: req.session.views,
sessionID: req.sessionID
});
});
secret option is used to sign the session ID cookie. Use a strong, random secret in production and store it in environment variables, never hardcode it.Session Configuration Options
Understanding important session configuration options:
store: new RedisStore({
client: redisClient,
prefix: 'sess:', // Key prefix in Redis
ttl: 86400 // Session TTL in seconds (24 hours)
}),
secret: process.env.SESSION_SECRET,
name: 'sessionId', // Cookie name (default: connect.sid)
resave: false, // Don't save unchanged sessions
saveUninitialized: false, // Don't create sessions until data is stored
rolling: true, // Reset expiration on every response (sliding sessions)
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JavaScript access
sameSite: 'strict', // CSRF protection
maxAge: 1000 * 60 * 60 * 24, // 24 hours
domain: '.example.com' // Share across subdomains
}
}));
rolling: true to implement sliding sessions - the session expiration resets with each request, keeping active users logged in indefinitely.User Authentication with Sessions
Implement login, logout, and authentication middleware:
// Login endpoint
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Find user in database
const user = await db.users.findOne({ username });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Verify password
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Store user data in session
req.session.userId = user.id;
req.session.username = user.username;
req.session.role = user.role;
res.json({
message: 'Login successful',
user: {
id: user.id,
username: user.username,
role: user.role
}
});
});
// Logout endpoint
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.clearCookie('sessionId');
res.json({ message: 'Logout successful' });
});
});
// Authentication middleware
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: 'Authentication required' });
}
next();
}
// Protected route
app.get('/profile', requireAuth, async (req, res) => {
const user = await db.users.findById(req.session.userId);
res.json({ user });
});
Session Security Best Practices
Implement security measures to protect sessions:
app.post('/login', async (req, res) => {
// ... authentication logic ...
// Regenerate session ID
req.session.regenerate((err) => {
if (err) {
return res.status(500).json({ error: 'Login failed' });
}
// Set session data
req.session.userId = user.id;
req.session.username = user.username;
res.json({ message: 'Login successful' });
});
});
// 2. Store session creation time (detect session hijacking)
app.post('/login', async (req, res) => {
// ... authentication ...
req.session.userId = user.id;
req.session.createdAt = Date.now();
req.session.ipAddress = req.ip;
res.json({ message: 'Login successful' });
});
// 3. Validate session on each request
function validateSession(req, res, next) {
if (!req.session.userId) {
return next();
}
// Check if IP address changed (potential hijacking)
if (req.session.ipAddress && req.session.ipAddress !== req.ip) {
req.session.destroy();
return res.status(401).json({ error: 'Session invalid' });
}
// Check session age (force re-login after 7 days)
const maxAge = 1000 * 60 * 60 * 24 * 7; // 7 days
if (Date.now() - req.session.createdAt > maxAge) {
req.session.destroy();
return res.status(401).json({ error: 'Session expired' });
}
next();
}
app.use(validateSession);
Sliding Sessions (Activity-Based Expiration)
Implement sliding sessions that extend expiration on user activity:
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
rolling: true, // Reset maxAge on every request
cookie: {
maxAge: 1000 * 60 * 30 // 30 minutes of inactivity
}
}));
// Or implement custom sliding logic
function slidingSession(inactivityMinutes) {
const maxAge = 1000 * 60 * inactivityMinutes;
return (req, res, next) => {
if (!req.session.userId) {
return next();
}
const lastActivity = req.session.lastActivity || Date.now();
const timeSinceActivity = Date.now() - lastActivity;
if (timeSinceActivity > maxAge) {
// Session expired due to inactivity
req.session.destroy();
return res.status(401).json({ error: 'Session expired' });
}
// Update last activity timestamp
req.session.lastActivity = Date.now();
next();
};
}
app.use(slidingSession(30)); // 30 minutes inactivity timeout
Session Cleanup and Monitoring
Monitor and clean up expired sessions:
async function getActiveSessionCount() {
const keys = await redisClient.keys('sess:*');
return keys.length;
}
// Get user session count (detect multiple logins)
async function getUserSessionCount(userId) {
const keys = await redisClient.keys('sess:*');
let count = 0;
for (const key of keys) {
const sessionData = await redisClient.get(key);
if (sessionData) {
const session = JSON.parse(sessionData);
if (session.userId === userId) {
count++;
}
}
}
return count;
}
// Destroy all sessions for a user (force logout)
async function destroyUserSessions(userId) {
const keys = await redisClient.keys('sess:*');
for (const key of keys) {
const sessionData = await redisClient.get(key);
if (sessionData) {
const session = JSON.parse(sessionData);
if (session.userId === userId) {
await redisClient.del(key);
}
}
}
}
// Admin endpoint to view session stats
app.get('/admin/sessions', requireAdmin, async (req, res) => {
const totalSessions = await getActiveSessionCount();
res.json({ totalSessions });
});
Session Data Structure in Redis
Understanding how sessions are stored in Redis:
// Example: sess:abc123def456
// Session data (JSON string):
{
"cookie": {
"originalMaxAge": 86400000,
"expires": "2025-02-17T12:00:00.000Z",
"secure": true,
"httpOnly": true,
"path": "/"
},
"userId": 1001,
"username": "john_doe",
"role": "admin",
"createdAt": 1708171200000,
"lastActivity": 1708174800000
}
// View session in Redis CLI:
// redis-cli
// KEYS sess:*
// GET sess:abc123def456
// TTL sess:abc123def456