Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion docs/my-website/docs/proxy/admin_ui_sso.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,21 @@ GOOGLE_CLIENT_SECRET=
```shell
MICROSOFT_CLIENT_ID="84583a4d-"
MICROSOFT_CLIENT_SECRET="nbk8Q~"
MICROSOFT_TENANT="5a39737
MICROSOFT_TENANT="5a39737"
```

**Optional: Custom Microsoft SSO Endpoints**

If you need to use custom Microsoft SSO endpoints (e.g., for a custom identity provider, sovereign cloud, or proxy), you can override the default endpoints:

```shell
MICROSOFT_AUTHORIZATION_ENDPOINT="https://your-custom-url.com/oauth2/v2.0/authorize"
MICROSOFT_TOKEN_ENDPOINT="https://your-custom-url.com/oauth2/v2.0/token"
MICROSOFT_USERINFO_ENDPOINT="https://your-custom-graph-api.com/v1.0/me"
```

If these are not set, the default Microsoft endpoints are used based on your tenant.

- Set Redirect URI on your App Registration on https://portal.azure.com/
- Set a redirect url = `<your proxy base url>/sso/callback`
```shell
Expand Down
5 changes: 4 additions & 1 deletion docs/my-website/docs/proxy/config_settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -771,10 +771,13 @@ router_settings:
| MINIMUM_PROMPT_CACHE_TOKEN_COUNT | Minimum token count for caching a prompt. Default is 1024
| MISTRAL_API_BASE | Base URL for Mistral API. Default is https://api.mistral.ai
| MISTRAL_API_KEY | API key for Mistral API
| MICROSOFT_AUTHORIZATION_ENDPOINT | Custom authorization endpoint URL for Microsoft SSO (overrides default Microsoft OAuth authorization endpoint)
| MICROSOFT_CLIENT_ID | Client ID for Microsoft services
| MICROSOFT_CLIENT_SECRET | Client secret for Microsoft services
| MICROSOFT_TENANT | Tenant ID for Microsoft Azure
| 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_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
| NO_PROXY | List of addresses to bypass proxy
Expand Down
12 changes: 12 additions & 0 deletions litellm/proxy/management_endpoints/sso/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""
SSO (Single Sign-On) related modules for LiteLLM Proxy.

This package contains custom SSO implementations and utilities.
"""

from litellm.proxy.management_endpoints.sso.custom_microsoft_sso import (
CustomMicrosoftSSO,
)

__all__ = ["CustomMicrosoftSSO"]

91 changes: 91 additions & 0 deletions litellm/proxy/management_endpoints/sso/custom_microsoft_sso.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""
Custom Microsoft SSO class that allows overriding default Microsoft endpoints.

This module provides a subclass of fastapi_sso's MicrosoftSSO that allows
custom authorization, token, and userinfo endpoints to be specified via environment
variables.

Environment Variables:
- MICROSOFT_AUTHORIZATION_ENDPOINT: Custom authorization endpoint URL
- MICROSOFT_TOKEN_ENDPOINT: Custom token endpoint URL
- MICROSOFT_USERINFO_ENDPOINT: Custom userinfo endpoint URL

If these are not set, the default Microsoft endpoints are used.
"""

import os
from typing import List, Optional, Union

import pydantic
from fastapi_sso.sso.base import DiscoveryDocument
from fastapi_sso.sso.microsoft import MicrosoftSSO

from litellm._logging import verbose_proxy_logger


class CustomMicrosoftSSO(MicrosoftSSO):
"""
Microsoft SSO subclass that allows overriding default endpoints via environment variables.

Supports:
- MICROSOFT_AUTHORIZATION_ENDPOINT
- MICROSOFT_TOKEN_ENDPOINT
- MICROSOFT_USERINFO_ENDPOINT
"""

def __init__(
self,
client_id: str,
client_secret: str,
redirect_uri: Optional[Union[pydantic.AnyHttpUrl, str]] = None,
allow_insecure_http: bool = False,
scope: Optional[List[str]] = None,
tenant: Optional[str] = None,
):
super().__init__(
client_id=client_id,
client_secret=client_secret,
redirect_uri=redirect_uri,
allow_insecure_http=allow_insecure_http,
scope=scope,
tenant=tenant,
)

async def get_discovery_document(self) -> DiscoveryDocument:
"""
Override to support custom endpoints via environment variables.
Falls back to default Microsoft endpoints if not set.
"""
custom_authorization_endpoint = os.getenv(
"MICROSOFT_AUTHORIZATION_ENDPOINT", None
)
custom_token_endpoint = os.getenv("MICROSOFT_TOKEN_ENDPOINT", None)
custom_userinfo_endpoint = os.getenv("MICROSOFT_USERINFO_ENDPOINT", None)

# Use custom endpoints if set, otherwise use defaults
authorization_endpoint = (
custom_authorization_endpoint
or f"https://login.microsoftonline.com/{self.tenant}/oauth2/v2.0/authorize"
)
token_endpoint = (
custom_token_endpoint
or f"https://login.microsoftonline.com/{self.tenant}/oauth2/v2.0/token"
)
userinfo_endpoint = (
custom_userinfo_endpoint or f"https://graph.microsoft.com/{self.version}/me"
)

if custom_authorization_endpoint or custom_token_endpoint or custom_userinfo_endpoint:
verbose_proxy_logger.debug(
f"Using custom Microsoft SSO endpoints - "
f"authorization: {authorization_endpoint}, "
f"token: {token_endpoint}, "
f"userinfo: {userinfo_endpoint}"
)

return DiscoveryDocument(
authorization_endpoint=authorization_endpoint,
token_endpoint=token_endpoint,
userinfo_endpoint=userinfo_endpoint,
)

11 changes: 4 additions & 7 deletions litellm/proxy/management_endpoints/ui_sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
)
from litellm.proxy.common_utils.html_forms.ui_login import html_form
from litellm.proxy.management_endpoints.internal_user_endpoints import new_user
from litellm.proxy.management_endpoints.sso import CustomMicrosoftSSO
from litellm.proxy.management_endpoints.sso_helper_utils import (
check_is_admin_only_access,
has_admin_ui_access,
Expand Down Expand Up @@ -341,7 +342,7 @@ def generic_response_convertor(
if role_mappings is not None and role_mappings.provider.lower() in ["generic", "okta"]:
# Use role_mappings to determine role from groups
group_claim = role_mappings.group_claim
user_groups_raw = get_nested_value(response, group_claim)
user_groups_raw: Any = get_nested_value(response, group_claim)

# Handle different formats: could be a list, string (comma-separated), or single value
user_groups: List[str] = []
Expand Down Expand Up @@ -1450,8 +1451,6 @@ async def get_sso_login_redirect(
return await google_sso.get_login_redirect(state=state)
# Microsoft SSO Auth
elif microsoft_client_id is not None:
from fastapi_sso.sso.microsoft import MicrosoftSSO

microsoft_client_secret = os.getenv("MICROSOFT_CLIENT_SECRET", None)
microsoft_tenant = os.getenv("MICROSOFT_TENANT", None)
if microsoft_client_secret is None:
Expand All @@ -1461,7 +1460,7 @@ async def get_sso_login_redirect(
param="MICROSOFT_CLIENT_SECRET",
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
microsoft_sso = MicrosoftSSO(
microsoft_sso = CustomMicrosoftSSO(
client_id=microsoft_client_id,
client_secret=microsoft_client_secret,
tenant=microsoft_tenant,
Expand Down Expand Up @@ -2277,8 +2276,6 @@ async def get_microsoft_callback_response(
Args:
return_raw_sso_response: If True, return the raw SSO response
"""
from fastapi_sso.sso.microsoft import MicrosoftSSO

microsoft_client_secret = os.getenv("MICROSOFT_CLIENT_SECRET", None)
microsoft_tenant = os.getenv("MICROSOFT_TENANT", None)
if microsoft_client_secret is None:
Expand All @@ -2295,7 +2292,7 @@ async def get_microsoft_callback_response(
param="MICROSOFT_TENANT",
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
microsoft_sso = MicrosoftSSO(
microsoft_sso = CustomMicrosoftSSO(
client_id=microsoft_client_id,
client_secret=microsoft_client_secret,
tenant=microsoft_tenant,
Expand Down
124 changes: 124 additions & 0 deletions tests/test_litellm/proxy/management_endpoints/test_ui_sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import litellm
from litellm.proxy._types import LiteLLM_UserTable, NewTeamRequest, NewUserResponse
from litellm.proxy.auth.handle_jwt import JWTHandler
from litellm.proxy.management_endpoints.sso import CustomMicrosoftSSO
from litellm.proxy.management_endpoints.types import CustomOpenID
from litellm.proxy.management_endpoints.ui_sso import (
GoogleSSOHandler,
Expand Down Expand Up @@ -3386,3 +3387,126 @@ async def test_sso_readiness_generic_configurations(
)
finally:
app.dependency_overrides.clear()


class TestCustomMicrosoftSSO:
"""Tests for CustomMicrosoftSSO class."""

@pytest.mark.asyncio
async def test_custom_microsoft_sso_uses_default_endpoints_when_no_env_vars(self):
"""
Test that CustomMicrosoftSSO uses default Microsoft endpoints
when no custom environment variables are set.
"""
# Ensure no custom endpoints are set
for key in [
"MICROSOFT_AUTHORIZATION_ENDPOINT",
"MICROSOFT_TOKEN_ENDPOINT",
"MICROSOFT_USERINFO_ENDPOINT",
]:
os.environ.pop(key, None)

sso = CustomMicrosoftSSO(
client_id="test-client-id",
client_secret="test-client-secret",
tenant="test-tenant",
redirect_uri="http://localhost:4000/sso/callback",
)

discovery = await sso.get_discovery_document()

assert discovery["authorization_endpoint"] == "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/authorize"
assert discovery["token_endpoint"] == "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token"
assert discovery["userinfo_endpoint"] == "https://graph.microsoft.com/v1.0/me"

@pytest.mark.asyncio
async def test_custom_microsoft_sso_uses_custom_endpoints_when_env_vars_set(self):
"""
Test that CustomMicrosoftSSO uses custom endpoints
when environment variables are set.
"""
custom_auth_endpoint = "https://custom.example.com/oauth2/v2.0/authorize"
custom_token_endpoint = "https://custom.example.com/oauth2/v2.0/token"
custom_userinfo_endpoint = "https://custom.example.com/v1.0/me"

with patch.dict(
os.environ,
{
"MICROSOFT_AUTHORIZATION_ENDPOINT": custom_auth_endpoint,
"MICROSOFT_TOKEN_ENDPOINT": custom_token_endpoint,
"MICROSOFT_USERINFO_ENDPOINT": custom_userinfo_endpoint,
},
):
sso = CustomMicrosoftSSO(
client_id="test-client-id",
client_secret="test-client-secret",
tenant="test-tenant",
redirect_uri="http://localhost:4000/sso/callback",
)

discovery = await sso.get_discovery_document()

assert discovery["authorization_endpoint"] == custom_auth_endpoint
assert discovery["token_endpoint"] == custom_token_endpoint
assert discovery["userinfo_endpoint"] == custom_userinfo_endpoint

@pytest.mark.asyncio
async def test_custom_microsoft_sso_uses_partial_custom_endpoints(self):
"""
Test that CustomMicrosoftSSO uses custom endpoints for those set,
and defaults for others.
"""
custom_auth_endpoint = "https://custom.example.com/oauth2/v2.0/authorize"

# Clear other env vars first
os.environ.pop("MICROSOFT_TOKEN_ENDPOINT", None)
os.environ.pop("MICROSOFT_USERINFO_ENDPOINT", None)

with patch.dict(
os.environ,
{
"MICROSOFT_AUTHORIZATION_ENDPOINT": custom_auth_endpoint,
},
):
sso = CustomMicrosoftSSO(
client_id="test-client-id",
client_secret="test-client-secret",
tenant="test-tenant",
redirect_uri="http://localhost:4000/sso/callback",
)

discovery = await sso.get_discovery_document()

# Custom auth endpoint
assert discovery["authorization_endpoint"] == custom_auth_endpoint
# Default token and userinfo endpoints
assert discovery["token_endpoint"] == "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token"
assert discovery["userinfo_endpoint"] == "https://graph.microsoft.com/v1.0/me"

def test_custom_microsoft_sso_uses_common_tenant_when_none(self):
"""
Test that CustomMicrosoftSSO uses 'common' tenant when tenant is None.
"""
sso = CustomMicrosoftSSO(
client_id="test-client-id",
client_secret="test-client-secret",
tenant=None,
redirect_uri="http://localhost:4000/sso/callback",
)

assert sso.tenant == "common"

def test_custom_microsoft_sso_is_subclass_of_microsoft_sso(self):
"""
Test that CustomMicrosoftSSO is a subclass of MicrosoftSSO.
"""
from fastapi_sso.sso.microsoft import MicrosoftSSO

sso = CustomMicrosoftSSO(
client_id="test-client-id",
client_secret="test-client-secret",
tenant="test-tenant",
redirect_uri="http://localhost:4000/sso/callback",
)

assert isinstance(sso, MicrosoftSSO)
Loading