""" 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