Session Management Security
Introduction
Session management is the mechanism by which a web application maintains state across multiple requests from the same user. Flawed session management leads to session hijacking, fixation, and replay attacks. A robust session management strategy must address token generation, storage, transmission, rotation, and invalidation.
JWT vs Opaque Tokens
JSON Web Tokens
JWTs are self-contained tokens carrying claims in a signed JSON payload. They enable stateless authentication — the server validates the signature without database lookups.
import jwt
from datetime import datetime, timedelta
# Generate a JWT access token
def create_access_token(user_id, roles, secret_key):
payload = {
'sub': user_id,
'roles': roles,
'iat': datetime.utcnow(),
'exp': datetime.utcnow() + timedelta(minutes=15),
'jti': secrets.token_hex(16), # Unique token ID for revocation
'type': 'access'
}
return jwt.encode(payload, secret_key, algorithm='HS256')
# Generate a refresh token
def create_refresh_token(user_id, secret_key):
payload = {
'sub': user_id,
'exp': datetime.utcnow() + timedelta(days=7),
'jti': secrets.token_hex(16),
'type': 'refresh'
}
return jwt.encode(payload, secret_key, algorithm='HS256')
# Verify and decode
def verify_token(token, secret_key):
try:
payload = jwt.decode(token, secret_key, algorithms=['HS256'])
# Check if token is revoked (check jti against blocklist)
if is_revoked(payload['jti']):
raise jwt.InvalidTokenError('Token revoked')
return payload
except jwt.ExpiredSignatureError:
raise
except jwt.InvalidTokenError:
raise
JWT advantages: stateless, self-validating, carries user claims. Disadvantages: cannot revoke without a blocklist, payload is signed not encrypted (unless JWE), token size can be large.
Opaque Tokens
Opaque tokens are random strings stored server-side in a session store. The client presents the token, and the server looks up the associated session data.
import secrets
import redis
class OpaqueTokenManager:
def __init__(self, redis_client):
self.redis = redis_client
self.token_length = 32
def create_session(self, user_id, claims, ttl_seconds=3600):
token = secrets.token_hex(self.token_length)
session_key = f"session:{token}"
session_data = {
'user_id': user_id,
'claims': claims,
'created_at': datetime.utcnow().isoformat(),
'last_activity': datetime.utcnow().isoformat()
}
self.redis.setex(session_key, ttl_seconds, json.dumps(session_data))
return token
def validate_session(self, token):
session_key = f"session:{token}"
data = self.redis.get(session_key)
if not data:
return None
session = json.loads(data)
# Update last activity
session['last_activity'] = datetime.utcnow().isoformat()
self.redis.setex(session_key, 3600, json.dumps(session))
return session
def revoke_session(self, token):
self.redis.delete(f"session:{token}")
def revoke_all_user_sessions(self, user_id):
# Pattern-based revocation
for key in self.redis.scan_iter(f"session:*"):
data = json.loads(self.redis.get(key))
if data['user_id'] == user_id:
self.redis.delete(key)
Token Rotation
Rotating tokens limits the window of opportunity for stolen tokens.
# Refresh token rotation
def refresh_access_token(refresh_token, secret_key):
payload = verify_token(refresh_token, secret_key)
if payload['type'] != 'refresh':
raise InvalidTokenError('Not a refresh token')
# Revoke old refresh token
revoke_token(payload['jti'])
# Issue new tokens
new_access = create_access_token(payload['sub'], payload['roles'], secret_key)
new_refresh = create_refresh_token(payload['sub'], secret_key)
return {'access_token': new_access, 'refresh_token': new_refresh}
Secure Cookies
For web applications, cookies remain the primary session token transport mechanism.
from flask import make_response
def set_session_cookie(response, token):
response.set_cookie(
'session_token',
value=token,
httponly=True, # Not accessible via JavaScript
secure=True, # Only over HTTPS
samesite='Strict', # Not sent with cross-origin requests
max_age=3600,
path='/'
)
# Modern recommended cookie configuration
session_cookie_config = {
'http_only': True,
'secure': True,
'same_site': 'Lax',
'max_age': 3600, # 1 hour
'domain': 'app.example.com',
'path': '/',
# __Host- prefix for cookie name ensures path=/ and no domain attribute
'name': '__Host-session'
}
Session Fixation Prevention
Session fixation occurs when an attacker forces a victim to use a known session identifier. Mitigation: regenerate the session ID after authentication.
def login(request, username, password):
if authenticate(username, password):
# Regenerate session ID after successful login
old_session = request.session
request.session.regenerate() # New session ID, same data
# Copy relevant data and invalidate old session
request.session['user_id'] = get_user_id(username)
request.session['authenticated'] = True
request.session['auth_time'] = datetime.utcnow().isoformat()
# Invalidate old session in store
session_store.delete(old_session.session_key)
return redirect('/dashboard')
Session Timeout Strategies
session_timeouts = {
'idle_timeout': timedelta(minutes=30), # Absolute: no activity for 30 min
'absolute_timeout': timedelta(hours=8), # Absolute: max session lifetime
}
def check_session_timeout(session):
now = datetime.utcnow()
# Idle timeout
last_activity = datetime.fromisoformat(session['last_activity'])
if now - last_activity > session_timeouts['idle_timeout']:
return {'expired': True, 'reason': 'idle_timeout'}
# Absolute timeout
auth_time = datetime.fromisoformat(session['auth_time'])
if now - auth_time > session_timeouts['absolute_timeout']:
return {'expired': True, 'reason': 'absolute_timeout'}
return {'expired': False}
Conclusion
Secure session management requires defense in depth. Use JWTs for stateless distributed systems with short expiration times, or opaque tokens for server-side control with instant revocation. Always use `HttpOnly`, `Secure`, and `SameSite` attributes on session cookies, regenerate session IDs after login, enforce idle and absolute timeouts, and implement proper token rotation for refresh flows.