Introduction

Session Management Security

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.