Session Management
What is a Session?
HTTP is stateless. Every request is independent. Sessions provide the illusion of state by associating a series of requests with a specific user. After authentication, the server issues a session identifier that the client sends with each subsequent request.
Session management is a critical security control. If an attacker obtains a valid session token, they have the user's access without needing their password.
Session Tokens
A session token must be unpredictable, unique, and protected in transit and storage.
Generating Secure Tokens
Requirements:
- At least 128 bits of entropy (OWASP recommendation)
- Generated using a cryptographically secure random number generator
- Not derived from user data (username, email, timestamp)
- Not sequential or predictable
Bad token generation:
token = base64(username + ":" + timestamp)
token = md5(user_id + secret_key)
token = str(auto_increment_id)
Good token generation:
Python: secrets.token_hex(32) # 256 bits of entropy
Node: crypto.randomBytes(32).toString('hex')
Go: crypto/rand.Read(make([]byte, 32))
The language's standard cryptographic library handles this.
Do not build your own token generation.
Cookie Security Flags
Session tokens stored in cookies must have proper security flags.
Set-Cookie: session=abc123def456;
HttpOnly;
Secure;
SameSite=Lax;
Path=/;
Max-Age=3600;
Domain=app.example.com
HttpOnly:
Cookie is not accessible via JavaScript (document.cookie).
Prevents XSS from stealing session tokens.
This is non-negotiable for session cookies.
Secure:
Cookie is only sent over HTTPS connections.
Prevents interception on unencrypted connections.
Always set this in production.
SameSite:
Controls when cookies are sent with cross-site requests.
- Strict: Never sent cross-site (breaks legitimate links from email)
- Lax: Sent on top-level navigations (GET), not on POST/fetch
- None: Always sent (requires Secure flag, needed for cross-site APIs)
Lax is the recommended default for session cookies.
Path:
Limits which paths the cookie is sent to.
Use "/" for the session cookie unless there's a specific reason not to.
Domain:
Limits which domains the cookie is sent to.
Set this explicitly. Do not use wildcard domains.
Real example: 2012 Firesheep attack
- Tool that captured session cookies on unencrypted WiFi
- Worked on Facebook, Twitter, Amazon, and many others
- Demonstrated why the Secure flag and HTTPS are essential
- Led to widespread adoption of HTTPS across major sites
Session Lifecycle
Create on Login
When a user successfully authenticates:
1. Verify credentials (password hash comparison)
2. Invalidate any pre-existing session (prevents fixation)
3. Generate a new session token
4. Store session data server-side (user ID, creation time, IP)
5. Set the session cookie with proper flags
6. Log the authentication event
Important: Generate the session AFTER authentication, not before.
A session created before login should be replaced with a new one.
Invalidate on Logout
When a user logs out:
1. Delete the session from the server-side session store
2. Clear the session cookie (set Max-Age=0 or Expires in the past)
3. Log the logout event
Common mistake: Only deleting the cookie without invalidating
the server-side session. The old token remains valid — an attacker
who captured it can still use it.
Expire After Inactivity
Two types of session expiration:
Absolute timeout:
Session expires after a fixed time regardless of activity.
Banking: 15-30 minutes
SaaS: 8-24 hours
Social media: days to weeks
Idle timeout:
Session expires after a period of inactivity.
Reset the timer on each request.
Banking: 5-15 minutes of inactivity
SaaS: 30-60 minutes of inactivity
Implementation:
Store last_activity_time in the session.
On each request: check if (now - last_activity_time) > idle_timeout.
If expired: destroy session, redirect to login.
If valid: update last_activity_time.
Both types should be enforced. Absolute timeout prevents
stolen sessions from being used indefinitely by keeping them active.
Session Fixation Prevention
Session fixation is an attack where the attacker sets the session ID before the user authenticates, then uses that known session ID after the user logs in.
Attack flow:
1. Attacker creates a session on the target site (gets session ID "abc123")
2. Attacker tricks victim into using that session ID:
- Link with session in URL: https://app.com/login?sid=abc123
- Setting a cookie via XSS or a subdomain
3. Victim logs in using session "abc123"
4. The session is now authenticated and associated with the victim's account
5. Attacker uses session "abc123" — they are now the victim
Prevention: Regenerate the session ID on login.
The old session ID "abc123" becomes invalid.
A new session ID is issued that the attacker does not know.
Regeneration points (always regenerate the session ID when):
- User logs in
- User's privilege level changes (regular user becomes admin)
- User performs a sensitive operation
- A significant time has passed
Most web frameworks handle this with a single call:
PHP: session_regenerate_id(true)
Python: request.session.cycle_key()
Ruby: reset_session
Java: request.getSession().invalidate(); request.getSession(true);
Node (Express): req.session.regenerate()
Concurrent Session Control
Should a user be allowed to have multiple active sessions (multiple devices, multiple browsers)?
Options:
Allow unlimited sessions (most common):
- User logs in from phone, laptop, tablet — all work
- Simplest implementation
- Risk: stolen session goes unnoticed because the user's
own session still works
Limit to N sessions:
- Allow 3-5 concurrent sessions
- When limit is exceeded, invalidate the oldest session
- Provides some protection without inconveniencing users
Single session (strictest):
- New login invalidates all previous sessions
- User immediately notices unauthorized access (they get logged out)
- Can be frustrating for legitimate multi-device use
- Good for banking and high-security applications
Recommended: Allow concurrent sessions but show users their active
sessions and let them revoke any they do not recognize.
Google, GitHub, and most major platforms take this approach.
Implementation:
Store sessions in a database or Redis (not just memory):
{
session_id: "a8f5f167...",
user_id: 42,
created_at: "2025-03-15T10:30:00Z",
last_active: "2025-03-15T14:22:00Z",
ip_address: "203.0.113.50",
user_agent: "Mozilla/5.0...",
device_name: "Chrome on macOS"
}
Expose an endpoint: GET /account/sessions
Allow revocation: DELETE /account/sessions/{session_id}
Server-Side vs Client-Side Sessions
Server-Side Sessions
The server stores session data (user ID, permissions, preferences) and only sends an opaque identifier to the client.
Flow:
Client sends: Cookie: session=a8f5f167f44d...
Server looks up: sessions["a8f5f167f44d..."] -> { user_id: 42, role: "admin" }
Server knows who the user is and what they can do
Storage backends:
- In-memory (fast, lost on restart, not horizontally scalable)
- Redis (fast, persistent, shared across servers)
- Database (slower, persistent, auditable)
- File system (simple, not recommended for production)
Advantages:
- Server controls all session data
- Session can be revoked instantly (delete from store)
- No sensitive data sent to the client
- Session data size is unlimited
Disadvantages:
- Requires server-side storage
- Horizontal scaling requires shared session store (Redis)
- Additional infrastructure dependency
Client-Side Sessions (JWT)
JSON Web Tokens store session data in the token itself, signed by the server. The client sends the entire token with each request.
JWT structure:
header.payload.signature
Header: { "alg": "HS256", "typ": "JWT" }
Payload: { "sub": "42", "role": "admin", "exp": 1710000000 }
Signature: HMAC-SHA256(header + "." + payload, secret_key)
The server verifies the signature to trust the claims in the payload.
JWT advantages:
- No server-side session store needed
- Works well for stateless microservices
- Can be verified by any service with the secret key (or public key for RSA)
JWT disadvantages:
- Cannot be revoked without additional infrastructure
(once issued, valid until expiry)
- Size grows with claims (sent on every request)
- Token theft grants access until expiry
- Common implementation mistakes lead to vulnerabilities
JWT Security Pitfalls
Real vulnerability: "alg": "none" attack
- JWT spec allows "none" as an algorithm (no signature)
- Attacker changes the header to { "alg": "none" }
- Removes the signature
- Some libraries accept this token as valid
- Fix: Always validate the algorithm server-side,
reject "none" and unexpected algorithms
Real vulnerability: HMAC/RSA confusion
- Server uses RSA (asymmetric): signs with private key,
verifies with public key
- Attacker changes alg to HS256 (symmetric)
- Signs with the public key (which is public)
- Vulnerable libraries use the public key as the HMAC secret
- Fix: Explicitly specify the expected algorithm when verifying
Token expiration:
- Short-lived access tokens (5-15 minutes)
- Longer-lived refresh tokens (hours to days)
- Refresh tokens should be stored securely (HttpOnly cookie, not localStorage)
- Refresh tokens can be revoked server-side (requires a store)
When to Use Which
Use server-side sessions when:
- You need instant session revocation
- You have a monolithic application
- You need to store large amounts of session data
- Security is the primary concern (banking, healthcare)
Use JWTs when:
- You have stateless microservices
- You need cross-service authentication
- You accept the trade-off of non-revocable tokens
- You use short expiry times with refresh token rotation
Hybrid approach (common in practice):
- JWT for API authentication between services (short-lived)
- Server-side sessions for user-facing web application
- Refresh tokens stored server-side for revocation capability
Session Hijacking Prevention
Attack vectors for session theft:
XSS (Cross-Site Scripting):
- Attacker injects JavaScript that reads document.cookie
- Prevention: HttpOnly flag prevents JavaScript access to cookies
- Also: Content Security Policy, input/output encoding
Network sniffing:
- Attacker captures session cookie over unencrypted connection
- Prevention: Secure flag, HTTPS everywhere, HSTS
Session fixation:
- Attacker sets the session ID before authentication
- Prevention: Regenerate session ID on login
Malware / browser extensions:
- Malicious software on the user's device reads cookies
- Prevention: Limited — this is a client-side compromise
- Mitigate with device verification and session monitoring
Cross-Site Request Forgery (CSRF):
- Attacker makes the browser send authenticated requests
- Not session theft, but session abuse
- Prevention: SameSite cookies, CSRF tokens, checking Origin header
Common Pitfalls
-
Storing session tokens in localStorage: localStorage is accessible via JavaScript and vulnerable to XSS. Session tokens belong in HttpOnly cookies. localStorage is appropriate for non-sensitive UI preferences.
-
Not regenerating session ID on login: This enables session fixation attacks. Always generate a new session ID when a user's authentication state changes.
-
Using JWTs as a session replacement without understanding the trade-offs: JWTs cannot be revoked without building revocation infrastructure (which negates the "stateless" advantage). If you need instant logout, use server-side sessions.
-
Setting long expiration on JWTs: A JWT with a 24-hour expiry that gets stolen gives the attacker 24 hours of access. Use short-lived access tokens (5-15 minutes) with refresh token rotation.
-
Not expiring sessions server-side: Relying only on cookie expiry means the server never cleans up. Old sessions accumulate and remain valid. Enforce expiry on the server side.
-
Transmitting session tokens in URLs: URL parameters appear in browser history, referrer headers, server logs, and proxy logs. Never put session tokens in URLs.
Key Takeaways
- Session tokens must be cryptographically random (128+ bits of entropy), stored in cookies with HttpOnly, Secure, and SameSite flags.
- Regenerate the session ID on login to prevent session fixation. Invalidate sessions on logout by destroying the server-side record.
- Enforce both absolute timeout and idle timeout. Banking applications need short timeouts; SaaS applications can be longer.
- Server-side sessions provide instant revocation and are more secure for user-facing applications. JWTs work well for stateless service-to-service authentication.
- JWT pitfalls are well-documented: "alg: none", HMAC/RSA confusion, long expiry, and storing in localStorage. Understand them before choosing JWTs.
- Show users their active sessions and let them revoke unrecognized ones. This is both a security control and a transparency measure.