fix(cli): address review feedback on EXPERIMENTAL_UI_LOGIN gate and e2e test

Restore EXPERIMENTAL_UI_LOGIN=false as an explicit opt-out: operators who set it to false keep the old boundary; unset (new default) and true both attempt NaCl decryption, which fails closed for non-blob tokens.

In the e2e test: replace the silent Redis fallback with pytest.skip so a missing Redis instance is explicit rather than silently degrading to a directly-minted token. Write the seeded flow back as JSON (proxy reads it via json.loads on cache fetch) instead of Python repr, and build the updated flow immutably.
This commit is contained in:
Sameer Kankute 2026-06-23 16:48:32 +05:30
parent 36c2b029ab
commit ef8482dcd4
No known key found for this signature in database
2 changed files with 26 additions and 53 deletions

View file

@ -84,6 +84,7 @@ from litellm.proxy.utils import (
normalize_route_for_root_path,
)
from litellm.repositories.table_repositories import TeamMembershipRepository
from litellm.secret_managers.main import get_secret_bool
from litellm.types.services import ServiceTypes
try:
@ -1513,10 +1514,15 @@ async def _user_api_key_auth_builder(
valid_token = None
## Check UI/CLI Hash Key
# Embedded session tokens (lite login, UI dashboard) are encrypted
# blobs, never sk- keys. Attempt decryption only for non-sk- keys;
# decryption fails closed for anything that isn't a genuine blob.
if valid_token is None and not api_key.startswith("sk-"):
# Attempt decryption for non-sk- tokens unless the operator has
# explicitly set EXPERIMENTAL_UI_LOGIN=false to disable it.
# Unset (None) keeps the new default of always attempting decryption;
# decryption fails closed for anything that is not a genuine blob.
if (
valid_token is None
and not api_key.startswith("sk-")
and get_secret_bool("EXPERIMENTAL_UI_LOGIN") is not False
):
valid_token = ExperimentalUIJWTToken.get_key_object_from_ui_hash_key(
api_key
)

View file

@ -357,37 +357,6 @@ async def add_team_member(
return await response.json()
def _mint_cli_sso_token(
*,
user_id: str,
user_email: str,
team_id: str,
team_alias: str,
models: list[str],
) -> str:
"""
Mint the encrypted CLI session JWT returned by /sso/cli/poll after SSO login.
Uses the same code path as ExperimentalUIJWTToken.get_cli_jwt_auth_token.
"""
from litellm.proxy import proxy_server
from litellm.proxy._types import LiteLLM_UserTable, LitellmUserRoles
from litellm.proxy.auth.auth_checks import ExperimentalUIJWTToken
proxy_server.master_key = "sk-1234"
user_info = LiteLLM_UserTable(
user_id=user_id,
user_email=user_email,
user_role=LitellmUserRoles.INTERNAL_USER.value,
teams=[team_id],
models=models,
)
return ExperimentalUIJWTToken.get_cli_jwt_auth_token(
user_info=user_info,
team_id=team_id,
team_alias=team_alias,
)
async def obtain_cli_sso_token_via_poll_flow(
session,
*,
@ -424,13 +393,7 @@ async def obtain_cli_sso_token_via_poll_flow(
browser_complete_token=browser_complete_token,
)
if not seeded:
return _mint_cli_sso_token(
user_id=user_id,
user_email=user_email,
team_id=team_id,
team_alias=team_alias,
models=models,
)
pytest.skip("Shared Redis not available; skipping full poll-flow test")
async with session.post(
f"{PROXY_BASE}/sso/cli/complete/{login_id}",
@ -469,6 +432,7 @@ async def _seed_cli_sso_flow_in_shared_redis(
) -> bool:
"""Seed the CLI SSO flow in Redis when tests share the proxy's Redis instance."""
import ast
import json
import os
try:
@ -509,18 +473,21 @@ async def _seed_cli_sso_flow_in_shared_redis(
if not isinstance(flow, dict):
return False
flow["sso_complete"] = True
flow["user_code_verified"] = False
flow["session_data"] = {
"user_id": user_id,
"user_role": "internal_user",
"models": models,
"user_email": user_email,
"teams": [team_id],
"team_details": [{"team_id": team_id, "team_alias": team_alias}],
updated_flow = {
**flow,
"sso_complete": True,
"user_code_verified": False,
"session_data": {
"user_id": user_id,
"user_role": "internal_user",
"models": models,
"user_email": user_email,
"teams": [team_id],
"team_details": [{"team_id": team_id, "team_alias": team_alias}],
},
"browser_complete_token_hash": _hash_cli_sso_secret(browser_complete_token),
}
flow["browser_complete_token_hash"] = _hash_cli_sso_secret(browser_complete_token)
client.setex(cache_key, 600, str(flow))
client.setex(cache_key, 600, json.dumps(updated_flow))
return True