From 564b50c5510f9881021d499e24aa98f60cbfc934 Mon Sep 17 00:00:00 2001 From: pandego <7780875+pandego@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:04:30 +0100 Subject: [PATCH 1/3] fix(auth): add PKCE params for MCP OAuth authorization --- tests/unittests/auth/test_auth_handler.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/unittests/auth/test_auth_handler.py b/tests/unittests/auth/test_auth_handler.py index c19a5d93fd..07c360ac06 100644 --- a/tests/unittests/auth/test_auth_handler.py +++ b/tests/unittests/auth/test_auth_handler.py @@ -66,6 +66,12 @@ def create_authorization_url(self, url, **kwargs): params = f"client_id={self.client_id}&scope={self.scope}" if kwargs.get("audience"): params += f"&audience={kwargs.get('audience')}" + if kwargs.get("code_challenge_method"): + params += ( + "&code_challenge_method=" + f"{kwargs.get('code_challenge_method')}" + ) + params += f"&code_challenge={kwargs.get('code_challenge')}" return f"{url}?{params}", "mock_state" def fetch_token( @@ -251,6 +257,19 @@ def test_generate_auth_uri_with_audience_and_prompt( assert "audience=test_audience" in result.oauth2.auth_uri + @patch("google.adk.auth.auth_handler.OAuth2Session", MockOAuth2Session) + def test_generate_auth_uri_with_pkce(self, auth_config): + """Test generating an auth URI with PKCE enabled.""" + auth_config.raw_auth_credential.oauth2.code_challenge_method = "S256" + handler = AuthHandler(auth_config) + + result = handler.generate_auth_uri() + + assert "code_challenge_method=S256" in result.oauth2.auth_uri + assert "code_challenge=" in result.oauth2.auth_uri + assert "code_verifier=" not in result.oauth2.auth_uri + assert result.oauth2.code_verifier + @patch("google.adk.auth.auth_handler.OAuth2Session", MockOAuth2Session) def test_generate_auth_uri_openid( self, openid_auth_scheme, oauth2_credentials From 2c690ae77f3dca266601aeb7923b9b5188a006a2 Mon Sep 17 00:00:00 2001 From: pandego <7780875+pandego@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:38:06 +0100 Subject: [PATCH 2/3] fix(auth): send PKCE code_challenge in auth URL --- tests/unittests/auth/test_auth_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unittests/auth/test_auth_handler.py b/tests/unittests/auth/test_auth_handler.py index 07c360ac06..917b8f162c 100644 --- a/tests/unittests/auth/test_auth_handler.py +++ b/tests/unittests/auth/test_auth_handler.py @@ -71,6 +71,7 @@ def create_authorization_url(self, url, **kwargs): "&code_challenge_method=" f"{kwargs.get('code_challenge_method')}" ) + if kwargs.get("code_challenge"): params += f"&code_challenge={kwargs.get('code_challenge')}" return f"{url}?{params}", "mock_state" From 849f35bc939f810502a63c405ac81e7e59c56e58 Mon Sep 17 00:00:00 2001 From: Michael <7780875+pandego@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:03:50 +0100 Subject: [PATCH 3/3] fix(auth): address mypy guards and pyink in PKCE flow --- src/google/adk/auth/auth_handler.py | 30 ++++++++++++----------- tests/unittests/auth/test_auth_handler.py | 13 +++++----- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/google/adk/auth/auth_handler.py b/src/google/adk/auth/auth_handler.py index 8e8f5d340b..e011f9a8dd 100644 --- a/src/google/adk/auth/auth_handler.py +++ b/src/google/adk/auth/auth_handler.py @@ -70,7 +70,7 @@ async def parse_and_store_auth_response(self, state: State) -> None: state[credential_key] = await self.exchange_auth_token() def _validate(self) -> None: - if not self.auth_scheme: + if not self.auth_config.auth_scheme: raise ValueError("auth_scheme is empty.") def get_auth_response(self, state: State) -> AuthCredential: @@ -160,7 +160,8 @@ def generate_auth_uri( auth_scheme = self.auth_config.auth_scheme auth_credential = self.auth_config.raw_auth_credential if not auth_credential or not auth_credential.oauth2: - raise ValueError("raw_auth_credential or oauth2 is empty") + raise ValueError("OAuth2 auth_credential with oauth2 config is required.") + oauth2_credential = auth_credential.oauth2 if isinstance(auth_scheme, OpenIdConnectWithConfig): authorization_endpoint = auth_scheme.authorization_endpoint @@ -189,24 +190,24 @@ def generate_auth_uri( scopes = list(scopes.keys()) client = OAuth2Session( - auth_credential.oauth2.client_id, - auth_credential.oauth2.client_secret, + oauth2_credential.client_id, + oauth2_credential.client_secret, scope=" ".join(scopes), - redirect_uri=auth_credential.oauth2.redirect_uri, - code_challenge_method=auth_credential.oauth2.code_challenge_method, + redirect_uri=oauth2_credential.redirect_uri, + code_challenge_method=oauth2_credential.code_challenge_method, ) params = { "access_type": "offline", "prompt": "consent", } - if auth_credential.oauth2.audience: - params["audience"] = auth_credential.oauth2.audience + if oauth2_credential.audience: + params["audience"] = oauth2_credential.audience # If using PKCE with S256, ensure a code_verifier exists. # If not provided in the credential, generate a cryptographically secure # random token of 48 characters (OAuth2 recommends 43-128 characters). - code_verifier = auth_credential.oauth2.code_verifier - method = auth_credential.oauth2.code_challenge_method + code_verifier = oauth2_credential.code_verifier + method = oauth2_credential.code_challenge_method if method: if method != "S256": @@ -222,9 +223,10 @@ def generate_auth_uri( ) exchanged_auth_credential = auth_credential.model_copy(deep=True) - exchanged_auth_credential.oauth2.auth_uri = uri - exchanged_auth_credential.oauth2.state = state - if code_verifier: - exchanged_auth_credential.oauth2.code_verifier = code_verifier + if exchanged_auth_credential.oauth2: + exchanged_auth_credential.oauth2.auth_uri = uri + exchanged_auth_credential.oauth2.state = state + if code_verifier: + exchanged_auth_credential.oauth2.code_verifier = code_verifier return exchanged_auth_credential diff --git a/tests/unittests/auth/test_auth_handler.py b/tests/unittests/auth/test_auth_handler.py index 917b8f162c..78571db4ee 100644 --- a/tests/unittests/auth/test_auth_handler.py +++ b/tests/unittests/auth/test_auth_handler.py @@ -66,13 +66,12 @@ def create_authorization_url(self, url, **kwargs): params = f"client_id={self.client_id}&scope={self.scope}" if kwargs.get("audience"): params += f"&audience={kwargs.get('audience')}" - if kwargs.get("code_challenge_method"): - params += ( - "&code_challenge_method=" - f"{kwargs.get('code_challenge_method')}" - ) - if kwargs.get("code_challenge"): - params += f"&code_challenge={kwargs.get('code_challenge')}" + code_challenge_method = self.extra_kwargs.get( + "code_challenge_method" + ) or kwargs.get("code_challenge_method") + if code_challenge_method: + params += f"&code_challenge_method={code_challenge_method}" + params += "&code_challenge=mock_code_challenge" return f"{url}?{params}", "mock_state" def fetch_token(