Merge pull request #24475 from BerriAI/litellm_fix-sso-return-to-validation

fix(proxy): ignore return_to in SSO when control_plane_url is not con…
This commit is contained in:
ryan-crabbe
2026-03-23 22:15:45 -07:00
committed by GitHub
2 changed files with 22 additions and 25 deletions
+18 -19
View File
@@ -404,14 +404,14 @@ async def google_login(
state=cli_state,
)
if return_to is not None and sso_redirect is not None:
SSOAuthenticationHandler._validate_return_to(return_to)
sso_redirect.set_cookie(
key="litellm_cp_return_to",
value=return_to,
max_age=600,
httponly=True,
samesite="lax",
)
if SSOAuthenticationHandler._validate_return_to(return_to):
sso_redirect.set_cookie(
key="litellm_cp_return_to",
value=return_to,
max_age=600,
httponly=True,
samesite="lax",
)
return sso_redirect
elif ui_username is not None:
# No Google, Microsoft SSO
@@ -1778,22 +1778,19 @@ class SSOAuthenticationHandler:
"""
@staticmethod
def _validate_return_to(return_to: str) -> None:
def _validate_return_to(return_to: str) -> bool:
"""
Validate that return_to matches the configured control_plane_url origin.
Raises HTTPException(400) if:
- control_plane_url is not configured in general_settings
- return_to origin does not match control_plane_url origin
Returns True if return_to is valid and should be used.
Returns False if control_plane_url is not configured (return_to is ignored).
Raises HTTPException(400) if return_to origin does not match control_plane_url origin.
"""
from litellm.proxy.proxy_server import general_settings
control_plane_url = general_settings.get("control_plane_url")
if control_plane_url is None:
raise HTTPException(
status_code=400,
detail="return_to is not allowed: control_plane_url is not configured",
)
return False
def _origin(url: str) -> tuple:
parsed = urlparse(url)
@@ -1809,6 +1806,8 @@ class SSOAuthenticationHandler:
detail="return_to does not match the configured control_plane_url",
)
return True
@staticmethod
async def get_sso_login_redirect(
redirect_url: str,
@@ -2589,9 +2588,9 @@ class SSOAuthenticationHandler:
# Control-plane cross-origin: store JWT behind a single-use opaque
# code (60s TTL) so the token never appears in browser history / logs.
# The control plane redeems it via POST /v3/login/exchange.
if return_to is not None:
SSOAuthenticationHandler._validate_return_to(return_to)
if return_to is not None and SSOAuthenticationHandler._validate_return_to(
return_to
):
code = secrets.token_urlsafe(32)
cache_key = f"login_code:{code}"
cache_value = {"token": jwt_token, "redirect_url": return_to}
@@ -5164,15 +5164,13 @@ def test_generic_response_convertor_extra_attributes_missing_field(monkeypatch):
class TestValidateReturnTo:
"""Tests for SSOAuthenticationHandler._validate_return_to"""
def test_rejects_when_no_control_plane_url_configured(self, monkeypatch):
"""return_to should be rejected if control_plane_url is not in general_settings."""
def test_returns_false_when_no_control_plane_url_configured(self, monkeypatch):
"""return_to should be silently ignored if control_plane_url is not in general_settings."""
monkeypatch.setattr(
"litellm.proxy.proxy_server.general_settings", {}
)
with pytest.raises(HTTPException) as exc_info:
SSOAuthenticationHandler._validate_return_to("https://cp.example.com/ui")
assert exc_info.value.status_code == 400
assert "not configured" in exc_info.value.detail
result = SSOAuthenticationHandler._validate_return_to("https://cp.example.com/ui")
assert result is False
def test_allows_matching_origin(self, monkeypatch):
"""return_to matching the configured control_plane_url origin should pass."""