diff --git a/src/google/adk/auth/auth_tool.py b/src/google/adk/auth/auth_tool.py index 820540ef12..cef9e3fe40 100644 --- a/src/google/adk/auth/auth_tool.py +++ b/src/google/adk/auth/auth_tool.py @@ -129,6 +129,7 @@ def get_credential_key(self): auth_credential.oauth2.refresh_token = None auth_credential.oauth2.expires_at = None auth_credential.oauth2.expires_in = None + auth_credential.oauth2.redirect_uri = None credential_name = ( f"{auth_credential.auth_type.value}_{_stable_model_digest(auth_credential)}" if auth_credential diff --git a/src/google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py b/src/google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py index 0d78a5759b..e415b9ce24 100644 --- a/src/google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py +++ b/src/google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py @@ -70,6 +70,7 @@ def _get_legacy_credential_key( auth_credential.oauth2.refresh_token = None auth_credential.oauth2.expires_at = None auth_credential.oauth2.expires_in = None + auth_credential.oauth2.redirect_uri = None scheme_name = ( f"{auth_scheme.type_.name}_{self._legacy_stable_digest(auth_scheme.model_dump_json())}" if auth_scheme @@ -99,6 +100,7 @@ def get_credential_key( auth_credential.oauth2.refresh_token = None auth_credential.oauth2.expires_at = None auth_credential.oauth2.expires_in = None + auth_credential.oauth2.redirect_uri = None scheme_name = ( f"{auth_scheme.type_.name}_{_stable_model_digest(auth_scheme)}" if auth_scheme diff --git a/tests/unittests/auth/test_auth_config.py b/tests/unittests/auth/test_auth_config.py index ab5f6b584c..a4cd1c27bf 100644 --- a/tests/unittests/auth/test_auth_config.py +++ b/tests/unittests/auth/test_auth_config.py @@ -176,3 +176,41 @@ def test_credential_key_with_custom_auth_scheme(): key = custom_config.credential_key assert key.startswith("adk_mock_custom_type_") assert len(key) > len("adk_mock_custom_type_") + + +def test_credential_key_is_stable_across_redirect_uri(oauth2_auth_scheme): + """AuthConfig.credential_key should be invariant under redirect_uri changes. + + redirect_uri is deployment configuration (which callback URL the auth + server should redirect to), not part of the credential identity. Two + AuthConfig instances built from credentials that share the same client_id, + client_secret, and scopes but differ only in redirect_uri should produce + the same credential_key. + """ + credential_local = AuthCredential( + auth_type=AuthCredentialTypes.OAUTH2, + oauth2=OAuth2Auth( + client_id="client", + client_secret="secret", + redirect_uri="http://localhost:8001/oauth2callback", + ), + ) + credential_deployed = AuthCredential( + auth_type=AuthCredentialTypes.OAUTH2, + oauth2=OAuth2Auth( + client_id="client", + client_secret="secret", + redirect_uri="https://deployed.example.com/oauth2callback", + ), + ) + + config_local = AuthConfig( + auth_scheme=oauth2_auth_scheme, + raw_auth_credential=credential_local, + ) + config_deployed = AuthConfig( + auth_scheme=oauth2_auth_scheme, + raw_auth_credential=credential_deployed, + ) + + assert config_local.credential_key == config_deployed.credential_key diff --git a/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_tool_auth_handler.py b/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_tool_auth_handler.py index d32fc132da..bd56195126 100644 --- a/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_tool_auth_handler.py +++ b/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_tool_auth_handler.py @@ -353,3 +353,65 @@ async def test_refreshed_credential_is_persisted_to_store( assert persisted is not None assert persisted.oauth2.access_token == 'new_access_token' assert persisted.oauth2.refresh_token == 'new_refresh_token' + + +def test_credential_key_is_stable_across_redirect_uri(): + """get_credential_key should be invariant under redirect_uri changes. + + redirect_uri is deployment configuration (which callback URL the auth + server should redirect to), not part of the credential identity. Two + AuthCredential instances that share the same client_id, client_secret, + and scopes but differ only in redirect_uri should produce the same key. + """ + scheme, _ = get_mock_openid_scheme_credential() + credential_local = AuthCredential( + auth_type=AuthCredentialTypes.OAUTH2, + oauth2=OAuth2Auth( + client_id='client', + client_secret='secret', + redirect_uri='http://localhost:8001/oauth2callback', + ), + ) + credential_deployed = AuthCredential( + auth_type=AuthCredentialTypes.OAUTH2, + oauth2=OAuth2Auth( + client_id='client', + client_secret='secret', + redirect_uri='https://deployed.example.com/oauth2callback', + ), + ) + store = ToolContextCredentialStore(tool_context=create_mock_tool_context()) + + assert store.get_credential_key( + scheme, credential_local + ) == store.get_credential_key(scheme, credential_deployed) + + +def test_legacy_credential_key_is_stable_across_redirect_uri(): + """_get_legacy_credential_key should be invariant under redirect_uri changes. + + The same redirect_uri-strip behavior must apply to the legacy key path so + that already-stored credentials remain findable after the fix. + """ + scheme, _ = get_mock_openid_scheme_credential() + credential_local = AuthCredential( + auth_type=AuthCredentialTypes.OAUTH2, + oauth2=OAuth2Auth( + client_id='client', + client_secret='secret', + redirect_uri='http://localhost:8001/oauth2callback', + ), + ) + credential_deployed = AuthCredential( + auth_type=AuthCredentialTypes.OAUTH2, + oauth2=OAuth2Auth( + client_id='client', + client_secret='secret', + redirect_uri='https://deployed.example.com/oauth2callback', + ), + ) + store = ToolContextCredentialStore(tool_context=create_mock_tool_context()) + + assert store._get_legacy_credential_key( + scheme, credential_local + ) == store._get_legacy_credential_key(scheme, credential_deployed)