fix(proxy): keep stored user_email when SSO login carries no email claim

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yassin 2026-10-01 22:01:41 +00:00
parent a9e9a9d6fc
commit 47c65cecff
2 changed files with 64 additions and 5 deletions

View file

@ -1894,11 +1894,12 @@ def _build_sso_user_update_data(
existing_user_alias: The user's current alias in the DB; only an empty alias is filled from SSO
Returns:
dict: Update data containing user_email, user_alias when newly available, and user_role if valid
dict: Update data containing user_email and user_alias when the IdP supplied them, and user_role if valid
"""
sso_user_email: Final = normalize_email(user_email)
sso_user_alias: Final = None if existing_user_alias else _get_sso_user_alias(result)
update_data: Final[dict[str, object]] = {
"user_email": normalize_email(user_email),
**({"user_email": sso_user_email} if sso_user_email is not None else {}),
**({"user_alias": sso_user_alias} if sso_user_alias is not None else {}),
}
@ -3284,9 +3285,10 @@ class SSOAuthenticationHandler:
existing_user_alias=user_info.user_alias if isinstance(user_info, LiteLLM_UserTable) else None,
)
await _user_meta_db(UserRepository(prisma_client)).update_many(
where={"user_id": user_id}, data=update_data
)
if update_data:
await _user_meta_db(UserRepository(prisma_client)).update_many(
where={"user_id": user_id}, data=update_data
)
else:
verbose_proxy_logger.info("user not in DB, inserting user into LiteLLM DB")
# user not in DB, insert User into LiteLLM DB

View file

@ -1141,6 +1141,63 @@ async def test_upsert_sso_user_fills_user_alias_for_existing_user():
)
def test_build_sso_user_update_data_omits_email_when_idp_sends_none():
"""
A login without an email claim must not write user_email, so a stored email survives.
"""
from litellm.proxy.management_endpoints.types import CustomOpenID
from litellm.proxy.management_endpoints.ui_sso import _build_sso_user_update_data
sso_result = CustomOpenID(
id="S-1-5-21-adfs-user",
email=None,
display_name="Doe, Jane",
provider="generic",
team_ids=[],
)
update_data = _build_sso_user_update_data(
result=sso_result,
user_email=None,
user_id="S-1-5-21-adfs-user",
existing_user_alias=None,
)
assert update_data == {"user_alias": "Doe, Jane"}
@pytest.mark.asyncio
async def test_upsert_sso_user_keeps_stored_email_when_idp_sends_none():
"""
An existing user whose IdP token carries no email and no new name is left untouched on login.
"""
from litellm.proxy._types import LiteLLM_UserTable
from litellm.proxy.management_endpoints.types import CustomOpenID
from litellm.proxy.management_endpoints.ui_sso import SSOAuthenticationHandler
mock_prisma = MagicMock()
mock_prisma.db.litellm_usertable.update_many = AsyncMock()
existing_user = LiteLLM_UserTable(
user_id="S-1-5-21-adfs-user",
user_email="admin.typed@example.com",
user_role="internal_user",
user_alias="Doe, Jane",
)
sso_result = CustomOpenID(id="S-1-5-21-adfs-user", email=None, provider="generic", team_ids=[])
returned = await SSOAuthenticationHandler.upsert_sso_user(
result=sso_result,
user_info=existing_user,
user_email=None,
user_defined_values=None,
prisma_client=mock_prisma,
)
assert returned is existing_user
mock_prisma.db.litellm_usertable.update_many.assert_not_called()
@pytest.mark.asyncio
async def test_insert_sso_user_sets_user_alias_from_display_name():
"""