[Feat] MSFT SSO - allow overriding env var attribute names (#18998)

* add MSFT SSO constants

* fix MSFT SSO env vars

* test_microsoft_sso_handler_openid_from_response_with_custom_attributes
This commit is contained in:
Ishaan Jaff
2026-01-12 18:56:35 -08:00
committed by GitHub
parent 0feedfdf3d
commit a1bba8c99b
5 changed files with 112 additions and 6 deletions
@@ -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://<proxy_base_url>/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
```
</TabItem>
<TabItem value="Generic" label="Generic SSO Provider">
@@ -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
+17
View File
@@ -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")
)
+13 -6
View File
@@ -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,
)
@@ -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)