feat: add cross-origin SSO cookie handoff for control plane

- Add return_to param to /sso/key/generate for cross-origin redirect
- Store return_to in httpOnly cookie during SSO redirect
- Read cookie in /sso/callback and pass to OpenID handler
- Redirect to control plane with token in URL, delete cookie after use
This commit is contained in:
Ryan Crabbe 2026-03-19 14:02:49 -07:00
parent d04f8bcc45
commit 5543718caf

View file

@ -301,6 +301,7 @@ async def google_login(
source: Optional[str] = None,
key: Optional[str] = None,
existing_key: Optional[str] = None,
return_to: Optional[str] = None,
): # noqa: PLR0915
"""
Create Proxy API Keys using Google Workspace SSO. Requires setting PROXY_BASE_URL in .env
@ -394,13 +395,22 @@ async def google_login(
is True
):
verbose_proxy_logger.info(f"Redirecting to SSO login for {redirect_url}")
return await SSOAuthenticationHandler.get_sso_login_redirect(
sso_redirect = await SSOAuthenticationHandler.get_sso_login_redirect(
redirect_url=redirect_url,
microsoft_client_id=microsoft_client_id,
google_client_id=google_client_id,
generic_client_id=generic_client_id,
state=cli_state,
)
if return_to is not None and sso_redirect is not None:
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
# Use UI Credentials set in .env
@ -1306,12 +1316,19 @@ async def auth_callback(request: Request, state: Optional[str] = None): # noqa:
request=request, key=key_id, existing_key=existing_key, result=result
)
# Control-plane cross-origin: read return_to from cookie
cp_return_to: Optional[str] = None
cookie_return_to = request.cookies.get("litellm_cp_return_to")
if cookie_return_to:
cp_return_to = cookie_return_to.strip('"')
return await SSOAuthenticationHandler.get_redirect_response_from_openid(
result=result,
request=request,
received_response=received_response,
generic_client_id=generic_client_id,
ui_access_mode=ui_access_mode,
return_to=cp_return_to,
)
@ -2352,6 +2369,7 @@ class SSOAuthenticationHandler:
received_response: Optional[dict] = None,
generic_client_id: Optional[str] = None,
ui_access_mode: Optional[Dict] = None,
return_to: Optional[str] = None,
) -> RedirectResponse:
import jwt
@ -2528,6 +2546,31 @@ class SSOAuthenticationHandler:
master_key or "",
algorithm="HS256",
)
# Control-plane cross-origin: redirect back to the control plane UI
# with the token in the URL (cookie won't work cross-origin)
if return_to is not None:
from urllib.parse import urlencode, urlparse
parsed = urlparse(return_to)
if parsed.scheme not in ("http", "https"):
raise HTTPException(
status_code=400,
detail="return_to must be an HTTP(S) URL",
)
separator = "&" if "?" in return_to else "?"
redirect_url = (
return_to
+ separator
+ urlencode({"login": "success", "token": jwt_token})
)
verbose_proxy_logger.info(
f"Cross-origin SSO: redirecting to control plane at {parsed.netloc}"
)
redirect_response = RedirectResponse(url=redirect_url, status_code=303)
redirect_response.delete_cookie("litellm_cp_return_to")
return redirect_response
if user_id is not None and isinstance(user_id, str):
litellm_dashboard_ui += "?login=success"
verbose_proxy_logger.info(f"Redirecting to {litellm_dashboard_ui}")