import jwt from jwt import PyJWKClient import logging from typing import Optional, Dict, Any from quart import current_app class MSALService: """Service for validating Microsoft MSAL tokens and extracting user information.""" def __init__(self): import os self.tenant_id = os.environ.get('MSAL_TENANT_ID') self.client_id = os.environ.get('MSAL_CLIENT_ID') if not self.tenant_id or not self.client_id: raise RuntimeError("MSAL_TENANT_ID and MSAL_CLIENT_ID environment variables must be set") # Microsoft endpoints self.jwks_url = f'https://login.microsoftonline.com/{self.tenant_id}/discovery/v2.0/keys' # Initialize JWK client for token verification self.jwks_client = PyJWKClient(self.jwks_url) def validate_token(self, id_token: str) -> Optional[Dict[str, Any]]: """ Validate a Microsoft ID token and return user information. Args: id_token: The Microsoft ID token (JWT) to validate Returns: Dictionary containing user information if valid, None if invalid """ try: # 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"ID token validation failed: {str(e)}") return None def _decode_jwt_token(self, id_token: str) -> Optional[Dict[str, Any]]: """ Decode and validate ID token as JWT. Args: 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(id_token) # Decode and validate the ID token decoded_token = jwt.decode( id_token, signing_key.key, algorithms=['RS256'], audience=self.client_id, issuer=f'https://login.microsoftonline.com/{self.tenant_id}/v2.0' ) # Extract user information from token claims return { 'microsoft_id': decoded_token.get('oid') or decoded_token.get('sub'), 'username': decoded_token.get('preferred_username', '').split('@')[0], 'email': decoded_token.get('email') or decoded_token.get('preferred_username'), 'display_name': decoded_token.get('name', ''), 'given_name': decoded_token.get('given_name', ''), 'surname': decoded_token.get('family_name', ''), 'auth_type': 'microsoft' } except jwt.InvalidTokenError as e: current_app.logger.error(f"JWT token validation failed: {str(e)}") return None except Exception as e: current_app.logger.error(f"Token decoding failed: {str(e)}") return None def create_user_data(self, microsoft_user_info: Dict[str, Any]) -> Dict[str, Any]: """ Create user data dictionary from Microsoft user information. Args: microsoft_user_info: User information from Microsoft Returns: Dictionary formatted for our user system """ # Use display name if available, otherwise construct from given/surname display_name = microsoft_user_info.get('display_name', '') if not display_name: given_name = microsoft_user_info.get('given_name', '') surname = microsoft_user_info.get('surname', '') display_name = f"{given_name} {surname}".strip() or microsoft_user_info.get('username', 'Microsoft User') return { 'username': display_name, # Use display name as username for Microsoft users 'email': microsoft_user_info.get('email', ''), 'microsoft_id': microsoft_user_info.get('microsoft_id', ''), 'role': 'user', # Default role for all users 'auth_type': 'microsoft', 'password_hash': None # Microsoft users don't have local passwords }