fix(sso): use active provider for callback env_prefix; dedupe SSO payload util

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.
This commit is contained in:
mateo-berri 2026-05-15 13:13:26 +00:00
parent f8f43a4295
commit 3454b00c28
No known key found for this signature in database
2 changed files with 29 additions and 54 deletions

View file

@ -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,
)

View file

@ -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<SSOModalsProps> = ({
}
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<string, string> = {
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);