Redis & Advanced Caching

Session Management with Redis

18 min Lesson 10 of 30

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:

npm install express express-session connect-redis ioredis

Basic Session Setup

Configure Express with Redis session store:

const express = require('express');
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
  });
});
Note: The 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:

app.use(session({
  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
  }
}));
Tip: Set 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:

const bcrypt = require('bcrypt');

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

// 1. Regenerate session ID after login (prevent session fixation)
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);
Warning: Never store sensitive data like passwords or credit card numbers in sessions. Store only user identifiers and load sensitive data from the database when needed.

Sliding Sessions (Activity-Based Expiration)

Implement sliding sessions that extend expiration on user activity:

app.use(session({
  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:

// Get active session count
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 });
});
Note: Redis automatically removes expired sessions based on the TTL. You don't need manual cleanup jobs, but monitoring active sessions helps track user activity and detect anomalies.

Session Data Structure in Redis

Understanding how sessions are stored in Redis:

// Session key format: sess:<sessionID>
// 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
Exercise: Build a complete authentication system with registration, login, logout, and protected routes. Implement sliding sessions (15 minutes inactivity timeout), session validation middleware (check IP address), and an admin endpoint to view active sessions. Test by logging in from two different browsers and verify sessions work independently.