mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
[Fix] MSFT SSO - allow setting custom MSFT Base URLs (#18977)
* fix TestCustomMicrosoftSSO * init CustomMicrosoftSSO * use CustomMicrosoftSSO * docs fix * docs fix
This commit is contained in:
parent
c126cfd8db
commit
21d611554b
6 changed files with 249 additions and 9 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
12
litellm/proxy/management_endpoints/sso/__init__.py
Normal file
12
litellm/proxy/management_endpoints/sso/__init__.py
Normal file
|
|
@ -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"]
|
||||
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
@ -57,6 +57,7 @@ from litellm.proxy.common_utils.html_forms.jwt_display_template import (
|
|||
)
|
||||
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,
|
||||
|
|
@ -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] = []
|
||||
|
|
@ -1450,8 +1451,6 @@ class SSOAuthenticationHandler:
|
|||
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:
|
||||
|
|
@ -1461,7 +1460,7 @@ class SSOAuthenticationHandler:
|
|||
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,
|
||||
|
|
@ -2277,8 +2276,6 @@ class MicrosoftSSOHandler:
|
|||
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:
|
||||
|
|
@ -2295,7 +2292,7 @@ class MicrosoftSSOHandler:
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ sys.path.insert(
|
|||
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,
|
||||
|
|
@ -3386,3 +3387,126 @@ class TestSSOReadinessEndpoint:
|
|||
)
|
||||
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…
Add table
Reference in a new issue