From 74a609e9cb7da69fd745d2ec60d9a2ee2551ffee Mon Sep 17 00:00:00 2001 From: doug <110487462+doughayden@users.noreply.github.com> Date: Wed, 27 May 2026 20:34:08 -0400 Subject: [PATCH] fix(auth): omit scope from OAuth2 token requests - Stop setting scope on the shared create_oauth2_session helper - Drops scope from both token exchange and refresh requests - Neither needs scope (RFC 6749 4.1.3, 6); some providers reject it - Auth URL construction in auth_handler.py keeps scope, unchanged - Add body-level tests asserting refresh and exchange omit scope --- src/google/adk/auth/oauth2_credential_util.py | 6 +- .../auth/test_oauth2_credential_util.py | 76 +++++++++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/google/adk/auth/oauth2_credential_util.py b/src/google/adk/auth/oauth2_credential_util.py index d0d1255fbe..978d645a05 100644 --- a/src/google/adk/auth/oauth2_credential_util.py +++ b/src/google/adk/auth/oauth2_credential_util.py @@ -49,7 +49,6 @@ def create_oauth2_session( logger.warning("OpenIdConnect scheme missing token_endpoint") return None, None token_endpoint = auth_scheme.token_endpoint - scopes = auth_scheme.scopes or [] elif isinstance(auth_scheme, OAuth2): # Support both authorization code and client credentials flows if ( @@ -57,13 +56,11 @@ def create_oauth2_session( and auth_scheme.flows.authorizationCode.tokenUrl ): token_endpoint = auth_scheme.flows.authorizationCode.tokenUrl - scopes = list(auth_scheme.flows.authorizationCode.scopes.keys()) elif ( auth_scheme.flows.clientCredentials and auth_scheme.flows.clientCredentials.tokenUrl ): token_endpoint = auth_scheme.flows.clientCredentials.tokenUrl - scopes = list(auth_scheme.flows.clientCredentials.scopes.keys()) else: logger.warning( "OAuth2 scheme missing required flow configuration. Expected either" @@ -84,11 +81,12 @@ def create_oauth2_session( ): return None, None + # Scope is intentionally omitted: token exchange and refresh don't require + # it per RFC 6749, and some providers reject it on these requests. return ( OAuth2Session( auth_credential.oauth2.client_id, auth_credential.oauth2.client_secret, - scope=" ".join(scopes), redirect_uri=auth_credential.oauth2.redirect_uri, state=auth_credential.oauth2.state, token_endpoint_auth_method=auth_credential.oauth2.token_endpoint_auth_method, diff --git a/tests/unittests/auth/test_oauth2_credential_util.py b/tests/unittests/auth/test_oauth2_credential_util.py index b9d4da6711..17a5200255 100644 --- a/tests/unittests/auth/test_oauth2_credential_util.py +++ b/tests/unittests/auth/test_oauth2_credential_util.py @@ -207,6 +207,82 @@ def test_create_oauth2_session_oauth2_scheme_with_token_endpoint_auth_method( assert token_endpoint == "https://example.com/token" assert client.token_endpoint_auth_method == "client_secret_jwt" + def _oauth2_scheme_with_scopes(self): + """Build an OAuth2 scheme that declares scopes.""" + return OAuth2( + type_="oauth2", + flows=OAuthFlows( + authorizationCode=OAuthFlowAuthorizationCode( + authorizationUrl="https://example.com/auth", + tokenUrl="https://example.com/token", + scopes={"read": "Read access", "write": "Write access"}, + ) + ), + ) + + def _capturing_post(self, captured): + """Stub for OAuth2Session.post that records the token-request body.""" + + def _post(*args, **kwargs): + captured["data"] = kwargs.get("data") + response = Mock() + response.status_code = 200 + response.json.return_value = { + "access_token": "new_access_token", + "token_type": "Bearer", + "expires_in": 3600, + "refresh_token": "new_refresh_token", + } + return response + + return _post + + def test_refresh_request_omits_scope(self): + """Refresh requests must not carry scope (some providers reject it).""" + credential = AuthCredential( + auth_type=AuthCredentialTypes.OAUTH2, + oauth2=OAuth2Auth( + client_id="test_client_id", + client_secret="test_client_secret", + redirect_uri="https://example.com/callback", + ), + ) + + client, token_endpoint = create_oauth2_session( + self._oauth2_scheme_with_scopes(), credential + ) + assert client is not None + + captured = {} + client.post = self._capturing_post(captured) + client.refresh_token(token_endpoint, refresh_token="old_refresh_token") + + assert "scope" not in captured["data"] + + def test_token_exchange_omits_scope(self): + """Authorization-code exchange must not carry scope (it is redundant).""" + credential = AuthCredential( + auth_type=AuthCredentialTypes.OAUTH2, + oauth2=OAuth2Auth( + client_id="test_client_id", + client_secret="test_client_secret", + redirect_uri="https://example.com/callback", + ), + ) + + client, token_endpoint = create_oauth2_session( + self._oauth2_scheme_with_scopes(), credential + ) + assert client is not None + + captured = {} + client.post = self._capturing_post(captured) + client.fetch_token( + token_endpoint, grant_type="authorization_code", code="test_code" + ) + + assert "scope" not in captured["data"] + def test_update_credential_with_tokens(self): """Test update_credential_with_tokens function.""" credential = AuthCredential(