lumaspeech-backend / utils /session_manager.py
yasmine hemmati
Sync backend code with latest changes
637ad6d
Raw
History Blame Contribute Delete
6.95 kB
"""
Session Manager - Handles automatic session timeout for HIPAA compliance
"""
import jwt
from datetime import datetime, timedelta
from functools import wraps
from flask import request, jsonify
import os
import logging
logger = logging.getLogger(__name__)
class SessionManager:
"""
Manages user sessions with automatic timeout for HIPAA compliance
"""
# Inactivity timeout - 2 hours for better UX while still being secure
# Note: HIPAA recommends 15 minutes for clinical workstations with direct PHI access,
# but web apps with less sensitive access can use longer timeouts
INACTIVITY_TIMEOUT = timedelta(hours=2)
# Maximum session duration - 24 hours (user must re-login daily)
MAX_SESSION_DURATION = timedelta(hours=24)
@staticmethod
def create_token(user_id, role):
"""
Create a JWT token with session management
"""
now = datetime.utcnow()
# Token expires after inactivity timeout
expiration = now + SessionManager.INACTIVITY_TIMEOUT
# But never exceed max session duration
max_expiration = now + SessionManager.MAX_SESSION_DURATION
if expiration > max_expiration:
expiration = max_expiration
payload = {
'sub': str(user_id),
'role': role,
'iat': now,
'exp': expiration,
'last_activity': now.timestamp(),
'session_start': now.timestamp()
}
token = jwt.encode(
payload,
os.environ.get('JWT_SECRET_KEY', 'your-secret-key-change-this'),
algorithm='HS256'
)
logger.info(f"Session created for user {user_id} with role {role}")
return token
@staticmethod
def refresh_token(token):
"""
Refresh token on activity to extend session
"""
try:
# Decode without verification first to get payload
payload = jwt.decode(
token,
os.environ.get('JWT_SECRET_KEY', 'your-secret-key-change-this'),
algorithms=['HS256'],
options={"verify_exp": False}
)
now = datetime.utcnow()
session_start = datetime.fromtimestamp(payload['session_start'])
# Check if session has exceeded maximum duration
if now - session_start > SessionManager.MAX_SESSION_DURATION:
logger.warning(f"Session exceeded maximum duration for user {payload['sub']}")
return None
# Check if session has been inactive too long
last_activity = datetime.fromtimestamp(payload['last_activity'])
if now - last_activity > SessionManager.INACTIVITY_TIMEOUT:
logger.warning(f"Session timeout due to inactivity for user {payload['sub']}")
return None
# Create refreshed token
new_expiration = now + SessionManager.INACTIVITY_TIMEOUT
max_expiration = session_start + SessionManager.MAX_SESSION_DURATION
if new_expiration > max_expiration:
new_expiration = max_expiration
payload['exp'] = new_expiration
payload['last_activity'] = now.timestamp()
new_token = jwt.encode(
payload,
os.environ.get('JWT_SECRET_KEY', 'your-secret-key-change-this'),
algorithm='HS256'
)
return new_token
except jwt.ExpiredSignatureError:
logger.warning("Token expired during refresh attempt")
return None
except Exception as e:
logger.error(f"Error refreshing token: {str(e)}")
return None
@staticmethod
def validate_session(token):
"""
Validate if session is still active
"""
try:
payload = jwt.decode(
token,
os.environ.get('JWT_SECRET_KEY', 'your-secret-key-change-this'),
algorithms=['HS256']
)
now = datetime.utcnow()
last_activity = datetime.fromtimestamp(payload.get('last_activity', 0))
# Check inactivity timeout
if now - last_activity > SessionManager.INACTIVITY_TIMEOUT:
return False, "Session expired due to inactivity"
return True, "Session valid"
except jwt.ExpiredSignatureError:
return False, "Session expired"
except Exception as e:
logger.error(f"Session validation error: {str(e)}")
return False, "Invalid session"
def require_active_session(f):
"""
Decorator to ensure session is active and refresh it on activity
"""
@wraps(f)
def decorated_function(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': 'No valid authorization header'}), 401
token = auth_header.split(' ')[1]
# Validate session
is_valid, message = SessionManager.validate_session(token)
if not is_valid:
logger.warning(f"Session validation failed: {message}")
return jsonify({'error': message, 'session_expired': True}), 401
# Refresh token on activity
new_token = SessionManager.refresh_token(token)
if not new_token:
return jsonify({'error': 'Session expired', 'session_expired': True}), 401
# Add new token to response headers
response = f(*args, **kwargs)
if hasattr(response, 'headers'):
response.headers['X-New-Token'] = new_token
return response
return decorated_function
def get_session_info(token):
"""
Get information about current session
"""
try:
payload = jwt.decode(
token,
os.environ.get('JWT_SECRET_KEY', 'your-secret-key-change-this'),
algorithms=['HS256'],
options={"verify_exp": False}
)
now = datetime.utcnow()
last_activity = datetime.fromtimestamp(payload.get('last_activity', 0))
session_start = datetime.fromtimestamp(payload.get('session_start', 0))
time_until_timeout = SessionManager.INACTIVITY_TIMEOUT - (now - last_activity)
session_duration = now - session_start
return {
'user_id': payload['sub'],
'role': payload['role'],
'session_start': session_start.isoformat(),
'last_activity': last_activity.isoformat(),
'time_until_timeout': max(0, time_until_timeout.total_seconds()),
'session_duration': session_duration.total_seconds(),
'max_session_duration': SessionManager.MAX_SESSION_DURATION.total_seconds()
}
except Exception as e:
logger.error(f"Error getting session info: {str(e)}")
return None