Fix Azure scope fallback (#4469) (#4662)

(cherry picked from commit 1d932cc778)

Co-authored-by: nate nowack <thrast36@gmail.com>
This commit is contained in:
Jeremiah Lowin 2026-07-27 13:10:27 -04:00 committed by GitHub
commit aa8ab1ee66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 34 additions and 7 deletions

View file

@ -501,8 +501,12 @@ class AzureProvider(OAuthProxy):
Returns:
List of scopes for Azure token endpoint
"""
# Prefix scopes for this API
prefixed_scopes = self._prefix_scopes_for_azure(scopes or [])
# Prefix scopes for this API. Some clients omit the scope parameter on
# the MCP authorization request; use the provider's configured scopes
# just like the authorize URL path does.
prefixed_scopes = self._prefix_scopes_for_azure(
scopes or self.required_scopes or []
)
# Add OIDC scopes only (not other API scopes) to avoid AADSTS28000
if self.additional_authorize_scopes:
@ -528,9 +532,13 @@ class AzureProvider(OAuthProxy):
"""
logger.debug("Base scopes from storage: %s", scopes)
# Some clients omit the scope parameter on the MCP authorization request;
# use the provider's configured scopes just like the authorize URL path does.
requested_scopes = scopes or self.required_scopes or []
# Filter out any additional_authorize_scopes that may have been stored
additional_scopes_set = set(self.additional_authorize_scopes or [])
base_scopes = [s for s in scopes if s not in additional_scopes_set]
base_scopes = [s for s in requested_scopes if s not in additional_scopes_set]
# Prefix base scopes with identifier_uri for Azure
prefixed_scopes = self._prefix_scopes_for_azure(base_scopes)

View file

@ -445,6 +445,25 @@ class TestAzureProvider:
assert "Mail.Read" in upstream_url
assert "User.Read" in upstream_url
def test_prepare_scopes_for_token_exchange_falls_back_to_required_scopes(
self, memory_storage: MemoryStore
):
"""Clients may omit scope; Azure still needs an API scope with offline_access."""
provider = AzureProvider(
client_id="test_client",
client_secret="test_secret",
tenant_id="test-tenant",
base_url="https://myserver.com",
identifier_uri="api://my-api",
required_scopes=["read"],
jwt_signing_key="test-secret",
client_storage=memory_storage,
)
result = provider._prepare_scopes_for_token_exchange([])
assert result == ["api://my-api/read", "offline_access"]
def test_base_authority_defaults_to_public_cloud(self, memory_storage: MemoryStore):
"""Test that base_authority defaults to login.microsoftonline.com."""
provider = AzureProvider(
@ -724,10 +743,10 @@ class TestAzureProvider:
"https://graph.microsoft.com/.default" in result
) # Not prefixed (contains ://)
def test_prepare_scopes_for_upstream_refresh_empty_scopes(
def test_prepare_scopes_for_upstream_refresh_empty_scopes_falls_back_to_required(
self, memory_storage: MemoryStore
):
"""Test behavior with empty scopes list."""
"""Clients may omit scope; refresh should still request the configured API scope."""
provider = AzureProvider(
client_id="test_client",
client_secret="test_secret",
@ -740,13 +759,13 @@ class TestAzureProvider:
client_storage=memory_storage,
)
# Empty scopes should still add OIDC scopes (not User.Read)
result = provider._prepare_scopes_for_upstream_refresh([])
assert "api://my-api/read" in result
assert "User.Read" not in result # Not OIDC
assert "openid" in result
assert "offline_access" in result # Auto-included
assert len(result) == 2 # Only OIDC scopes: openid + offline_access
assert len(result) == 3
def test_prepare_scopes_for_upstream_refresh_no_additional_scopes(
self, memory_storage: MemoryStore