JustAppSec
Back to guides

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-BasedToken-Based (JWT)
StorageServer (DB/Redis) + session ID in cookieClient (cookie or header)
StateStateful (server stores session data)Stateless (token contains data)
RevocationImmediate (delete from store)Delayed (wait for expiry or blocklist)
ScalabilityRequires shared session storeNo shared state needed
Best forTraditional web appsAPIs, 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)
};
AttributePurposeValue
HttpOnlyPrevents XSS from reading the cookietrue
SecureOnly sent over HTTPStrue
SameSitePrevents CSRFLax or Strict
PathScope the cookie to a path/ (usually)
DomainScope to domainomit (defaults to current host)
Max-AgeExpiryshortest 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

StoreSpeedPersistenceBest For
RedisFastConfigurable (AOF/RDB)Most production apps
Database (PostgreSQL)ModerateFullApps needing audit trail
In-memoryFastestNone (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 is Secure, has Path=/, and has no Domain attribute (only the exact host).
  • __Secure- prefix: browser ensures the cookie is Secure.

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 HttpOnly flag (cookie readable by JavaScript).
  • Missing SameSite attribute (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.

Need help?Get in touch.