diff --git a/docs/my-website/docs/proxy/admin_ui_sso.md b/docs/my-website/docs/proxy/admin_ui_sso.md index 4984cac2d0..7b299429db 100644 --- a/docs/my-website/docs/proxy/admin_ui_sso.md +++ b/docs/my-website/docs/proxy/admin_ui_sso.md @@ -111,6 +111,42 @@ To set up app roles: 4. Assign users to these roles in your Enterprise Application 5. When users sign in via SSO, LiteLLM will automatically assign them the corresponding role +**Advanced: Custom User Attribute Mapping** + +For certain Microsoft Entra ID configurations, you may need to override the default user attribute field names. This is useful when your organization uses custom claims or non-standard attribute names in the SSO response. + +**Step 1: Debug SSO Response** + +First, inspect the JWT fields returned by your Microsoft SSO provider using the [SSO Debug Route](#debugging-sso-jwt-fields). + +1. Add `/sso/debug/callback` as a redirect URL in your Azure App Registration +2. Navigate to `https:///sso/debug/login` +3. Complete the SSO flow to see the returned user attributes + +**Step 2: Identify Field Attribute Names** + +From the debug response, identify the field names used for email, display name, user ID, first name, and last name. + +**Step 3: Set Environment Variables** + +Override the default attribute names by setting these environment variables: + +| Environment Variable | Description | Default Value | +|---------------------|-------------|---------------| +| `MICROSOFT_USER_EMAIL_ATTRIBUTE` | Field name for user email | `userPrincipalName` | +| `MICROSOFT_USER_DISPLAY_NAME_ATTRIBUTE` | Field name for display name | `displayName` | +| `MICROSOFT_USER_ID_ATTRIBUTE` | Field name for user ID | `id` | +| `MICROSOFT_USER_FIRST_NAME_ATTRIBUTE` | Field name for first name | `givenName` | +| `MICROSOFT_USER_LAST_NAME_ATTRIBUTE` | Field name for last name | `surname` | + +**Step 4: Restart the Proxy** + +After setting the environment variables, restart the proxy: + +```bash +litellm --config /path/to/config.yaml +``` + diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index e9f9fb37a2..6c5c45dc90 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -777,6 +777,11 @@ router_settings: | MICROSOFT_SERVICE_PRINCIPAL_ID | Service Principal ID for Microsoft Enterprise Application. (This is an advanced feature if you want litellm to auto-assign members to Litellm Teams based on their Microsoft Entra ID Groups) | MICROSOFT_TENANT | Tenant ID for Microsoft Azure | MICROSOFT_TOKEN_ENDPOINT | Custom token endpoint URL for Microsoft SSO (overrides default Microsoft OAuth token endpoint) +| MICROSOFT_USER_DISPLAY_NAME_ATTRIBUTE | Field name for user display name in Microsoft SSO response. Default is `displayName` +| MICROSOFT_USER_EMAIL_ATTRIBUTE | Field name for user email in Microsoft SSO response. Default is `userPrincipalName` +| MICROSOFT_USER_FIRST_NAME_ATTRIBUTE | Field name for user first name in Microsoft SSO response. Default is `givenName` +| MICROSOFT_USER_ID_ATTRIBUTE | Field name for user ID in Microsoft SSO response. Default is `id` +| MICROSOFT_USER_LAST_NAME_ATTRIBUTE | Field name for user last name in Microsoft SSO response. Default is `surname` | MICROSOFT_USERINFO_ENDPOINT | Custom userinfo endpoint URL for Microsoft SSO (overrides default Microsoft Graph userinfo endpoint) | NO_DOCS | Flag to disable Swagger UI documentation | NO_REDOC | Flag to disable Redoc documentation diff --git a/litellm/constants.py b/litellm/constants.py index e24186d567..4ea0be247b 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1285,3 +1285,20 @@ COROUTINE_CHECKER_MAX_SIZE_IN_MEMORY = int( ########################### RAG Text Splitter Constants ########################### DEFAULT_CHUNK_SIZE = int(os.getenv("DEFAULT_CHUNK_SIZE", 1000)) DEFAULT_CHUNK_OVERLAP = int(os.getenv("DEFAULT_CHUNK_OVERLAP", 200)) + +########################### Microsoft SSO Constants ########################### +MICROSOFT_USER_EMAIL_ATTRIBUTE = str( + os.getenv("MICROSOFT_USER_EMAIL_ATTRIBUTE", "userPrincipalName") +) +MICROSOFT_USER_DISPLAY_NAME_ATTRIBUTE = str( + os.getenv("MICROSOFT_USER_DISPLAY_NAME_ATTRIBUTE", "displayName") +) +MICROSOFT_USER_ID_ATTRIBUTE = str( + os.getenv("MICROSOFT_USER_ID_ATTRIBUTE", "id") +) +MICROSOFT_USER_FIRST_NAME_ATTRIBUTE = str( + os.getenv("MICROSOFT_USER_FIRST_NAME_ATTRIBUTE", "givenName") +) +MICROSOFT_USER_LAST_NAME_ATTRIBUTE = str( + os.getenv("MICROSOFT_USER_LAST_NAME_ATTRIBUTE", "surname") +) diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index 5b3ebbf1c2..3075792f9f 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -23,7 +23,14 @@ import litellm from litellm._logging import verbose_proxy_logger from litellm._uuid import uuid from litellm.caching import DualCache -from litellm.constants import MAX_SPENDLOG_ROWS_TO_QUERY +from litellm.constants import ( + MAX_SPENDLOG_ROWS_TO_QUERY, + MICROSOFT_USER_DISPLAY_NAME_ATTRIBUTE, + MICROSOFT_USER_EMAIL_ATTRIBUTE, + MICROSOFT_USER_FIRST_NAME_ATTRIBUTE, + MICROSOFT_USER_ID_ATTRIBUTE, + MICROSOFT_USER_LAST_NAME_ATTRIBUTE, +) from litellm.litellm_core_utils.dot_notation_indexing import get_nested_value from litellm.llms.custom_httpx.http_handler import ( AsyncHTTPHandler, @@ -2358,12 +2365,12 @@ class MicrosoftSSOHandler: response = response or {} verbose_proxy_logger.debug(f"Microsoft SSO Callback Response: {response}") openid_response = CustomOpenID( - email=response.get("userPrincipalName") or response.get("mail"), - display_name=response.get("displayName"), + email=response.get(MICROSOFT_USER_EMAIL_ATTRIBUTE) or response.get("mail"), + display_name=response.get(MICROSOFT_USER_DISPLAY_NAME_ATTRIBUTE), provider="microsoft", - id=response.get("id"), - first_name=response.get("givenName"), - last_name=response.get("surname"), + id=response.get(MICROSOFT_USER_ID_ATTRIBUTE), + first_name=response.get(MICROSOFT_USER_FIRST_NAME_ATTRIBUTE), + last_name=response.get(MICROSOFT_USER_LAST_NAME_ATTRIBUTE), team_ids=team_ids, user_role=user_role, ) diff --git a/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py b/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py index be9d193e3f..f0cf5459ad 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py +++ b/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py @@ -115,6 +115,47 @@ def test_microsoft_sso_handler_with_empty_response(): assert result.team_ids == [] +def test_microsoft_sso_handler_openid_from_response_with_custom_attributes(): + """ + Test that MicrosoftSSOHandler.openid_from_response uses custom attribute names + from constants when environment variables are set. + """ + # Arrange + mock_response = { + "custom_email_field": "custom@example.com", + "custom_display_name": "Custom Display Name", + "custom_id_field": "custom_user_123", + "custom_first_name": "CustomFirst", + "custom_last_name": "CustomLast", + } + expected_team_ids = ["team1"] + + # Act + with patch("litellm.constants.MICROSOFT_USER_EMAIL_ATTRIBUTE", "custom_email_field"), \ + patch("litellm.constants.MICROSOFT_USER_DISPLAY_NAME_ATTRIBUTE", "custom_display_name"), \ + patch("litellm.constants.MICROSOFT_USER_ID_ATTRIBUTE", "custom_id_field"), \ + patch("litellm.constants.MICROSOFT_USER_FIRST_NAME_ATTRIBUTE", "custom_first_name"), \ + patch("litellm.constants.MICROSOFT_USER_LAST_NAME_ATTRIBUTE", "custom_last_name"), \ + patch("litellm.proxy.management_endpoints.ui_sso.MICROSOFT_USER_EMAIL_ATTRIBUTE", "custom_email_field"), \ + patch("litellm.proxy.management_endpoints.ui_sso.MICROSOFT_USER_DISPLAY_NAME_ATTRIBUTE", "custom_display_name"), \ + patch("litellm.proxy.management_endpoints.ui_sso.MICROSOFT_USER_ID_ATTRIBUTE", "custom_id_field"), \ + patch("litellm.proxy.management_endpoints.ui_sso.MICROSOFT_USER_FIRST_NAME_ATTRIBUTE", "custom_first_name"), \ + patch("litellm.proxy.management_endpoints.ui_sso.MICROSOFT_USER_LAST_NAME_ATTRIBUTE", "custom_last_name"): + result = MicrosoftSSOHandler.openid_from_response( + response=mock_response, team_ids=expected_team_ids, user_role=None + ) + + # Assert + assert isinstance(result, CustomOpenID) + assert result.email == "custom@example.com" + assert result.display_name == "Custom Display Name" + assert result.provider == "microsoft" + assert result.id == "custom_user_123" + assert result.first_name == "CustomFirst" + assert result.last_name == "CustomLast" + assert result.team_ids == expected_team_ids + + def test_get_microsoft_callback_response(): # Arrange mock_request = MagicMock(spec=Request)