diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index 0d7094f66dc..e3df047f1ce 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -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 diff --git a/tests/unit/proxy/management_endpoints/test_ui_sso.py b/tests/unit/proxy/management_endpoints/test_ui_sso.py index 8ff0b24982f..73c7c5c4266 100644 --- a/tests/unit/proxy/management_endpoints/test_ui_sso.py +++ b/tests/unit/proxy/management_endpoints/test_ui_sso.py @@ -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(): """