fix: resolve EntraOBOToken dependency injection through MultiAuth (#3609)

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
This commit is contained in:
Jeremy Simon 2026-03-25 15:58:19 +01:00 committed by GitHub
commit 492db9972f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 59 additions and 3 deletions

View file

@ -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,
)

View file

@ -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