From 3454b00c28187a5cfaf2ac8ca5acb979b3e7e1f5 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 15 May 2026 13:13:26 +0000 Subject: [PATCH] fix(sso): use active provider for callback env_prefix; dedupe SSO payload util MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug #7 (ui_sso.py auth_callback): env_prefix and generic_client_id were derived solely from whether OKTA_CLIENT_ID was set, so a Google or Microsoft login with OKTA_CLIENT_ID also configured would route through the generic attribute-mapping branch with OKTA_* env-var lookups. Track the active provider in the elif chain and derive env_prefix / callback client_id from that — Google/Microsoft pass None. Bug #8 (SSOModals.tsx): handleFormSubmit duplicated processSSOSettingsPayload from utils.ts. The two had already diverged (legacy modal missing use_team_mappings / team_ids_jwt_field handling and the provider-supports-role-mappings guard). Replace inline logic with the shared utility. --- litellm/proxy/management_endpoints/ui_sso.py | 29 +++++++--- .../src/components/SSOModals.tsx | 54 +++---------------- 2 files changed, 29 insertions(+), 54 deletions(-) diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index 0d34e38257c..42999e6b15a 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -1729,18 +1729,24 @@ async def auth_callback(request: Request, state: Optional[str] = None): # noqa: verbose_proxy_logger.info(f"Redirecting to {redirect_url}") result = None + # Track which SSO provider actually handled this callback so attribute + # mapping (env_prefix, generic_client_id) reflects the active branch. + # Multiple *_CLIENT_ID env vars may be set; the elif chain picks one. + active_provider: Optional[str] = None if google_client_id is not None: result = await GoogleSSOHandler.get_google_callback_response( request=request, google_client_id=google_client_id, redirect_url=redirect_url, ) + active_provider = "google" elif microsoft_client_id is not None: result = await MicrosoftSSOHandler.get_microsoft_callback_response( request=request, microsoft_client_id=microsoft_client_id, redirect_url=redirect_url, ) + active_provider = "microsoft" elif okta_client_id is not None: ( @@ -1755,6 +1761,7 @@ async def auth_callback(request: Request, state: Optional[str] = None): # noqa: sso_jwt_handler=sso_jwt_handler, provider=_OIDC_PROVIDER_OKTA, ) + active_provider = _OIDC_PROVIDER_OKTA elif generic_client_id is not None: ( @@ -1768,6 +1775,7 @@ async def auth_callback(request: Request, state: Optional[str] = None): # noqa: redirect_url=redirect_url, sso_jwt_handler=sso_jwt_handler, ) + active_provider = _OIDC_PROVIDER_GENERIC if result is None: raise HTTPException( @@ -1787,20 +1795,29 @@ async def auth_callback(request: Request, state: Optional[str] = None): # noqa: # Starlette's cookie_parser already handles RFC 2109 unquoting. cp_return_to: Optional[str] = request.cookies.get("litellm_cp_return_to") + # Only pass generic_client_id / env_prefix for the OIDC providers; Google + # and Microsoft results do not flow through the generic attribute-mapping + # branch in `_get_user_email_and_id_from_result`. + if active_provider == _OIDC_PROVIDER_OKTA: + callback_client_id: Optional[str] = okta_client_id + callback_env_prefix = _get_oidc_env_prefix(_OIDC_PROVIDER_OKTA) + elif active_provider == _OIDC_PROVIDER_GENERIC: + callback_client_id = generic_client_id + callback_env_prefix = _get_oidc_env_prefix(_OIDC_PROVIDER_GENERIC) + else: + callback_client_id = None + callback_env_prefix = _get_oidc_env_prefix(_OIDC_PROVIDER_GENERIC) + return await SSOAuthenticationHandler.get_redirect_response_from_openid( result=result, request=request, received_response=received_response, - generic_client_id=okta_client_id or generic_client_id, + generic_client_id=callback_client_id, ui_access_mode=ui_access_mode, access_token_payload=access_token_payload, jwt_handler=jwt_handler, return_to=cp_return_to, - env_prefix=_get_oidc_env_prefix( - _OIDC_PROVIDER_OKTA - if okta_client_id is not None - else _OIDC_PROVIDER_GENERIC - ), + env_prefix=callback_env_prefix, ) diff --git a/ui/litellm-dashboard/src/components/SSOModals.tsx b/ui/litellm-dashboard/src/components/SSOModals.tsx index 280cba71e5c..bb1e18a8614 100644 --- a/ui/litellm-dashboard/src/components/SSOModals.tsx +++ b/ui/litellm-dashboard/src/components/SSOModals.tsx @@ -2,7 +2,11 @@ import React, { useEffect, useState } from "react"; import { Modal, Form, Input, Button as Button2, Select, Checkbox } from "antd"; import { Text, TextInput } from "@tremor/react"; import { getSSOSettings, updateSSOSettings } from "./networking"; -import { detectSSOProvider, extractRoleMappingFields } from "./Settings/AdminSettings/SSOSettings/utils"; +import { + detectSSOProvider, + extractRoleMappingFields, + processSSOSettingsPayload, +} from "./Settings/AdminSettings/SSOSettings/utils"; import NotificationsManager from "./molecules/notifications_manager"; import { parseErrorMessage } from "./shared/errorUtils"; @@ -150,53 +154,7 @@ const SSOModals: React.FC = ({ } try { - const { - proxy_admin_teams, - admin_viewer_teams, - internal_user_teams, - internal_viewer_teams, - default_role, - group_claim, - use_role_mappings, - ...rest - } = formValues; - - const payload: any = { - ...rest, - }; - - // Add role mappings if use_role_mappings is checked - if (use_role_mappings) { - const provider = rest.sso_provider || "generic"; - // Helper function to split comma-separated string into array - const splitTeams = (teams: string | undefined): string[] => { - if (!teams || teams.trim() === "") return []; - return teams - .split(",") - .map((team) => team.trim()) - .filter((team) => team.length > 0); - }; - - // Map default role display values to backend values - const defaultRoleMapping: Record = { - internal_user_viewer: "internal_user_viewer", - internal_user: "internal_user", - proxy_admin_viewer: "proxy_admin_viewer", - proxy_admin: "proxy_admin", - }; - - payload.role_mappings = { - provider, - group_claim, - default_role: defaultRoleMapping[default_role] || "internal_user", - roles: { - proxy_admin: splitTeams(proxy_admin_teams), - proxy_admin_viewer: splitTeams(admin_viewer_teams), - internal_user: splitTeams(internal_user_teams), - internal_user_viewer: splitTeams(internal_viewer_teams), - }, - }; - } + const payload = processSSOSettingsPayload(formValues); // Save SSO settings using the new API await updateSSOSettings(accessToken, payload);