Session Management Best Practices: Secure User Authentication and Sessions
Introduction
Session management is critical for maintaining secure user authentication across requests. Poor session handling leads to session hijacking, fixation, and unauthorized access. This guide covers secure session implementation with practical examples for cookies, tokens, and session storage.
Common Session Security Issues
- Predictable session IDs
- Session fixation vulnerabilities
- Missing secure and HttpOnly flags
- No session expiration
- Session hijacking via XSS
- Insufficient session invalidation
- Insecure session storage
- Missing CSRF protection
Secure Cookie-Based Sessions
Vulnerable Session Implementation
javascript// VULNERABLE: Insecure session handling const express = require('express'); const session = require('express-session'); app.use(session({ secret: 'keyboard cat', // Weak secret resave: true, saveUninitialized: true, cookie: { // Missing security flags maxAge: null // No expiration } }));
Secure Session Configuration
javascript// SECURE: Production-ready session config const express = require('express'); const session = require('express-session'); const RedisStore = require('connect-redis').default; const { createClient } = require('redis'); const app = express(); // Redis client for session storage const redisClient = createClient({ host: process.env.REDIS_HOST, port: process.env.REDIS_PORT, password: process.env.REDIS_PASSWORD, tls: process.env.NODE_ENV === 'production' ? {} : undefined }); redisClient.connect(); app.use(session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, // Strong random secret from env resave: false, saveUninitialized: false, name: 'sessionId', // Custom name (don't use default 'connect.sid') cookie: { secure: true, // HTTPS only httpOnly: true, // Prevent JavaScript access maxAge: 1000 * 60 * 60 * 24, // 24 hours sameSite: 'strict', // CSRF protection domain: '.example.com', // Explicit domain path: '/' }, rolling: true, // Reset maxAge on every response unset: 'destroy' // Destroy session on logout })); // Session regeneration on login app.post('/login', async (req, res) => { const { username, password } = req.body; // Validate credentials const user = await authenticateUser(username, password); if (user) { // Regenerate session ID to prevent fixation req.session.regenerate((err) => { if (err) { return res.status(500).json({ error: 'Session error' }); } // Store user info in session req.session.userId = user.id; req.session.lastActivity = Date.now(); res.json({ success: true }); }); } else { res.status(401).json({ error: 'Invalid credentials' }); } }); // Logout with session destruction app.post('/logout', (req, res) => { req.session.destroy((err) => { if (err) { return res.status(500).json({ error: 'Logout failed' }); } res.clearCookie('sessionId'); res.json({ success: true }); }); });
Session Timeout and Activity Tracking
javascript// Automatic session timeout const SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes app.use((req, res, next) => { if (req.session && req.session.userId) { const now = Date.now(); const lastActivity = req.session.lastActivity || now; // Check if session expired if (now - lastActivity > SESSION_TIMEOUT) { return req.session.destroy((err) => { res.status(401).json({ error: 'Session expired' }); }); } // Update last activity req.session.lastActivity = now; } next(); });
JWT-Based Sessions
Secure JWT Implementation
javascript// Secure JWT session management const jwt = require('jsonwebtoken'); const crypto = require('crypto'); const JWT_SECRET = process.env.JWT_SECRET; const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET; // Token generation function generateTokens(userId) { const accessToken = jwt.sign( { userId }, JWT_SECRET, { expiresIn: '15m' } // Short-lived access token ); const refreshToken = jwt.sign( { userId, tokenId: crypto.randomUUID() }, JWT_REFRESH_SECRET, { expiresIn: '7d' } // Longer-lived refresh token ); return { accessToken, refreshToken }; } // Login with JWT app.post('/auth/login', async (req, res) => { const { username, password } = req.body; const user = await authenticateUser(username, password); if (!user) { return res.status(401).json({ error: 'Invalid credentials' }); } const { accessToken, refreshToken } = generateTokens(user.id); // Store refresh token hash in database await storeRefreshToken(user.id, refreshToken); // Set refresh token as HttpOnly cookie res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days }); res.json({ accessToken }); }); // Token refresh app.post('/auth/refresh', async (req, res) => { const refreshToken = req.cookies.refreshToken; if (!refreshToken) { return res.status(401).json({ error: 'No refresh token' }); } try { const decoded = jwt.verify(refreshToken, JWT_REFRESH_SECRET); // Check if refresh token is valid in database const isValid = await verifyRefreshToken(decoded.userId, refreshToken); if (!isValid) { return res.status(401).json({ error: 'Invalid refresh token' }); } // Generate new access token const accessToken = jwt.sign( { userId: decoded.userId }, JWT_SECRET, { expiresIn: '15m' } ); res.json({ accessToken }); } catch (error) { res.status(401).json({ error: 'Invalid refresh token' }); } }); // Logout app.post('/auth/logout', async (req, res) => { const refreshToken = req.cookies.refreshToken; if (refreshToken) { // Invalidate refresh token await revokeRefreshToken(refreshToken); } res.clearCookie('refreshToken'); res.json({ success: true }); }); // Middleware to verify access token function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'No token provided' }); } jwt.verify(token, JWT_SECRET, (err, decoded) => { if (err) { return res.status(403).json({ error: 'Invalid token' }); } req.userId = decoded.userId; next(); }); }
Session Storage Options
Redis Session Store
javascript// Redis for distributed sessions const Redis = require('ioredis'); const RedisStore = require('connect-redis').default; const redis = new Redis({ host: process.env.REDIS_HOST, port: 6379, password: process.env.REDIS_PASSWORD, db: 0, tls: process.env.NODE_ENV === 'production' ? {} : undefined, retryStrategy(times) { const delay = Math.min(times * 50, 2000); return delay; } }); const sessionStore = new RedisStore({ client: redis, prefix: 'sess:', ttl: 86400 // 24 hours }); app.use(session({ store: sessionStore, secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, maxAge: 86400000 } }));
Database Session Store
javascript// PostgreSQL session store const session = require('express-session'); const pgSession = require('connect-pg-simple')(session); const pg = require('pg'); const pgPool = new pg.Pool({ host: process.env.DB_HOST, port: 5432, database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, ssl: { rejectUnauthorized: false } }); app.use(session({ store: new pgSession({ pool: pgPool, tableName: 'user_sessions', createTableIfMissing: true }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days } }));
Preventing Session Fixation
javascript// Session fixation prevention app.post('/login', async (req, res) => { const { username, password } = req.body; const user = await authenticateUser(username, password); if (user) { // Regenerate session ID after successful login const oldSessionId = req.sessionID; req.session.regenerate((err) => { if (err) { return res.status(500).json({ error: 'Login failed' }); } // Set user data in new session req.session.userId = user.id; req.session.createdAt = Date.now(); // Log session change console.log(`Session regenerated: ${oldSessionId} -> ${req.sessionID}`); res.json({ success: true }); }); } else { res.status(401).json({ error: 'Invalid credentials' }); } });
Session Security Middleware
javascript// Comprehensive session security function sessionSecurityMiddleware(req, res, next) { if (!req.session || !req.session.userId) { return next(); } // 1. Check session age const SESSION_MAX_AGE = 24 * 60 * 60 * 1000; // 24 hours const sessionAge = Date.now() - (req.session.createdAt || 0); if (sessionAge > SESSION_MAX_AGE) { return req.session.destroy(() => { res.status(401).json({ error: 'Session expired' }); }); } // 2. Check inactivity timeout const INACTIVITY_TIMEOUT = 30 * 60 * 1000; // 30 minutes const inactiveTime = Date.now() - (req.session.lastActivity || Date.now()); if (inactiveTime > INACTIVITY_TIMEOUT) { return req.session.destroy(() => { res.status(401).json({ error: 'Session timeout due to inactivity' }); }); } // 3. Verify user agent (basic fingerprinting) if (req.session.userAgent && req.session.userAgent !== req.headers['user-agent']) { return req.session.destroy(() => { res.status(401).json({ error: 'Session hijacking detected' }); }); } // 4. Verify IP address (optional, can cause issues with mobile users) // if (req.session.ip && req.session.ip !== req.ip) { // return req.session.destroy(() => { // res.status(401).json({ error: 'IP address mismatch' }); // }); // } // Update last activity req.session.lastActivity = Date.now(); next(); } app.use(sessionSecurityMiddleware);
Concurrent Session Management
javascript// Limit concurrent sessions per user const activeSessionsStore = new Map(); app.post('/login', async (req, res) => { const { username, password } = req.body; const user = await authenticateUser(username, password); if (!user) { return res.status(401).json({ error: 'Invalid credentials' }); } req.session.regenerate(async (err) => { if (err) { return res.status(500).json({ error: 'Login failed' }); } const newSessionId = req.sessionID; // Get existing sessions for user const existingSessions = await getUserSessions(user.id); // Limit to 3 concurrent sessions const MAX_SESSIONS = 3; if (existingSessions.length >= MAX_SESSIONS) { // Remove oldest session const oldestSession = existingSessions[0]; await destroySession(oldestSession); } // Store new session await storeUserSession(user.id, newSessionId); req.session.userId = user.id; req.session.createdAt = Date.now(); res.json({ success: true }); }); });
Session Security Checklist
- ✅ Use cryptographically random session IDs
- ✅ Regenerate session ID on login
- ✅ Set secure and HttpOnly cookie flags
- ✅ Use SameSite=Strict or Lax
- ✅ Implement session expiration
- ✅ Track session inactivity
- ✅ Store sessions securely (Redis, DB)
- ✅ Destroy sessions on logout
- ✅ Implement CSRF protection
- ✅ Use HTTPS only
- ✅ Limit concurrent sessions
- ✅ Monitor session activity
- ✅ Implement session fingerprinting
- ✅ Use short-lived JWT access tokens
- ✅ Rotate refresh tokens
- ✅ Log security events
Conclusion
Secure session management requires careful attention to cookie flags, session storage, expiration policies, and protection against fixation and hijacking attacks. By implementing secure session configuration, regeneration on login, and proper timeout handling, you create a robust authentication system.
Session security should be regularly reviewed and tested as part of your overall security strategy. For comprehensive authentication and session management reviews, contact the Whitespots team for expert consultation.


