From 08b01333822ce90c9c920e6a3abb3ec1dd5cdacf Mon Sep 17 00:00:00 2001 From: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com> Date: Sat, 26 Sep 2026 12:47:35 -0700 Subject: [PATCH] fix(agents): reject whitespace-only delegated scope claims --- litellm/proxy/agent_endpoints/managed_identity.py | 3 ++- litellm/types/proxy/agent_identity.py | 7 +++++-- .../proxy/agent_endpoints/test_managed_identity.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/agent_endpoints/managed_identity.py b/litellm/proxy/agent_endpoints/managed_identity.py index a0ee817db02..3947542b1f5 100644 --- a/litellm/proxy/agent_endpoints/managed_identity.py +++ b/litellm/proxy/agent_endpoints/managed_identity.py @@ -32,7 +32,8 @@ def classify_agent_subject( if isinstance(scope, str) and scope: if allowed_mode == "autonomous" or oid == binding.service_principal_id or claims.get("idtyp") == "app": return AgentIdentityFailure(message="Delegated token contradicts the configured agent identity or mode") - if not frozenset(binding.required_scopes).issubset(scope.split()): + granted_scopes: Final = frozenset(scope.split()) + if not granted_scopes or not frozenset(binding.required_scopes).issubset(granted_scopes): return AgentIdentityFailure(message="Token lacks the required delegated scopes") return AgentSubject(kind="delegated_subject", oid=oid, mode="delegated") if allowed_mode == "delegated" or oid != binding.service_principal_id or claims.get("idtyp") == "user": diff --git a/litellm/types/proxy/agent_identity.py b/litellm/types/proxy/agent_identity.py index 11b79982573..a7fe0be37e1 100644 --- a/litellm/types/proxy/agent_identity.py +++ b/litellm/types/proxy/agent_identity.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import Literal, TypeAlias from uuid import UUID -from pydantic import BaseModel, ConfigDict, field_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator AgentExecutionMode: TypeAlias = Literal["autonomous", "delegated", "both"] @@ -15,7 +15,10 @@ class EntraIdentityConfig(BaseModel): client_id: str service_principal_id: str | None = None required_roles: tuple[str, ...] = () - required_scopes: tuple[str, ...] = ("user_impersonation",) + required_scopes: tuple[str, ...] = Field( + default=("user_impersonation",), + description="Required delegated scopes. An empty list accepts any nonempty scope granted for this gateway.", + ) @field_validator("tenant_id", "client_id", "service_principal_id") @classmethod diff --git a/tests/test_litellm/proxy/agent_endpoints/test_managed_identity.py b/tests/test_litellm/proxy/agent_endpoints/test_managed_identity.py index 7d8db473f25..38c1d1aa40d 100644 --- a/tests/test_litellm/proxy/agent_endpoints/test_managed_identity.py +++ b/tests/test_litellm/proxy/agent_endpoints/test_managed_identity.py @@ -121,7 +121,7 @@ def test_empty_required_scopes_allow_valid_delegated_scope(mode: AgentExecutionM assert result == AgentSubject(kind="delegated_subject", oid=HUMAN, mode="delegated") -@pytest.mark.parametrize("scope", [None, "", 42]) +@pytest.mark.parametrize("scope", [None, "", " \t ", 42]) def test_empty_requirements_do_not_make_a_scope_less_human_token_valid(scope: object) -> None: binding: Final = BINDING.model_copy(update={"required_scopes": ()}) result: Final = classify_agent_subject(binding, claims(oid=HUMAN, scp=scope), "both")