# Microsoft SSO (Azure AD OAuth2/OIDC) Integration

**Source**: `prototype/src/talentverse/services/microsoft_sso_service.py`
**Protocol**: OAuth2/OpenID Connect with PKCE
**HTTP Client**: httpx (async)
**State Storage**: Redis (production) or in-memory (development)

### Key Endpoints

| Endpoint | URL |
|----------|-----|
| Authorization | `https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize` |
| Token | `https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token` |
| User Profile | `https://graph.microsoft.com/v1.0/me` |
| Logout | `https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/logout` |

## Data Models

```python
@dataclass
class SSOUserProfile:
    sso_subject_id: str
    email: str
    display_name: str
    given_name: Optional[str] = None
    family_name: Optional[str] = None
    job_title: Optional[str] = None
    department: Optional[str] = None
    office_location: Optional[str] = None

@dataclass
class SSOTokens:
    access_token: str
    id_token: str
    refresh_token: Optional[str] = None
    expires_in: int = 3600
    token_type: str = "Bearer"
```

## PKCE Flow

### 1. Generate Authorization URL

```python
async def generate_authorization_url(self) -> Tuple[str, str]:
    # Generate PKCE parameters
    state = secrets.token_urlsafe(32)
    code_verifier = secrets.token_urlsafe(32)
    code_challenge = base64.urlsafe_b64encode(
        hashlib.sha256(code_verifier.encode()).digest()
    ).decode().rstrip('=')

    # Store state + PKCE verifier
    await self._store_state(state, {'code_verifier': code_verifier, ...})

    auth_params = {
        'client_id': self.config.microsoft.client_id,
        'response_type': 'code',
        'redirect_uri': self.config.microsoft.redirect_uri,
        'scope': ' '.join(self.config.microsoft.scopes),
        'state': state,
        'response_mode': 'query',
        'code_challenge': code_challenge,
        'code_challenge_method': 'S256'
    }

    authorization_url = f"{auth_endpoint}?{urlencode(auth_params)}"
    return authorization_url, state
```

### 2. Handle Callback (Code Exchange)

```python
async def handle_callback(self, code: str, state: str) -> Tuple[SSOTokens, SSOUserProfile]:
    # Validate state (CSRF protection)
    if not await self._validate_state(state):
        raise HTTPException(status_code=400, detail="Invalid authentication state")

    state_data = await self._get_and_remove_state(state)
    code_verifier = state_data.get('code_verifier')

    # Exchange code for tokens
    tokens = await self._exchange_code_for_tokens(code, code_verifier)

    # Get user profile from Microsoft Graph
    user_profile = await self._get_user_profile(tokens.access_token)

    return tokens, user_profile
```

### 3. Token Exchange

```python
async def _exchange_code_for_tokens(self, code, code_verifier):
    token_data = {
        'client_id': self.config.microsoft.client_id,
        'client_secret': self.config.microsoft.client_secret,
        'code': code,
        'redirect_uri': self.config.microsoft.redirect_uri,
        'grant_type': 'authorization_code',
        'code_verifier': code_verifier  # PKCE
    }
    response = await self.client.post(token_endpoint, data=token_data,
        headers={'Content-Type': 'application/x-www-form-urlencoded'})
    return SSOTokens(**response.json())
```

### 4. User Profile (Microsoft Graph)

```python
async def _get_user_profile(self, access_token):
    headers = {'Authorization': f'Bearer {access_token}'}
    response = await self.client.get(f"{graph_endpoint}/me", headers=headers)
    profile = response.json()
    return SSOUserProfile(
        sso_subject_id=profile['id'],
        email=profile['mail'] or profile['userPrincipalName'],
        display_name=profile.get('displayName', ''),
        given_name=profile.get('givenName'),
        family_name=profile.get('surname'),
        job_title=profile.get('jobTitle'),
        department=profile.get('department'),
    )
```

### 5. Token Refresh

```python
async def refresh_access_token(self, refresh_token):
    token_data = {
        'client_id': self.config.microsoft.client_id,
        'client_secret': self.config.microsoft.client_secret,
        'refresh_token': refresh_token,
        'grant_type': 'refresh_token',
        'scope': ' '.join(self.config.microsoft.scopes)
    }
    response = await self.client.post(token_endpoint, data=token_data)
    return SSOTokens(**response.json())
```

## JWT Session Token Generation

```python
def generate_session_token(self, user_id, user_profile):
    payload = {
        'user_id': user_id,
        'email': user_profile.email,
        'name': user_profile.display_name,
        'sso_provider': 'microsoft',
        'iat': datetime.utcnow(),
        'exp': datetime.utcnow() + timedelta(minutes=expire_minutes),
        'iss': 'talentverse',
        'aud': 'talentverse-frontend'
    }
    return jwt.encode(payload, secret_key, algorithm='HS256')
```

## State Storage (Redis + Fallback)

```python
# Redis for production, in-memory dict for development
async def _store_state(self, state, state_data):
    if self._redis_client:
        await self._redis_client.setex(
            f"sso_state:{state}",
            expire_minutes * 60,
            json.dumps(state_data)
        )
    else:
        self._state_storage[state] = state_data
```
