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.
Enjoy this article? Share your thoughts, questions, or experiences in the comments below — your insights help other readers too.
Join the discussion ↓