From 23df4f3b84c341ae39f2bc1ec442d93c133ab526 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:12:15 -0700 Subject: [PATCH] fix(proxy): refuse a stale gateway team pick and require an explicit choice --- .../anthropic_endpoints/gateway_endpoints.py | 17 +++--- .../html_forms/native_client_consent.py | 7 ++- litellm/proxy/management_endpoints/ui_sso.py | 7 ++- .../test_gateway_endpoints.py | 56 +++++++++++++------ .../proxy/management_endpoints/test_ui_sso.py | 10 ++-- 5 files changed, 64 insertions(+), 33 deletions(-) diff --git a/litellm/proxy/anthropic_endpoints/gateway_endpoints.py b/litellm/proxy/anthropic_endpoints/gateway_endpoints.py index 8c937e4cbd4..6c58f2edc02 100644 --- a/litellm/proxy/anthropic_endpoints/gateway_endpoints.py +++ b/litellm/proxy/anthropic_endpoints/gateway_endpoints.py @@ -261,12 +261,6 @@ async def device_authorization(request: Request) -> JSONResponse: return JSONResponse(content=body.model_dump(exclude_none=True)) -def _session_team_id(chosen_team_id: str | None, teams: tuple[str, ...]) -> str | None: - if chosen_team_id in teams: - return chosen_team_id - return teams[0] if teams else None - - def _validate_login(flow: Mapping[str, object]) -> _GatewayLogin | _OAuthError: from litellm.proxy.management_endpoints.ui_sso import selected_cli_sso_team_detail @@ -278,8 +272,15 @@ def _validate_login(flow: Mapping[str, object]) -> _GatewayLogin | _OAuthError: status_code=400, error="invalid_grant", description="The login session is malformed; sign in again" ) - chosen_team_id: Final = flow.get(CLI_SSO_CHOSEN_TEAM_ID) - team_id: Final = _session_team_id(chosen_team_id if isinstance(chosen_team_id, str) else None, session_data.teams) + chosen: Final = flow.get(CLI_SSO_CHOSEN_TEAM_ID) + chosen_team_id: Final = chosen if isinstance(chosen, str) else None + if chosen_team_id is not None and chosen_team_id not in session_data.teams: + return _OAuthError( + status_code=400, + error="invalid_grant", + description="The team picked at sign-in is no longer one of your teams; sign in again", + ) + team_id: Final = chosen_team_id or next(iter(session_data.teams), None) selected_team: Final = selected_cli_sso_team_detail(team_details=session_data.team_details, team_id=team_id) if selected_team is None: return _OAuthError( diff --git a/litellm/proxy/common_utils/html_forms/native_client_consent.py b/litellm/proxy/common_utils/html_forms/native_client_consent.py index 4c7e7a5fedd..aeadc93221c 100644 --- a/litellm/proxy/common_utils/html_forms/native_client_consent.py +++ b/litellm/proxy/common_utils/html_forms/native_client_consent.py @@ -74,7 +74,7 @@ button {{ flex: 1; padding: 10px; border-radius: 6px; font-size: 15px; cursor: p """ -def team_select_field(teams: Sequence[tuple[str, str]]) -> str: +def team_select_field(teams: Sequence[tuple[str, str]], *, require_choice: bool = False) -> str: if not teams: return "" if len(teams) == 1: @@ -83,9 +83,12 @@ def team_select_field(teams: Sequence[tuple[str, str]]) -> str: f'' f"
Requests are attributed to team {escape(team_label)}.
" ) + placeholder: Final = '' if require_choice else "" options: Final = "".join( f'' for team_id, team_label in teams ) + required: Final = " required" if require_choice else "" return ( - f'' + '' + f'' ) diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index f347a742ae3..790556e5b7b 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -16,6 +16,7 @@ import json import os import re import secrets +from collections import Counter from collections.abc import Awaitable, Callable, Mapping, Sequence from copy import deepcopy from html import escape @@ -748,7 +749,7 @@ def _render_cli_sso_verification_page({instructions}