From 492db9972f77c74c6277a44c44cf4e4a9921fb87 Mon Sep 17 00:00:00 2001 From: Jeremy Simon Date: Wed, 25 Mar 2026 15:58:19 +0100 Subject: [PATCH] fix: resolve EntraOBOToken dependency injection through MultiAuth (#3609) Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> --- src/fastmcp/server/auth/providers/azure.py | 19 +++++++- .../auth/providers/test_azure_scopes.py | 43 ++++++++++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/fastmcp/server/auth/providers/azure.py b/src/fastmcp/server/auth/providers/azure.py index e419c3291..00c1ab154 100644 --- a/src/fastmcp/server/auth/providers/azure.py +++ b/src/fastmcp/server/auth/providers/azure.py @@ -14,6 +14,7 @@ import httpx from key_value.aio.protocols import AsyncKeyValue from fastmcp.dependencies import Dependency +from fastmcp.server.auth.auth import MultiAuth from fastmcp.server.auth.oauth_proxy import OAuthProxy from fastmcp.server.auth.providers.jwt import JWTVerifier from fastmcp.utilities.auth import decode_jwt_payload, parse_scopes @@ -24,6 +25,8 @@ if TYPE_CHECKING: from mcp.server.auth.provider import AuthorizationParams from mcp.shared.auth import OAuthClientInformationFull + from fastmcp.server.auth.auth import AuthProvider + logger = get_logger(__name__) # Standard OIDC scopes that should never be prefixed with identifier_uri. @@ -663,6 +666,17 @@ def _require_azure_identity(feature: str) -> None: ) from e +def _find_azure_provider(auth: AuthProvider | None) -> AzureProvider | None: + """Extract an AzureProvider from an auth provider, unwrapping MultiAuth if needed.""" + if isinstance(auth, AzureProvider): + return auth + + if isinstance(auth, MultiAuth) and isinstance(auth.server, AzureProvider): + return auth.server + + return None + + class _EntraOBOToken(Dependency[str]): """Dependency that performs OBO token exchange for Microsoft Entra. @@ -687,13 +701,14 @@ class _EntraOBOToken(Dependency[str]): ) server = get_server() - if not isinstance(server.auth, AzureProvider): + azure_provider = _find_azure_provider(server.auth) + if azure_provider is None: raise RuntimeError( "EntraOBOToken requires an AzureProvider as the auth provider. " f"Current provider: {type(server.auth).__name__}" ) - credential = await server.auth.get_obo_credential( + credential = await azure_provider.get_obo_credential( user_assertion=access_token.token, ) diff --git a/tests/server/auth/providers/test_azure_scopes.py b/tests/server/auth/providers/test_azure_scopes.py index 9b518a1ff..edbfa3165 100644 --- a/tests/server/auth/providers/test_azure_scopes.py +++ b/tests/server/auth/providers/test_azure_scopes.py @@ -3,12 +3,14 @@ import pytest from key_value.aio.stores.memory import MemoryStore +from fastmcp.server.auth.auth import MultiAuth from fastmcp.server.auth.providers.azure import ( OIDC_SCOPES, AzureJWTVerifier, AzureProvider, + _find_azure_provider, ) -from fastmcp.server.auth.providers.jwt import RSAKeyPair +from fastmcp.server.auth.providers.jwt import RSAKeyPair, StaticTokenVerifier @pytest.fixture @@ -725,3 +727,42 @@ class TestAzureOBOIntegration: dep = _EntraOBOToken(["scope"]) assert isinstance(dep, Dependency) + + +class TestFindAzureProvider: + """Tests for _find_azure_provider helper used by EntraOBOToken.""" + + def test_returns_azure_provider_directly(self, memory_storage): + """When auth is an AzureProvider, return it directly.""" + provider = AzureProvider( + tenant_id="test-tenant", + client_id="test-client", + client_secret="test-secret", + client_storage=memory_storage, + base_url="https://example.com", + required_scopes=["read"], + ) + assert _find_azure_provider(provider) is provider + + def test_unwraps_multiauth_with_azure_server(self, memory_storage): + """When auth is a MultiAuth wrapping an AzureProvider, return the inner provider.""" + provider = AzureProvider( + tenant_id="test-tenant", + client_id="test-client", + client_secret="test-secret", + client_storage=memory_storage, + base_url="https://example.com", + required_scopes=["read"], + ) + multi = MultiAuth(server=provider) + assert _find_azure_provider(multi) is provider + + def test_returns_none_for_no_auth(self): + """When auth is None, return None.""" + assert _find_azure_provider(None) is None + + def test_returns_none_for_multiauth_without_azure_server(self): + """When MultiAuth has no server or a non-Azure server, return None.""" + verifier = StaticTokenVerifier(tokens={"t": {"client_id": "c", "scopes": []}}) + multi = MultiAuth(verifiers=[verifier]) + assert _find_azure_provider(multi) is None