Secure Session Management
Session management is how your application tracks who is logged in. Get it wrong and attackers can hijack sessions, fixate sessions, or bypass logout. This guide covers the patterns that work and the mistakes to avoid.
Cookie-Based Sessions vs Token-Based Sessions
| Cookie-Based | Token-Based (JWT) | |
|---|---|---|
| Storage | Server (DB/Redis) + session ID in cookie | Client (cookie or header) |
| State | Stateful (server stores session data) | Stateless (token contains data) |
| Revocation | Immediate (delete from store) | Delayed (wait for expiry or blocklist) |
| Scalability | Requires shared session store | No shared state needed |
| Best for | Traditional web apps | APIs, SPAs, mobile apps |
For server-rendered web apps, cookie-based sessions are simpler and easier to secure. Use tokens for APIs.
Reference: OWASP — Session Management Cheat Sheet
Secure Cookie Configuration
Every session cookie must have these attributes:
// Express / Next.js example
const sessionCookie = {
name: "__session",
value: sessionId,
httpOnly: true, // not accessible via JavaScript
secure: true, // only sent over HTTPS
sameSite: "lax", // CSRF protection
path: "/", // available on all routes
maxAge: 60 * 60 * 24, // 24 hours (in seconds)
};
| Attribute | Purpose | Value |
|---|---|---|
HttpOnly | Prevents XSS from reading the cookie | true |
Secure | Only sent over HTTPS | true |
SameSite | Prevents CSRF | Lax or Strict |
Path | Scope the cookie to a path | / (usually) |
Domain | Scope to domain | omit (defaults to current host) |
Max-Age | Expiry | shortest practical lifetime |
Reference: MDN — Set-Cookie
Generate Secure Session IDs
Session IDs must be random and unguessable:
import crypto from "crypto";
function generateSessionId(): string {
return crypto.randomBytes(32).toString("hex"); // 256 bits of entropy
}
Requirements:
- At least 128 bits of entropy (256 is better).
- Generated by a cryptographically secure random number generator.
- Not sequential, not derived from user data, not predictable.
Reference: OWASP — Session ID Properties
Session Rotation
Issue a new session ID after any privilege change to prevent session fixation attacks:
async function rotateSession(req: Request, userId: string) {
// Delete old session
const oldSessionId = getSessionIdFromCookie(req);
if (oldSessionId) {
await sessionStore.delete(oldSessionId);
}
// Create new session
const newSessionId = generateSessionId();
await sessionStore.create(newSessionId, { userId, createdAt: Date.now() });
return newSessionId; // set as new cookie
}
Rotate on:
- Login (most important)
- Privilege escalation (e.g., entering admin mode)
- Password change
- Any sensitive action
Session Expiration
Implement both idle timeout and absolute timeout:
const IDLE_TIMEOUT = 30 * 60 * 1000; // 30 minutes of inactivity
const ABSOLUTE_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours total
async function validateSession(sessionId: string): Promise<Session | null> {
const session = await sessionStore.get(sessionId);
if (!session) return null;
const now = Date.now();
// Absolute timeout
if (now - session.createdAt > ABSOLUTE_TIMEOUT) {
await sessionStore.delete(sessionId);
return null;
}
// Idle timeout
if (now - session.lastActivity > IDLE_TIMEOUT) {
await sessionStore.delete(sessionId);
return null;
}
// Update last activity
await sessionStore.update(sessionId, { lastActivity: now });
return session;
}
Logout Must Destroy the Session
Logout is not just clearing the cookie — you must destroy the server-side session:
async function logout(req: Request): Promise<Response> {
const sessionId = getSessionIdFromCookie(req);
if (sessionId) {
// 1. Delete server-side session
await sessionStore.delete(sessionId);
}
// 2. Clear the cookie
return new Response("Logged out", {
headers: {
"Set-Cookie": `__session=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0`,
},
});
}
Without server-side deletion, the session ID in the cookie is still valid if the user (or an attacker) replays it.
Logout Everywhere
Allow users to invalidate all their active sessions:
async function logoutEverywhere(userId: string) {
// Delete all sessions for this user
await sessionStore.deleteByUser(userId);
}
This is essential for:
- Password changes (force re-login on all devices).
- Account compromise response.
- User-initiated "sign out of all devices."
Concurrent Session Limits
Optionally limit the number of active sessions per user:
const MAX_SESSIONS = 5;
async function createSession(userId: string): Promise<string> {
const activeSessions = await sessionStore.getByUser(userId);
if (activeSessions.length >= MAX_SESSIONS) {
// Remove oldest session
const oldest = activeSessions.sort((a, b) => a.createdAt - b.createdAt)[0];
await sessionStore.delete(oldest.id);
}
const sessionId = generateSessionId();
await sessionStore.create(sessionId, { userId, createdAt: Date.now() });
return sessionId;
}
Session Storage
| Store | Speed | Persistence | Best For |
|---|---|---|---|
| Redis | Fast | Configurable (AOF/RDB) | Most production apps |
| Database (PostgreSQL) | Moderate | Full | Apps needing audit trail |
| In-memory | Fastest | None (lost on restart) | Development only |
Redis with TTL is the standard approach for session storage.
Cookie Prefixes
Use cookie prefixes for additional browser-enforced security:
__Host-session=abc123; Secure; HttpOnly; SameSite=Lax; Path=/
__Host-prefix: browser ensures the cookie isSecure, hasPath=/, and has noDomainattribute (only the exact host).__Secure-prefix: browser ensures the cookie isSecure.
Reference: MDN — Cookie Prefixes
Common Mistakes
- Storing session IDs in localStorage (vulnerable to XSS).
- Not rotating session ID after login (session fixation).
- Logout only clears the cookie without destroying the server-side session.
- No idle timeout (sessions last forever while the tab is open).
- Using predictable session IDs (sequential numbers, timestamps).
- Missing
HttpOnlyflag (cookie readable by JavaScript). - Missing
SameSiteattribute (vulnerable to CSRF in older browsers).
Related Guides
Content is AI-assisted and reviewed by our team, but issues may be missed and best practices evolve rapidly, send corrections to [email protected]. Always consult official documentation and validate key implementation decisions before making design or security choices.
