From 368bfabedce4478947318248b930c29efd5c1eca Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Mon, 8 Jun 2026 18:53:26 -0700 Subject: [PATCH] feat(identity): add LITELLM_OAUTH2_UNKNOWN_ROLE_DEFAULT escape hatch for OAuth2 unknown roles OAuth2 role mapping is fail-closed by default; unknown IdP roles raise ValueError instead of silently granting INTERNAL_USER access. Operators whose IdP returns custom role names not in LitellmUserRoles can set LITELLM_OAUTH2_UNKNOWN_ROLE_DEFAULT= to map unknown roles to that fallback. An invalid env value raises on first use so misconfiguration is loud, not silently downgraded. Resolves the reviewer split: Veria flagged the original INTERNAL_USER default as fail-open; Greptile flagged the pure-raise as breaking deployments with custom IdP role names. The opt-in flag preserves the secure default and gives operators an explicit escape hatch. --- litellm/identity/oauth2.py | 21 +++++++++++++++- .../identity/test_oauth2_builder.py | 24 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/litellm/identity/oauth2.py b/litellm/identity/oauth2.py index dc403ed33eb..c8238ead47c 100644 --- a/litellm/identity/oauth2.py +++ b/litellm/identity/oauth2.py @@ -6,10 +6,20 @@ an already-validated response payload into the carrier. from __future__ import annotations +import os from typing import Optional from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth +UNKNOWN_ROLE_DEFAULT_ENV = "LITELLM_OAUTH2_UNKNOWN_ROLE_DEFAULT" + + +def _unknown_role_fallback() -> Optional[LitellmUserRoles]: + raw = os.getenv(UNKNOWN_ROLE_DEFAULT_ENV) + if not raw: + return None + return LitellmUserRoles(raw) + def build_user_api_key_auth_from_oauth2_response( *, @@ -29,6 +39,11 @@ def build_user_api_key_auth_from_oauth2_response( Active-token validation, scope checks, and token-not-active rejection happen upstream in ``Oauth2Handler``; this builder trusts its input. + + Unknown IdP role values are rejected by default. Set + ``LITELLM_OAUTH2_UNKNOWN_ROLE_DEFAULT`` to a valid + ``LitellmUserRoles`` value to map unknown roles to that fallback; + an invalid env value raises on first use so misconfiguration is loud. """ user_id: Optional[str] = response_data.get(user_id_field_name) raw_role = response_data.get(user_role_field_name) @@ -41,7 +56,11 @@ def build_user_api_key_auth_from_oauth2_response( try: user_role = LitellmUserRoles(raw_role) except ValueError as e: - raise ValueError(f"Invalid OAuth2 role: {raw_role!r}") from e + fallback = _unknown_role_fallback() + if fallback is not None: + user_role = fallback + else: + raise ValueError(f"Invalid OAuth2 role: {raw_role!r}") from e return UserAPIKeyAuth( api_key=token, diff --git a/tests/test_litellm/identity/test_oauth2_builder.py b/tests/test_litellm/identity/test_oauth2_builder.py index f0ea3084dd6..7ca3546cc3e 100644 --- a/tests/test_litellm/identity/test_oauth2_builder.py +++ b/tests/test_litellm/identity/test_oauth2_builder.py @@ -66,6 +66,30 @@ def test_missing_role_field_stays_none(): assert uak.user_role is None +def test_unknown_idp_role_uses_env_fallback_when_set(monkeypatch): + monkeypatch.setenv("LITELLM_OAUTH2_UNKNOWN_ROLE_DEFAULT", "internal_user") + uak = build_user_api_key_auth_from_oauth2_response( + token="t", response_data={"sub": "u", "role": "custom-idp-role"} + ) + assert uak.user_role == LitellmUserRoles.INTERNAL_USER + + +def test_unknown_idp_role_fallback_with_invalid_env_value_fails(monkeypatch): + monkeypatch.setenv("LITELLM_OAUTH2_UNKNOWN_ROLE_DEFAULT", "not-a-real-role") + with pytest.raises(ValueError): + build_user_api_key_auth_from_oauth2_response( + token="t", response_data={"sub": "u", "role": "custom-idp-role"} + ) + + +def test_known_idp_role_ignores_env_fallback(monkeypatch): + monkeypatch.setenv("LITELLM_OAUTH2_UNKNOWN_ROLE_DEFAULT", "internal_user") + uak = build_user_api_key_auth_from_oauth2_response( + token="t", response_data={"sub": "u", "role": "proxy_admin"} + ) + assert uak.user_role == LitellmUserRoles.PROXY_ADMIN + + def test_token_is_hashed_into_token_field(): """The api_key is hashed by the UserAPIKeyAuth validator; the OAuth2 builder must not bypass that path."""