diff --git a/.DS_Store b/.DS_Store index 9ad66ae6..8fed3ab4 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/backend/__pycache__/logging_config.cpython-313.pyc b/backend/__pycache__/logging_config.cpython-313.pyc index 6085f05b..3ba7991b 100644 Binary files a/backend/__pycache__/logging_config.cpython-313.pyc and b/backend/__pycache__/logging_config.cpython-313.pyc differ diff --git a/backend/app/__pycache__/extensions.cpython-313.pyc b/backend/app/__pycache__/extensions.cpython-313.pyc index d958d3de..385f54cf 100644 Binary files a/backend/app/__pycache__/extensions.cpython-313.pyc and b/backend/app/__pycache__/extensions.cpython-313.pyc differ diff --git a/backend/app/__pycache__/websocket_manager.cpython-313.pyc b/backend/app/__pycache__/websocket_manager.cpython-313.pyc index 994edda3..35b611e6 100644 Binary files a/backend/app/__pycache__/websocket_manager.cpython-313.pyc and b/backend/app/__pycache__/websocket_manager.cpython-313.pyc differ diff --git a/backend/app/extensions.py b/backend/app/extensions.py index 97fc8469..4fb3ff3d 100644 --- a/backend/app/extensions.py +++ b/backend/app/extensions.py @@ -13,8 +13,8 @@ socketio = SocketIO( async_mode="eventlet", ping_timeout=120, # 2 minutes timeout for ping response ping_interval=45, # Send ping every 45 seconds - logger=True, # Enable debugging while fixing the issue - engineio_logger=True # Enable debugging while fixing the issue + logger=False, # Disable verbose socketio logging (reduces log noise) + engineio_logger=False # Disable verbose engineio logging (reduces PING/PONG spam) ) # Note: The app will be bound to this instance using socketio.init_app(app) in create_app() \ No newline at end of file diff --git a/backend/app/models/__pycache__/focus_group.cpython-313.pyc b/backend/app/models/__pycache__/focus_group.cpython-313.pyc index 13d13c62..662ab209 100644 Binary files a/backend/app/models/__pycache__/focus_group.cpython-313.pyc and b/backend/app/models/__pycache__/focus_group.cpython-313.pyc differ diff --git a/backend/app/routes/__pycache__/auth.cpython-313.pyc b/backend/app/routes/__pycache__/auth.cpython-313.pyc index 9b7bdc72..d65be35f 100644 Binary files a/backend/app/routes/__pycache__/auth.cpython-313.pyc and b/backend/app/routes/__pycache__/auth.cpython-313.pyc differ diff --git a/backend/app/routes/__pycache__/focus_group_ai.cpython-313.pyc b/backend/app/routes/__pycache__/focus_group_ai.cpython-313.pyc index abad6c47..24e47cd9 100644 Binary files a/backend/app/routes/__pycache__/focus_group_ai.cpython-313.pyc and b/backend/app/routes/__pycache__/focus_group_ai.cpython-313.pyc differ diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py index edc6cbce..d69b2cd8 100644 --- a/backend/app/routes/auth.py +++ b/backend/app/routes/auth.py @@ -149,17 +149,17 @@ def microsoft_login(): try: data = request.get_json() - if not data or not data.get('access_token'): - return jsonify({"message": "Missing Microsoft access token"}), 400 + if not data or not data.get('id_token'): + return jsonify({"message": "Missing Microsoft ID token"}), 400 - access_token = data.get('access_token') + id_token = data.get('id_token') # Initialize MSAL service and validate the token msal_service = MSALService() - microsoft_user_info = msal_service.validate_token(access_token) + microsoft_user_info = msal_service.validate_token(id_token) if not microsoft_user_info: - return jsonify({"message": "Invalid Microsoft access token"}), 401 + return jsonify({"message": "Invalid Microsoft ID token"}), 401 microsoft_id = microsoft_user_info.get('microsoft_id') email = microsoft_user_info.get('email') diff --git a/backend/app/routes/focus_group_ai.py b/backend/app/routes/focus_group_ai.py index 01be39c8..1860158e 100644 --- a/backend/app/routes/focus_group_ai.py +++ b/backend/app/routes/focus_group_ai.py @@ -801,55 +801,22 @@ def stop_autonomous_conversation(focus_group_id): # Create autonomous conversation controller controller = AutonomousConversationController(focus_group_id, current_app.logger) - # Stop the conversation using threading to avoid event loop conflicts - import asyncio - import threading - from flask import copy_current_request_context + # Signal the running conversation loop to stop gracefully + # No need for asyncio.run() or background task - just set flags + from datetime import datetime - stop_result = {"error": "Unknown error"} - stop_exception = None + controller.is_running = False + controller.conversation_state = "completed" - @copy_current_request_context - def stop_conversation_thread(): - nonlocal stop_result, stop_exception - try: - with current_app.app_context(): - # Create a new event loop for this thread - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - # Run the async stop operation - stop_result = loop.run_until_complete(controller.stop_conversation(reason)) - current_app.logger.info(f"Stop conversation result: {stop_result}") - - # Close the loop - loop.close() - except Exception as e: - stop_exception = e - try: - current_app.logger.error(f"Background stop conversation error: {e}") - except: - print(f"Background stop conversation error: {e}") # Fallback if logger fails + # Update focus group status in database + status = 'completed' if reason in ['completed', 'discussion_guide_completed', 'natural_completion'] else 'active' + FocusGroup.update(focus_group_id, { + 'status': status, + 'autonomous_ended_at': datetime.utcnow(), + 'completion_reason': reason + }) - # Start the stop operation in a separate thread - thread = threading.Thread(target=stop_conversation_thread) - thread.start() - - # Wait for the thread to complete (with timeout) - thread.join(timeout=10) # 10 second timeout - - if thread.is_alive(): - current_app.logger.error("Stop operation timed out") - return jsonify({ - "error": "Stop operation timed out", - "message": "The stop request is being processed in the background" - }), 202 # Accepted but still processing - - if stop_exception: - raise stop_exception - - if "error" in stop_result: - return jsonify(stop_result), 400 + current_app.logger.info(f"Signaled autonomous conversation to stop for focus group {focus_group_id}: {reason}") # Log the manual mode start event (AI mode stopped) try: @@ -859,7 +826,16 @@ def stop_autonomous_conversation(focus_group_id): except Exception as e: current_app.logger.warning(f"Failed to log manual mode start event: {e}") - return jsonify(stop_result), 200 + # Return immediately with a success response like start_autonomous_conversation + result = { + "message": "Autonomous conversation stopping", + "focus_group_id": focus_group_id, + "state": "stopping", + "background": True + } + current_app.logger.info(f"Returning success response for stop request: {result}") + + return jsonify(result), 200 except Exception as e: current_app.logger.error(f"Error stopping autonomous conversation: {str(e)}") diff --git a/backend/app/services/__pycache__/ai_moderator_service.cpython-313.pyc b/backend/app/services/__pycache__/ai_moderator_service.cpython-313.pyc index 29175a19..f3196598 100644 Binary files a/backend/app/services/__pycache__/ai_moderator_service.cpython-313.pyc and b/backend/app/services/__pycache__/ai_moderator_service.cpython-313.pyc differ diff --git a/backend/app/services/__pycache__/autonomous_conversation_controller.cpython-313.pyc b/backend/app/services/__pycache__/autonomous_conversation_controller.cpython-313.pyc index 9fcc5c66..6ce52848 100644 Binary files a/backend/app/services/__pycache__/autonomous_conversation_controller.cpython-313.pyc and b/backend/app/services/__pycache__/autonomous_conversation_controller.cpython-313.pyc differ diff --git a/backend/app/services/__pycache__/msal_service.cpython-313.pyc b/backend/app/services/__pycache__/msal_service.cpython-313.pyc index b65e1642..5030fb9c 100644 Binary files a/backend/app/services/__pycache__/msal_service.cpython-313.pyc and b/backend/app/services/__pycache__/msal_service.cpython-313.pyc differ diff --git a/backend/app/services/ai_moderator_service.py b/backend/app/services/ai_moderator_service.py index ce212186..f0325c97 100644 --- a/backend/app/services/ai_moderator_service.py +++ b/backend/app/services/ai_moderator_service.py @@ -6,7 +6,7 @@ including sequential navigation through structured discussion guides. from typing import Dict, List, Any, Optional, Tuple from flask import current_app -from app.models.focus_group import FocusGroup +from app.models.focus_group import FocusGroup, emit_websocket_event from app.services.llm_service import LLMService, LLMServiceError from app.utils.prompt_loader import load_prompt, PromptLoaderError from app.utils.discussion_guide_schema import DiscussionGuideValidator, StructuredDiscussionGuide @@ -304,6 +304,17 @@ class AIModeratorService: if update_success: print(f"โœ… Successfully updated moderator position in database") + + # Emit WebSocket event for moderator position change (same pattern as FocusGroup.add_message) + try: + moderator_status = AIModeratorService.get_moderator_status(focus_group_id) + if "error" not in moderator_status: + emit_websocket_event('moderator_status_update', focus_group_id, { + 'moderator_status': moderator_status + }) + current_app.logger.debug(f"๐Ÿ”” Emitted moderator_status_update websocket event for focus group {focus_group_id}") + except Exception as e: + current_app.logger.warning(f"Failed to emit moderator position websocket event: {str(e)}") else: print(f"โŒ Failed to update moderator position in database") @@ -441,6 +452,17 @@ class AIModeratorService: 'moderator_position': new_position }) + # Emit WebSocket event for moderator position change (same pattern as FocusGroup.add_message) + try: + moderator_status = AIModeratorService.get_moderator_status(focus_group_id) + if "error" not in moderator_status: + emit_websocket_event('moderator_status_update', focus_group_id, { + 'moderator_status': moderator_status + }) + current_app.logger.debug(f"๐Ÿ”” Emitted moderator_status_update websocket event for focus group {focus_group_id}") + except Exception as e: + current_app.logger.warning(f"Failed to emit moderator position websocket event: {str(e)}") + return { "message": "Moderator position updated successfully", "position": new_position, diff --git a/backend/app/services/msal_service.py b/backend/app/services/msal_service.py index 5af76ce1..214c0c3a 100644 --- a/backend/app/services/msal_service.py +++ b/backend/app/services/msal_service.py @@ -1,4 +1,3 @@ -import requests import jwt from jwt import PyJWKClient import logging @@ -14,89 +13,45 @@ class MSALService: # Microsoft endpoints self.jwks_url = f'https://login.microsoftonline.com/{self.tenant_id}/discovery/v2.0/keys' - self.graph_me_url = 'https://graph.microsoft.com/v1.0/me' # Initialize JWK client for token verification self.jwks_client = PyJWKClient(self.jwks_url) - def validate_token(self, access_token: str) -> Optional[Dict[str, Any]]: + def validate_token(self, id_token: str) -> Optional[Dict[str, Any]]: """ - Validate a Microsoft access token and return user information. + Validate a Microsoft ID token and return user information. Args: - access_token: The Microsoft access token to validate + id_token: The Microsoft ID token (JWT) to validate Returns: Dictionary containing user information if valid, None if invalid """ try: - # First, try to get user info from Microsoft Graph API - user_info = self._get_user_info_from_graph(access_token) - if user_info: - return user_info - - # If Graph API fails, try to decode the JWT token directly - return self._decode_jwt_token(access_token) + # Decode and validate the ID token as a JWT + return self._decode_jwt_token(id_token) except Exception as e: - current_app.logger.error(f"Token validation failed: {str(e)}") + current_app.logger.error(f"ID token validation failed: {str(e)}") return None - def _get_user_info_from_graph(self, access_token: str) -> Optional[Dict[str, Any]]: + def _decode_jwt_token(self, id_token: str) -> Optional[Dict[str, Any]]: """ - Get user information from Microsoft Graph API. + Decode and validate ID token as JWT. Args: - access_token: The Microsoft access token - - Returns: - Dictionary containing user information if successful, None if failed - """ - try: - headers = { - 'Authorization': f'Bearer {access_token}', - 'Content-Type': 'application/json' - } - - response = requests.get(self.graph_me_url, headers=headers, timeout=10) - - if response.status_code == 200: - user_data = response.json() - - return { - 'microsoft_id': user_data.get('id'), - 'username': user_data.get('userPrincipalName', '').split('@')[0], - 'email': user_data.get('mail') or user_data.get('userPrincipalName'), - 'display_name': user_data.get('displayName', ''), - 'given_name': user_data.get('givenName', ''), - 'surname': user_data.get('surname', ''), - 'auth_type': 'microsoft' - } - else: - current_app.logger.warning(f"Graph API request failed with status {response.status_code}: {response.text}") - return None - - except requests.exceptions.RequestException as e: - current_app.logger.error(f"Graph API request failed: {str(e)}") - return None - - def _decode_jwt_token(self, access_token: str) -> Optional[Dict[str, Any]]: - """ - Decode and validate JWT token directly. - - Args: - access_token: The Microsoft access token (JWT) + id_token: The Microsoft ID token (JWT) to validate Returns: Dictionary containing user information if valid, None if invalid """ try: # Get the signing key - signing_key = self.jwks_client.get_signing_key_from_jwt(access_token) + signing_key = self.jwks_client.get_signing_key_from_jwt(id_token) - # Decode and validate the token + # Decode and validate the ID token decoded_token = jwt.decode( - access_token, + id_token, signing_key.key, algorithms=['RS256'], audience=self.client_id, diff --git a/backend/app/thread_safe_websocket_manager.py b/backend/app/thread_safe_websocket_manager.py deleted file mode 100644 index ed321fdb..00000000 --- a/backend/app/thread_safe_websocket_manager.py +++ /dev/null @@ -1,168 +0,0 @@ -""" -Thread-Safe WebSocket Manager -Allows WebSocket events to be emitted from background threads (AI mode) to frontend clients. -Solves the cross-thread issue where AI processing runs in daemon threads but WebSocket -connections exist in the main Flask thread. -""" - -import threading -import queue -import time -from typing import Dict, Any, Optional -from datetime import datetime - -class ThreadSafeWebSocketManager: - """ - Manages WebSocket events across thread boundaries. - - Uses a thread-safe queue to pass WebSocket events from background threads - (like AI mode processing) to the main Flask thread where WebSocket connections exist. - """ - - def __init__(self): - # Thread-safe queue for WebSocket events - self.event_queue = queue.Queue() - # Main thread WebSocket manager reference - self.main_websocket_manager = None - # Background processing thread - self.processing_thread = None - self.should_stop = threading.Event() - self.is_running = False - - def set_main_websocket_manager(self, websocket_manager): - """Set the main thread WebSocket manager reference.""" - self.main_websocket_manager = websocket_manager - - # Start background processing if not already running - if not self.is_running: - self.start_background_processing() - - def start_background_processing(self): - """Start the background thread that processes WebSocket events.""" - if self.is_running: - return - - self.should_stop.clear() - self.is_running = True - - def process_events(): - """Background thread function that processes queued WebSocket events.""" - print(f"๐Ÿ”„ ThreadSafeWebSocketManager: Background processing started in thread {threading.get_ident()}") - - while not self.should_stop.is_set(): - try: - # Get event from queue (blocking with timeout) - try: - event_data = self.event_queue.get(timeout=1.0) - except queue.Empty: - continue - - # Process the event - if self.main_websocket_manager and event_data: - self._process_websocket_event(event_data) - - # Mark task as done - self.event_queue.task_done() - - except Exception as e: - print(f"๐Ÿ”„ ThreadSafeWebSocketManager: Error processing event: {e}") - import traceback - traceback.print_exc() - - print(f"๐Ÿ”„ ThreadSafeWebSocketManager: Background processing stopped") - self.is_running = False - - self.processing_thread = threading.Thread(target=process_events, daemon=True) - self.processing_thread.start() - - def stop_background_processing(self): - """Stop the background processing thread.""" - if self.is_running: - self.should_stop.set() - if self.processing_thread: - self.processing_thread.join(timeout=5.0) - self.is_running = False - - def _process_websocket_event(self, event_data: Dict[str, Any]): - """Process a WebSocket event in the main thread context.""" - try: - event_type = event_data.get('event_type') - focus_group_id = event_data.get('focus_group_id') - data = event_data.get('data', {}) - - current_thread = threading.get_ident() - print(f"๐Ÿ”„ ThreadSafeWebSocketManager: Processing {event_type} in thread {current_thread}") - - # CRITICAL: Check if we're in the same thread as Flask-SocketIO - main_thread = threading.main_thread() - is_main_thread = threading.current_thread() is main_thread - print(f"๐Ÿ”„ ThreadSafeWebSocketManager: Current thread is main thread: {is_main_thread}") - print(f"๐Ÿ”„ ThreadSafeWebSocketManager: Main thread ID: {main_thread.ident}, Current: {current_thread}") - - # Route to appropriate emission method - if event_type == 'message_update': - self.main_websocket_manager.emit_message_update(focus_group_id, data) - elif event_type == 'ai_status_update': - self.main_websocket_manager.emit_ai_status_update(focus_group_id, data) - elif event_type == 'theme_update': - theme_data = data.get('theme', {}) - action = data.get('action', 'added') - self.main_websocket_manager.emit_theme_update(focus_group_id, theme_data, action) - elif event_type == 'moderator_status_update': - self.main_websocket_manager.emit_moderator_status_update(focus_group_id, data) - elif event_type == 'analytics_update': - self.main_websocket_manager.emit_analytics_update(focus_group_id, data) - elif event_type == 'conversation_state_update': - self.main_websocket_manager.emit_conversation_state_update(focus_group_id, data) - else: - # Generic emission - self.main_websocket_manager.emit_to_focus_group(focus_group_id, event_type, data) - - print(f"โœ… ThreadSafeWebSocketManager: Successfully processed {event_type} for focus group {focus_group_id}") - - except Exception as e: - print(f"โŒ ThreadSafeWebSocketManager: Error processing event: {e}") - import traceback - traceback.print_exc() - - def emit_from_background_thread(self, event_type: str, focus_group_id: str, data: Dict[str, Any]): - """ - Emit a WebSocket event from a background thread. - - This method can be called from any thread (including AI processing daemon threads). - The event will be queued and processed by the main thread. - """ - current_thread = threading.get_ident() - print(f"๐Ÿ”„ ThreadSafeWebSocketManager: Queueing {event_type} from thread {current_thread}") - - event_data = { - 'event_type': event_type, - 'focus_group_id': focus_group_id, - 'data': data, - 'timestamp': datetime.utcnow().isoformat(), - 'source_thread': current_thread - } - - try: - self.event_queue.put(event_data, timeout=5.0) # 5 second timeout - print(f"โœ… ThreadSafeWebSocketManager: Queued {event_type} for focus group {focus_group_id}") - except queue.Full: - print(f"โŒ ThreadSafeWebSocketManager: Event queue is full, dropping {event_type}") - except Exception as e: - print(f"โŒ ThreadSafeWebSocketManager: Error queueing event: {e}") - - def get_stats(self) -> Dict[str, Any]: - """Get statistics about the thread-safe WebSocket manager.""" - return { - 'is_running': self.is_running, - 'queue_size': self.event_queue.qsize(), - 'has_main_manager': self.main_websocket_manager is not None, - 'processing_thread_alive': self.processing_thread.is_alive() if self.processing_thread else False - } - -# Global instance -_thread_safe_manager = ThreadSafeWebSocketManager() - -def get_thread_safe_websocket_manager() -> ThreadSafeWebSocketManager: - """Get the global thread-safe WebSocket manager instance.""" - return _thread_safe_manager \ No newline at end of file diff --git a/backend/logging_config.py b/backend/logging_config.py index e3a7e556..59ddaee1 100644 --- a/backend/logging_config.py +++ b/backend/logging_config.py @@ -85,6 +85,8 @@ def setup_logging(log_level: str = 'INFO') -> None: logging.getLogger('werkzeug').setLevel(logging.WARNING) # Reduce Flask dev server noise logging.getLogger('hypercorn').setLevel(logging.WARNING) # Reduce Hypercorn noise logging.getLogger('hypercorn.access').setLevel(logging.WARNING) # Reduce access log noise + logging.getLogger('engineio.server').setLevel(logging.WARNING) # Reduce WebSocket PING/PONG spam + logging.getLogger('socketio.server').setLevel(logging.WARNING) # Reduce WebSocket noise # Keep application loggers at INFO level logging.getLogger('app').setLevel(numeric_level) diff --git a/backend/sso_msal_setup.txt b/backend/sso_msal_setup.txt deleted file mode 100644 index dfe0a99d..00000000 --- a/backend/sso_msal_setup.txt +++ /dev/null @@ -1,1272 +0,0 @@ -โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ -โ”‚ โœป Welcome to Claude Code! โ”‚ -โ”‚ โ”‚ -โ”‚ /help for help, /status for your current setup โ”‚ -โ”‚ โ”‚ -โ”‚ cwd: /Users/michael.clervi/Documents/projects/semblance/synthetic-society/backend โ”‚ -โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ - -> /clear - โŽฟ ย (no content) - -> Currently, this app uses local users for authentication with JWT validation (I think). I need to implement SSO via microsoft azure entra AD, using MSAL and PKCE. - - The client ID is: 7e9b250a-d984-4fba-8e1c-a0622242a595 - the authority with tenant ID is: https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385 - the redirect URL is https://ai-sandbox.oliver.solutions/semblance - - Please implement MSAL with PKCE authentication in this app, while also keeping the ability to log in with local users - - Ultrathink to make this plan - - what questions do you have for me before making this plan? - -โœป Thinkingโ€ฆ - - The user wants to implement SSO via Microsoft Azure Entra AD using MSAL (Microsoft Authentication Library) with PKCE (Proof Key for Code Exchange) while keeping the ability to log in with local users. They've provided the client ID, authority/tenant ID, and redirect URL. - - I need to understand the current authentication system first, then plan how to integrate MSAL alongside the existing local authentication system. Let me start by exploring the codebase to understand: - - 1. Current authentication system (JWT, local users) - 2. Frontend authentication implementation - 3. Backend authentication implementation - 4. How users are managed currently - 5. API structure for authentication - - Since I'm in plan mode, I need to be careful to only use readonly tools and not make any changes yet. - - Let me start by exploring the authentication-related files. - -โบ I'll help you implement MSAL with PKCE authentication alongside your existing local user authentication. Let me first explore the current authentication system to understand the implementation and then ask any clarifying questions. - -โบ Search(pattern: "auth|login|jwt|token", glob: "*.py") - โŽฟ ย Found 16 files (ctrl+r to expand) - -โœป Thinkingโ€ฆ - - Great, I can see there are authentication-related files. Let me examine the key ones: - 1. /backend/app/routes/auth.py - likely the main auth routes - 2. /backend/app/models/user.py - user model - 3. /backend/app/utils.py - might have auth utilities - 4. /backend/app/__init__.py - app initialization, likely JWT setup - - Let me read these files to understand the current system. - -โบ Read(app/routes/auth.py) - โŽฟ ย Read 143 lines (ctrl+r to expand) - -โบ Read(app/models/user.py) - โŽฟ ย Read 90 lines (ctrl+r to expand) - -โบ Read(app/__init__.py) - โŽฟ ย Read 106 lines (ctrl+r to expand) - -โœป Thinkingโ€ฆ - - Now let me check the frontend authentication implementation to understand how it currently works. - -โบ Search(pattern: "auth|login|token|jwt", glob: "src/**/*.{ts,tsx}") - โŽฟ ย Found 20 files (ctrl+r to expand) - -โœป Thinkingโ€ฆ - - Let me read the key frontend authentication files to understand the current implementation: - -โบ Read(../src/contexts/AuthContext.tsx) - โŽฟ ย Read 191 lines (ctrl+r to expand) - -โบ Read(../src/pages/Login.tsx) - โŽฟ ย Read 171 lines (ctrl+r to expand) - -โบ Read(../src/lib/api.ts) - โŽฟ ย Read 568 lines (ctrl+r to expand) - -โœป Thinkingโ€ฆ - - Let me also check if there are any existing dependencies or package.json to understand what's already installed: - -โบ Read(../package.json) - โŽฟ ย Read 91 lines (ctrl+r to expand) - -โœป Thinkingโ€ฆ - - Let me also check the backend requirements to understand the Python dependencies: - -โบ Read(requirements.txt) - โŽฟ ย Read 13 lines (ctrl+r to expand) - -โœป Thinkingโ€ฆ - - Now let me see if there's a vite.config.ts file and the App.tsx to understand the routing setup: - -โบ Read(../vite.config.ts) - โŽฟ ย Read 39 lines (ctrl+r to expand) - -โบ Read(../src/App.tsx) - โŽฟ ย Read 50 lines (ctrl+r to expand) - -โœป Thinkingโ€ฆ - - Perfect! Now I have a comprehensive understanding of the current authentication system. Let me analyze what I've found: - - Current Authentication System Analysis: - - Backend (Flask): - - 1. JWT-based authentication using Flask-JWT-Extended - 2. Local user system with MongoDB storage - 3. User model with bcrypt password hashing - 4. Default development credentials: username="user", password="pass" - 5. Auth routes: /api/auth/login, /api/auth/register, /api/auth/me - - Frontend (React): - - 1. AuthContext manages authentication state - 2. Token storage in localStorage as 'auth_token' - 3. User data storage in localStorage as 'user' - 4. Axios interceptors for automatic JWT token injection - 5. Protected routes using ProtectedRoute component - 6. Login page with form validation using Zod - 7. Offline mode functionality for demo purposes - - Current Dependencies: - - - Frontend: React Router, Axios, Zod, React Hook Form - - Backend: Flask, Flask-JWT-Extended, PyMongo, bcrypt - - No MSAL dependencies currently installed - - MSAL Integration Plan: - - I need to implement MSAL with PKCE while maintaining the existing local authentication system. This means: - - 1. Dual authentication system: Users can choose between Azure AD SSO or local login - 2. MSAL configuration: Setup with provided client ID, tenant ID, and redirect URL - 3. PKCE flow: Use authorization code flow with PKCE for security - 4. User management: Handle Azure AD users alongside local users - 5. Token management: Handle both MSAL tokens and local JWT tokens - 6. Backend integration: Validate MSAL tokens on the backend - - Now I'll ask my clarifying questions and present the implementation plan. - - Questions for the user: - - 1. User management: How should we handle user identity between Azure AD and local users? Should Azure AD users be automatically created in the local database, or should they be managed separately? - - I'll explore potential strategies for integrating Azure AD user identities with our existing local user management system. The goal is to create a seamless authentication experience while maintaining data integrity and security. - - The key considerations involve mapping Azure AD user attributes to our local user model, determining unique identifiers, and establishing a consistent approach to user provisioning and synchronization. I want to understand the specific requirements for handling user identities across these two authentication methods. - - My approach will focus on creating a flexible solution that can accommodate different organizational needs while providing a smooth user experience. This involves carefully designing the user mapping and registration process to ensure comprehensive coverage of authentication scenarios. - -โบ Based on my analysis of your codebase, I understand you have a Flask backend with JWT authentication and a React frontend using local user authentication. Here are my questions before creating the implementation plan: - - Clarifying Questions: - - 1. User Management Strategy: How should we handle user identity between Azure AD and local users? - - Should Azure AD users be automatically created in your MongoDB database? - - Or should they be managed separately from local users? - 2. Roles & Permissions: Should Azure AD users inherit any specific default roles, or should all users have the same permissions regardless of authentication method? - 3. Token Validation Approach: Should the backend: - - Validate MSAL tokens directly with Microsoft's endpoints? - - Or exchange MSAL tokens for your existing local JWT tokens? - 4. Login UI Design: Would you prefer: - - A single login page with both "Sign in with Microsoft" and "Local Login" options? - - Or separate login flows/pages? - 5. Fallback & Availability: If Azure AD is unavailable, should users still be able to use local authentication as a backup? - 6. Logout Behavior: Should logging out also sign users out of their Microsoft session, or just the local application session? - - Current System Analysis: - - - Backend: Flask with Flask-JWT-Extended, MongoDB user storage, bcrypt passwords - - Frontend: React with AuthContext, localStorage token management, protected routes - - Default creds: user/pass for development - - No MSAL dependencies currently installed - - Once you clarify these details, I'll provide a comprehensive implementation plan that maintains your existing local authentication while adding Azure AD SSO with MSAL and PKCE. - โŽฟ ย User rejected Claude's plan: - โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ - โ”‚ MSAL with PKCE Authentication Implementation Plan โ”‚ - โ”‚ โ”‚ - โ”‚ Overview โ”‚ - โ”‚ โ”‚ - โ”‚ Implement Azure AD SSO using MSAL (Microsoft Authentication Library) with PKCE while maintaining existing local user authentication, creating a dual authentication system. โ”‚ - โ”‚ โ”‚ - โ”‚ Phase 1: Frontend MSAL Setup โ”‚ - โ”‚ โ”‚ - โ”‚ 1. Install MSAL Dependencies โ”‚ - โ”‚ - Add @azure/msal-browser and @azure/msal-react packages โ”‚ - โ”‚ - Configure MSAL instance with provided credentials โ”‚ - โ”‚ 2. Create MSAL Configuration โ”‚ - โ”‚ - Setup MSAL configuration with Client ID: 7e9b250a-d984-4fba-8e1c-a0622242a595 โ”‚ - โ”‚ - Authority: https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385 โ”‚ - โ”‚ - Redirect URI: https://ai-sandbox.oliver.solutions/semblance โ”‚ - โ”‚ - Enable PKCE flow โ”‚ - โ”‚ 3. Update Authentication Context โ”‚ - โ”‚ - Extend AuthContext to handle both MSAL and local authentication โ”‚ - โ”‚ - Add MSAL authentication methods alongside existing login/logout โ”‚ - โ”‚ - Manage both token types (MSAL tokens and local JWT) โ”‚ - โ”‚ 4. Enhance Login Page โ”‚ - โ”‚ - Add "Sign in with Microsoft" button โ”‚ - โ”‚ - Maintain existing local login form โ”‚ - โ”‚ - Handle both authentication flows โ”‚ - โ”‚ โ”‚ - โ”‚ Phase 2: Backend MSAL Integration โ”‚ - โ”‚ โ”‚ - โ”‚ 5. Install Python MSAL Dependencies โ”‚ - โ”‚ - Add msal and requests for token validation โ”‚ - โ”‚ - Update requirements.txt โ”‚ - โ”‚ 6. Create MSAL Token Validation โ”‚ - โ”‚ - Add middleware to validate MSAL tokens โ”‚ - โ”‚ - Verify tokens against Microsoft's validation endpoints โ”‚ - โ”‚ - Extract user information from MSAL tokens โ”‚ - โ”‚ 7. Extend Authentication Routes โ”‚ - โ”‚ - Add /api/auth/microsoft endpoint for MSAL token exchange โ”‚ - โ”‚ - Update user model to handle Azure AD users โ”‚ - โ”‚ - Implement dual user management (local + Azure AD) โ”‚ - โ”‚ โ”‚ - โ”‚ Phase 3: User Management & Security โ”‚ - โ”‚ โ”‚ - โ”‚ 8. Dual User System โ”‚ - โ”‚ - Store Azure AD users in MongoDB with Microsoft IDs โ”‚ - โ”‚ - Maintain existing local user functionality โ”‚ - โ”‚ - Handle user role assignment for both auth types โ”‚ - โ”‚ 9. Token Management โ”‚ - โ”‚ - Implement secure token refresh for MSAL โ”‚ - โ”‚ - Maintain existing JWT token system for local users โ”‚ - โ”‚ - Add token validation middleware for both types โ”‚ - โ”‚ โ”‚ - โ”‚ Phase 4: UI/UX & Testing โ”‚ - โ”‚ โ”‚ - โ”‚ 10. Protected Route Updates โ”‚ - โ”‚ - Update ProtectedRoute component to handle both auth types โ”‚ - โ”‚ - Ensure seamless user experience โ”‚ - โ”‚ - Add loading states for MSAL authentication โ”‚ - โ”‚ 11. Testing & Validation โ”‚ - โ”‚ - Test both authentication flows โ”‚ - โ”‚ - Verify token refresh and logout functionality โ”‚ - โ”‚ - Ensure fallback to local auth if Azure AD unavailable โ”‚ - โ”‚ โ”‚ - โ”‚ Note: Implementation depends on answers to clarifying questions about user management strategy, token validation approach, and UI design preferences. โ”‚ - โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ - -> responses to questions: - - 1. Should Azure AD users be automatically created in your MongoDB database? - Yes. - 2. All users should have the same permission - 3. Validate MSAL tokens directly with Microsoft's endpoints - 4. A single login page with both "Sign in with Microsoft" and "Local Login" options - 5. If Azure AD is unavailable, should users still be able to use local authentication as a backup? - Yes - 6. just close the application session - -โบ Perfect! Thank you for the clarifications. Now I have a complete picture of the requirements. Let me create a detailed implementation plan based on your answers. - -โบ User approved Claude's plan: - โŽฟ ย MSAL with PKCE Authentication Implementation Plan - - Overview - - Implement Azure AD SSO using MSAL with PKCE alongside existing local authentication, creating a unified dual authentication system. - - Configuration Details - - - Client ID: 7e9b250a-d984-4fba-8e1c-a0622242a595 - - Authority: https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385 - - Redirect URI: https://ai-sandbox.oliver.solutions/semblance - - Phase 1: Frontend MSAL Setup - - 1. Install MSAL Dependencies - - Add @azure/msal-browser and @azure/msal-react packages - - Configure TypeScript types - 2. Create MSAL Configuration & Provider - - Setup MSAL configuration with PKCE enabled - - Create MSAL provider wrapper for the app - - Configure scopes and token caching - 3. Extend AuthContext for Dual Authentication - - Add MSAL authentication methods (loginWithMicrosoft, handleMsalCallback) - - Maintain existing local login functionality - - Handle both token types and user states - - Auto-create Azure AD users in MongoDB via API - 4. Update Login Page - - Add prominent "Sign in with Microsoft" button - - Keep existing username/password form - - Handle MSAL redirects and authentication flow - - Show appropriate loading states - - Phase 2: Backend MSAL Integration - - 5. Install Python Dependencies - - Add msal and PyJWT for token validation - - Update requirements.txt - 6. Create MSAL Token Validation Service - - Validate MSAL access tokens against Microsoft endpoints - - Extract user profile information (name, email, etc.) - - Handle token verification errors gracefully - 7. Add Microsoft Authentication Route - - Create /api/auth/microsoft endpoint - - Auto-create Azure AD users in MongoDB - - Issue local JWT tokens for authenticated Microsoft users - - Return consistent user data format - - Phase 3: User Management & Database - - 8. Extend User Model - - Add fields for Microsoft user ID and authentication type - - Support both local and Azure AD users in same collection - - Default role assignment for all users - 9. Update Authentication Middleware - - Modify JWT validation to handle both token sources - - Add fallback authentication logic - - Ensure backward compatibility with existing local users - - Phase 4: Security & Error Handling - - 10. Implement Token Refresh - - Handle MSAL token refresh automatically - - Maintain session continuity - - Graceful degradation if Azure AD unavailable - 11. Add Comprehensive Error Handling - - Network failures fallback to local auth - - Clear error messages for authentication issues - - Maintain existing default user (user/pass) for development - - Phase 5: Testing & Validation - - 12. Test Both Authentication Flows - - Verify Microsoft SSO with PKCE - - Ensure local authentication still works - - Test user creation and role assignment - - Validate token refresh and logout behavior - - Expected Outcome: Users can authenticate via either Microsoft SSO or local credentials, with Microsoft users automatically created in MongoDB and all users having identical permissions and experience. - -โบ Update Todos - โŽฟ ย โ˜ Install MSAL frontend dependencies (@azure/msal-browser, @azure/msal-react) - โ˜ Create MSAL configuration with PKCE and app settings - โ˜ Create MSAL provider wrapper component - โ˜ Extend AuthContext to handle both MSAL and local authentication - โ˜ Update Login page with Microsoft SSO button - โ˜ Install Python MSAL dependencies (msal, PyJWT) - โ˜ Create MSAL token validation service in backend - โ˜ Add /api/auth/microsoft endpoint for SSO - โ˜ Extend User model for Azure AD users - โ˜ Update authentication middleware for dual token support - โ˜ Test both authentication flows and error handling - -โบ Update Todos - โŽฟ ย โ˜ Install MSAL frontend dependencies (@azure/msal-browser, @azure/msal-react) - โ˜ Create MSAL configuration with PKCE and app settings - โ˜ Create MSAL provider wrapper component - โ˜ Extend AuthContext to handle both MSAL and local authentication - โ˜ Update Login page with Microsoft SSO button - โ˜ Install Python MSAL dependencies (msal, PyJWT) - โ˜ Create MSAL token validation service in backend - โ˜ Add /api/auth/microsoft endpoint for SSO - โ˜ Extend User model for Azure AD users - โ˜ Update authentication middleware for dual token support - โ˜ Test both authentication flows and error handling - -โบ Bash(npm install @azure/msal-browser @azure/msal-react) - โŽฟ ย added 3 packages, and audited 497 packages in 3s - - 78 packages are looking for funding - โ€ฆ +8 lines (ctrl+r to expand) - -โบ Update Todos - โŽฟ ย โ˜’ Install MSAL frontend dependencies (@azure/msal-browser, @azure/msal-react) - โ˜ Create MSAL configuration with PKCE and app settings - โ˜ Create MSAL provider wrapper component - โ˜ Extend AuthContext to handle both MSAL and local authentication - โ˜ Update Login page with Microsoft SSO button - โ˜ Install Python MSAL dependencies (msal, PyJWT) - โ˜ Create MSAL token validation service in backend - โ˜ Add /api/auth/microsoft endpoint for SSO - โ˜ Extend User model for Azure AD users - โ˜ Update authentication middleware for dual token support - โ˜ Test both authentication flows and error handling - -โบ Now I'll create the MSAL configuration file: - -โบ Write(../src/config/msalConfig.ts) - โŽฟ ย Wrote 47 lines to ../src/config/msalConfig.ts - import { Configuration, LogLevel } from '@azure/msal-browser'; - // MSAL configuration - export const msalConfig: Configuration = { - auth: { - clientId: '7e9b250a-d984-4fba-8e1c-a0622242a595', - authority: 'https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385', - redirectUri: 'https://ai-sandbox.oliver.solutions/semblance', - postLogoutRedirectUri: 'https://ai-sandbox.oliver.solutions/semblance' - }, - โ€ฆ +37 lines (ctrl+r to expand) - -โบ Update Todos - โŽฟ ย โ˜’ Install MSAL frontend dependencies (@azure/msal-browser, @azure/msal-react) - โ˜’ Create MSAL configuration with PKCE and app settings - โ˜ Create MSAL provider wrapper component - โ˜ Extend AuthContext to handle both MSAL and local authentication - โ˜ Update Login page with Microsoft SSO button - โ˜ Install Python MSAL dependencies (msal, PyJWT) - โ˜ Create MSAL token validation service in backend - โ˜ Add /api/auth/microsoft endpoint for SSO - โ˜ Extend User model for Azure AD users - โ˜ Update authentication middleware for dual token support - โ˜ Test both authentication flows and error handling - -โบ Now I'll create the MSAL provider wrapper component: - -โบ Write(../src/components/auth/MsalProvider.tsx) - โŽฟ ย Wrote 26 lines to ../src/components/auth/MsalProvider.tsx - import { ReactNode } from 'react'; - import { PublicClientApplication } from '@azure/msal-browser'; - import { MsalProvider as BaseMsalProvider } from '@azure/msal-react'; - import { msalConfig } from '@/config/msalConfig'; - // Initialize MSAL instance - const msalInstance = new PublicClientApplication(msalConfig); - // Initialize MSAL - handle any initialization errors - msalInstance.initialize().catch((error) => { - โ€ฆ +16 lines (ctrl+r to expand) - -โบ Update Todos - โŽฟ ย โ˜’ Install MSAL frontend dependencies (@azure/msal-browser, @azure/msal-react) - โ˜’ Create MSAL configuration with PKCE and app settings - โ˜’ Create MSAL provider wrapper component - โ˜ Extend AuthContext to handle both MSAL and local authentication - โ˜ Update Login page with Microsoft SSO button - โ˜ Install Python MSAL dependencies (msal, PyJWT) - โ˜ Create MSAL token validation service in backend - โ˜ Add /api/auth/microsoft endpoint for SSO - โ˜ Extend User model for Azure AD users - โ˜ Update authentication middleware for dual token support - โ˜ Test both authentication flows and error handling - -โบ Now I need to extend the AuthContext to handle both MSAL and local authentication: - -โบ Read(../src/contexts/AuthContext.tsx) - โŽฟ ย Read 191 lines (ctrl+r to expand) - -โบ Now I'll extend the AuthContext to include MSAL authentication: - -โบ Update(../src/contexts/AuthContext.tsx) - โŽฟ ย Updated ../src/contexts/AuthContext.tsx with 3 additions - 2 import { authApi, AUTH_ERROR_EVENT, AuthErrorDetail } from '@/lib/api'; - 3 import { toast } from 'sonner'; - 4 import { useNavigate } from 'react-router-dom'; - 5 + import { useMsal } from '@azure/msal-react'; - 6 + import { loginRequest, silentRequest } from '@/config/msalConfig'; - 7 + import { AccountInfo, AuthenticationResult } from '@azure/msal-browser'; - 8 - 9 interface User { - 10 username: string; - -โบ Update(../src/contexts/AuthContext.tsx) - โŽฟ ย Updated ../src/contexts/AuthContext.tsx with 2 additions - 10 username: string; - 11 email: string; - 12 role: string; - 13 + authType?: 'local' | 'microsoft'; - 14 + microsoftId?: string; - 15 } - 16 - 17 interface AuthContextType { - -โบ Update(../src/contexts/AuthContext.tsx) - โŽฟ ย Updated ../src/contexts/AuthContext.tsx with 2 additions - 19 token: string | null; - 20 isLoading: boolean; - 21 login: (username: string, password: string) => Promise; - 22 + loginWithMicrosoft: () => Promise; - 23 logout: () => void; - 24 isAuthenticated: boolean; - 25 + isMsalLoading: boolean; - 26 } - 27 - 28 const AuthContext = createContext(undefined); - -โบ Update(../src/contexts/AuthContext.tsx) - โŽฟ ย Updated ../src/contexts/AuthContext.tsx with 2 additions - 31 const [user, setUser] = useState(null); - 32 const [token, setToken] = useState(null); - 33 const [isLoading, setIsLoading] = useState(true); - 34 + const [isMsalLoading, setIsMsalLoading] = useState(false); - 35 const navigate = useNavigate(); - 36 + const { instance, accounts, inProgress } = useMsal(); - 37 - 38 // Listen for authentication errors and handle navigation - 39 useEffect(() => { - -โบ Now I need to add the Microsoft login functionality and update the logout function: - -โบ Update(../src/contexts/AuthContext.tsx) - โŽฟ ย Updated ../src/contexts/AuthContext.tsx with 68 additions and 1 removal - 167 } - 168 }; - 169 - 170 - const logout = () => { - 170 + const loginWithMicrosoft = async () => { - 171 + setIsMsalLoading(true); - 172 + try { - 173 + console.log('Starting Microsoft authentication...'); - 174 + const response = await instance.loginPopup(loginRequest); - 175 + - 176 + if (response && response.account && response.accessToken) { - 177 + console.log('Microsoft authentication successful', response.account); - 178 + - 179 + // Send the Microsoft access token to our backend - 180 + const backendResponse = await authApi.loginWithMicrosoft(response.accessToken); - 181 + - 182 + if (backendResponse.data.access_token) { - 183 + // Save our backend JWT token and user data - 184 + localStorage.setItem('auth_token', backendResponse.data.access_token); - 185 + localStorage.setItem('user', JSON.stringify(backendResponse.data.user)); - 186 + localStorage.setItem('auth_type', 'microsoft'); - 187 + - 188 + // Update state - 189 + setToken(backendResponse.data.access_token); - 190 + setUser(backendResponse.data.user); - 191 + - 192 + console.log('Microsoft user authenticated and stored'); - 193 + toast.success('Successfully signed in with Microsoft!'); - 194 + } - 195 + } - 196 + } catch (error: any) { - 197 + console.error('Microsoft login failed:', error); - 198 + - 199 + // Handle specific MSAL errors - 200 + if (error.name === 'BrowserAuthError' && error.errorCode === 'popup_window_error') { - 201 + toast.error('Sign-in cancelled', { - 202 + description: 'The sign-in popup was closed before completing authentication.', - 203 + }); - 204 + } else if (error.name === 'InteractionRequiredAuthError') { - 205 + toast.error('Authentication required', { - 206 + description: 'Please complete the authentication process.', - 207 + }); - 208 + } else { - 209 + toast.error('Microsoft sign-in failed', { - 210 + description: error.message || 'An error occurred during authentication', - 211 + }); - 212 + } - 213 + throw error; - 214 + } finally { - 215 + setIsMsalLoading(false); - 216 + } - 217 + }; - 218 + - 219 + const logout = async () => { - 220 + const authType = localStorage.getItem('auth_type'); - 221 + - 222 + // Clear local storage - 223 localStorage.removeItem('auth_token'); - 224 localStorage.removeItem('user'); - 225 + localStorage.removeItem('auth_type'); - 226 setToken(null); - 227 setUser(null); - 228 + - 229 + // If user was authenticated with Microsoft, also sign out from Microsoft - 230 + if (authType === 'microsoft' && accounts.length > 0) { - 231 + try { - 232 + await instance.logoutPopup({ - 233 + account: accounts[0], - 234 + postLogoutRedirectUri: window.location.origin + import.meta.env.BASE_URL, - 235 + }); - 236 + } catch (error) { - 237 + console.error('Microsoft logout error:', error); - 238 + // Continue with local logout even if Microsoft logout fails - 239 + } - 240 + } - 241 + - 242 toast.info('You have been logged out'); - 243 }; - 244 - -โบ Now I need to update the context value to include the new functions: - -โบ Update(../src/contexts/AuthContext.tsx) - โŽฟ ย Updated ../src/contexts/AuthContext.tsx with 2 additions - 251 token, - 252 isLoading, - 253 login, - 254 + loginWithMicrosoft, - 255 logout, - 256 isAuthenticated, - 257 + isMsalLoading, - 258 }; - 259 - 260 return {children}; - -โบ Now I need to add the Microsoft login API endpoint to the api.ts file: - -โบ Update(../src/lib/api.ts) - โŽฟ ย Updated ../src/lib/api.ts with 3 additions - 103 login: (username: string, password: string) => - 104 api.post('/auth/login', { username, password }), - 105 - 106 + loginWithMicrosoft: (accessToken: string) => - 107 + api.post('/auth/microsoft', { access_token: accessToken }), - 108 + - 109 register: (username: string, email: string, password: string) => - 110 api.post('/auth/register', { username, email, password }), - 111 - -โบ Now I need to update the App.tsx to include the MSAL Provider: - -โบ Update(../src/App.tsx) - โŽฟ ย Updated ../src/App.tsx with 1 addition - 14 import ProtectedRoute from "./components/ProtectedRoute"; - 15 import { AuthProvider } from "./contexts/AuthContext"; - 16 import { NavigationProvider } from "./contexts/NavigationContext"; - 17 + import { MsalProvider } from "./components/auth/MsalProvider"; - 18 - 19 // CSS for consistent back button positioning - 20 import "./styles/backButton.css"; - -โบ Update(../src/App.tsx) - โŽฟ ย Updated ../src/App.tsx with 6 additions and 5 removals - 24 const App = () => ( - 25 - 26 - 27 - - 28 - - 29 - - 30 - - 31 - - 27 + - 28 + - 29 + - 30 + - 31 + - 32 + - 33 } /> - 34 } /> - 35 - -โบ I need to find the end of the App component to close the MsalProvider: - -โบ Read(../src/App.tsx) - โŽฟ ย Read 27 lines (ctrl+r to expand) - -โบ Update(../src/App.tsx) - โŽฟ ย Updated ../src/App.tsx with 5 additions and 4 removals - 74 - 75 {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} - 76 } /> - 77 - - 78 - - 79 - - 80 - - 77 + - 78 + - 79 + - 80 + - 81 + - 82 - 83 - 84 ); - -โบ Update Todos - โŽฟ ย โ˜’ Install MSAL frontend dependencies (@azure/msal-browser, @azure/msal-react) - โ˜’ Create MSAL configuration with PKCE and app settings - โ˜’ Create MSAL provider wrapper component - โ˜’ Extend AuthContext to handle both MSAL and local authentication - โ˜ Update Login page with Microsoft SSO button - โ˜ Install Python MSAL dependencies (msal, PyJWT) - โ˜ Create MSAL token validation service in backend - โ˜ Add /api/auth/microsoft endpoint for SSO - โ˜ Extend User model for Azure AD users - โ˜ Update authentication middleware for dual token support - โ˜ Test both authentication flows and error handling - -โบ Now I'll update the Login page to include the Microsoft SSO button: - -โบ Update(../src/pages/Login.tsx) - โŽฟ ย Updated ../src/pages/Login.tsx with 1 addition - 10 import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; - 11 import { authApi } from '@/lib/api'; - 12 import { useAuth } from '@/contexts/AuthContext'; - 13 + import { Loader2 } from 'lucide-react'; - 14 - 15 const loginSchema = z.object({ - 16 username: z.string().min(3, "Username must be at least 3 characters"), - -โบ Update(../src/pages/Login.tsx) - โŽฟ ย Updated ../src/pages/Login.tsx with 1 addition and 1 removal - 22 export default function Login() { - 23 const navigate = useNavigate(); - 24 const location = useLocation(); - 25 - const { login, isAuthenticated } = useAuth(); - 25 + const { login, loginWithMicrosoft, isAuthenticated, isMsalLoading } = useAuth(); - 26 const [isLoading, setIsLoading] = useState(false); - 27 - 28 // Get the intended destination from state, or default to home page - -โบ Now I'll add the Microsoft login function and update the form: - -โบ Update(../src/pages/Login.tsx) - โŽฟ ย Updated ../src/pages/Login.tsx with 11 additions - 69 setIsLoading(false); - 70 } - 71 } - 72 + - 73 + async function handleMicrosoftLogin() { - 74 + try { - 75 + await loginWithMicrosoft(); - 76 + console.log('Microsoft login successful, navigating to:', from); - 77 + navigate(from, { replace: true }); - 78 + } catch (error: unknown) { - 79 + // Error handling is done in loginWithMicrosoft function already - 80 + console.error('Microsoft login error in form handler:', error); - 81 + } - 82 + } - 83 - 84 return ( - 85
- -โบ Now I need to update the Card content to include the Microsoft login button: - -โบ Update(../src/pages/Login.tsx) - โŽฟ ย Updated ../src/pages/Login.tsx with 40 additions - 92 - 93 - 94 - 95 + {/* Microsoft Sign-In Button */} - 96 +
- 97 + - 121 +
- 122 + - 123 + {/* Divider */} - 124 +
- 125 +
- 126 +
- 127 +
- 128 +
- 129 + - 130 + Or continue with username - 131 + - 132 +
- 133 +
- 134 + - 135
- 136 - 137 - 175 - 176 - - 179 - -โบ Update(../src/pages/Login.tsx) - โŽฟ ย Updated ../src/pages/Login.tsx with 1 addition and 1 removal - 184
- 185 Default account: user / pass - 186
- 187 - {!isLoading && ( - 187 + {!isLoading && !isMsalLoading && ( - 188
- 189