From 4ed2ed90c7e8dbf0842262754c2658b7ff50d705 Mon Sep 17 00:00:00 2001 From: chelsealong Date: Thu, 24 Sep 2026 17:01:07 +0000 Subject: [PATCH] fix(auth): tolerate trailing slash on discovered OAuth2 issuer OAuth2DiscoveryManager.discover_auth_server_metadata() compares the discovery document's issuer to the requested issuer_url to defend against MIX-UP attacks, but only stripped the trailing slash from issuer_url. Servers that return an issuer with a trailing slash (e.g. FastMCP with GoogleProvider) never match, so discovery silently returns None. Fixes #7265 --- src/google/adk/auth/oauth2_discovery.py | 2 +- tests/unittests/auth/test_oauth2_discovery.py | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/google/adk/auth/oauth2_discovery.py b/src/google/adk/auth/oauth2_discovery.py index ef509102a12..e9220e23ead 100644 --- a/src/google/adk/auth/oauth2_discovery.py +++ b/src/google/adk/auth/oauth2_discovery.py @@ -89,7 +89,7 @@ async def discover_auth_server_metadata( response.raise_for_status() metadata = AuthorizationServerMetadata.model_validate(response.json()) # Validate issuer to defend against MIX-UP attacks - if metadata.issuer == issuer_url.rstrip("/"): + if metadata.issuer.rstrip("/") == issuer_url.rstrip("/"): return metadata else: logger.warning( diff --git a/tests/unittests/auth/test_oauth2_discovery.py b/tests/unittests/auth/test_oauth2_discovery.py index 48d755337d6..88a87cd8c34 100644 --- a/tests/unittests/auth/test_oauth2_discovery.py +++ b/tests/unittests/auth/test_oauth2_discovery.py @@ -208,6 +208,29 @@ async def test_discover_auth_server_metadata_discard_mismatched_issuer( ), ]) + @patch("httpx.AsyncClient.get") + @pytest.mark.asyncio + async def test_discover_auth_server_metadata_issuer_trailing_slash( + self, + mock_get, + auth_server_metadata, + ): + """Test issuer match tolerates a trailing slash on the returned issuer. + + Some servers (e.g. FastMCP with GoogleProvider) return an issuer with a + trailing slash even though the requested issuer_url has none. + """ + + auth_server_metadata.issuer = "https://auth.example.com/" + mock_get.side_effect = [ + self.mock_success_response(auth_server_metadata), + ] + discovery_manager = OAuth2DiscoveryManager() + result = await discovery_manager.discover_auth_server_metadata( + "https://auth.example.com" + ) + assert result == auth_server_metadata + @patch("httpx.AsyncClient.get") @pytest.mark.asyncio async def test_discover_resource_metadata_failed(