Spaces:
Runtime error
Runtime error
| """ | |
| 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) | |
| 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 | |
| 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 | |
| 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 | |
| """ | |
| 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 |