Safety

Session Management

Safely issuing, storing, rotating, and invalidating session tokens.

Overview

Session management covers how a server establishes, maintains, and terminates user sessions. A session token proves that a user authenticated previously. Tokens must be unpredictable, bound to the session, rotated after privilege changes, and invalidated on logout. Cookie storage attributes (HttpOnly, Secure, SameSite) and absolute/idle timeouts are fundamental controls.

Origin

Server-side sessions via cookies were introduced with Netscape's Cookie Specification (1994). PHP popularised session IDs in the late 1990s. Session fixation and session hijacking attacks were documented in the early 2000s. OWASP's Session Management Cheat Sheet (2005-present) is the canonical reference. Modern JWTs replaced server-side sessions in stateless API architectures, introducing new trade-offs.

Examples

Secure session configuration in Express with Redis store

import session from 'express-session';
import RedisStore from 'connect-redis';
import Redis from 'ioredis';
import { Express } from 'express';
import crypto from 'crypto';

const redis = new Redis(process.env.REDIS_URL!);

export function configureSession(app: Express): void {
  app.use(
    session({
      store: new RedisStore({ client: redis, prefix: 'sess:' }),
      secret: process.env.SESSION_SECRET!,
      name: '__Host-session', // __Host- prefix enforces Secure + path=/
      resave: false,
      saveUninitialized: false,
      cookie: {
        httpOnly: true,    // Inaccessible to JavaScript
        secure: true,      // HTTPS only
        sameSite: 'strict', // No cross-site requests
        maxAge: 30 * 60 * 1000, // 30 minute idle timeout
      },
      genid: () => crypto.randomBytes(32).toString('hex'),
    })
  );
}

// Rotate session ID after privilege elevation (login)
export async function loginUser(req: any, userId: string): Promise<void> {
  const oldData = req.session;
  await new Promise<void>((resolve, reject) =>
    req.session.regenerate((err: Error) => (err ? reject(err) : resolve()))
  );
  req.session.userId = userId;
  req.session.loginAt = new Date().toISOString();
}

session.regenerate() creates a new session ID after login, preventing session fixation attacks where an attacker pre-sets a known session ID. The __Host- cookie prefix enforces Secure and path=/ attributes, preventing subdomain cookie injection.

Session invalidation on logout and timeout in Ruby/Rails

class SessionsController < ApplicationController
  def create
    user = User.find_and_authenticate(params[:email], params[:password])
    return render json: { error: 'Invalid credentials' }, status: :unauthorized unless user

    # Rotate session ID to prevent fixation after authentication
    reset_session
    session[:user_id] = user.id
    session[:authenticated_at] = Time.current.iso8601
    session[:ip] = request.remote_ip
    render json: { message: 'Authenticated' }
  end

  def destroy
    reset_session # Clears all session data and rotates session ID
    render json: { message: 'Signed out' }
  end
end

class ApplicationController < ActionController::Base
  before_action :check_session_validity

  private

  def check_session_validity
    return unless session[:user_id]
    # Absolute timeout: 8 hours max session lifetime
    if Time.parse(session[:authenticated_at]) < 8.hours.ago
      reset_session
      render json: { error: 'Session expired' }, status: :unauthorized
    end
    # Bind session to originating IP to detect token theft (optional, breaks mobile)
    # if session[:ip] != request.remote_ip
    #   reset_session
    # end
  end
end

reset_session in Rails deletes the old session from the store and sets a new session ID, ensuring logout actually invalidates the session server-side. Absolute timeout (8 hours) prevents indefinitely valid sessions even with repeated activity.

Use Cases

  • 01Web applications where the browser session persists across multiple page loads and requests
  • 02Admin interfaces where session timeouts are shorter (15-30 minutes) and session binding to IP is appropriate
  • 03Banking applications requiring re-authentication for sensitive operations (transfers, password changes) even within a valid session
  • 04Multi-device logout functionality where server-side session stores allow invalidating all active sessions for a user simultaneously

When Not to Use

  • //Do not use client-side sessions (cookie-only, no server-side store) for sensitive applications; the client controls their data and can replay tokens indefinitely
  • //Do not implement JWTs as "sessions" with long expiry and no revocation; a stolen JWT is valid until expiry with no recourse
  • //Do not persist sessions across password resets; always invalidate all sessions when a user changes their password

Technical Notes

  • OWASP recommends session ID entropy of at least 128 bits (16 random bytes); the genid function above generates 256 bits via randomBytes(32). PHP's default session ID is 128 bits; older configs used MD5 of predictable data
  • The SameSite=Strict cookie attribute prevents the session cookie from being sent on any cross-site navigation, providing strong CSRF protection but breaking OAuth redirect flows (use SameSite=Lax for OAuth)
  • Session store backends: Redis provides fast in-memory storage with TTL-based expiry; PostgreSQL pg_sessions enables querying active sessions for security monitoring; Memcached is not recommended because it lacks persistence for graceful restarts
  • Concurrent session control (limiting active sessions per user) requires tracking active session IDs per user ID in the store; this adds complexity but prevents account sharing and limits the impact of credential theft