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
endreset_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
More in Safety