From 1123f870b47cd64f81ac432f9b3cc8d8e4207000 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:46:54 +0000 Subject: [PATCH 1/2] fix: raise RefreshError for missing token in impersonated credentials Instead of crashing with a KeyError when the ID token is missing from the response (even if the status code is 200), raise a proper RefreshError. Fixes #1167 --- google/auth/impersonated_credentials.py | 9 ++- tests/test_issue_1167.py | 77 +++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 tests/test_issue_1167.py diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py index e2724382a..8ac0a420e 100644 --- a/google/auth/impersonated_credentials.py +++ b/google/auth/impersonated_credentials.py @@ -640,7 +640,14 @@ def refresh(self, request): "Error getting ID token: {}".format(response.json()) ) - id_token = response.json()["token"] + try: + id_token = response.json()["token"] + except (KeyError, ValueError) as caught_exc: + new_exc = exceptions.RefreshError( + "No ID token in response.", response.json() + ) + raise new_exc from caught_exc + self.token = id_token self.expiry = datetime.utcfromtimestamp( jwt.decode(id_token, verify=False)["exp"] diff --git a/tests/test_issue_1167.py b/tests/test_issue_1167.py new file mode 100644 index 000000000..787b5f50e --- /dev/null +++ b/tests/test_issue_1167.py @@ -0,0 +1,77 @@ +import unittest +from unittest.mock import MagicMock, patch +import json +import http.client as http_client +from google.auth import exceptions +from google.auth import impersonated_credentials +from google.auth import credentials + +class TestIDTokenCredentialsFix(unittest.TestCase): + def test_refresh_200_missing_token_raises_refresh_error(self): + # Setup + mock_source_creds = MagicMock(spec=credentials.Credentials) + mock_target_creds = MagicMock(spec=impersonated_credentials.Credentials) + mock_target_creds._source_credentials = mock_source_creds + mock_target_creds.universe_domain = "googleapis.com" + mock_target_creds.signer_email = "signer@example.com" + mock_target_creds._delegates = [] + + creds = impersonated_credentials.IDTokenCredentials( + target_credentials=mock_target_creds, + target_audience="aud", + include_email=True + ) + + # Mock AuthorizedSession + with patch("google.auth.transport.requests.AuthorizedSession") as MockSession: + mock_session_instance = MockSession.return_value + + # Mock Response 200 but missing token + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"not_token": "something"} + mock_session_instance.post.return_value = mock_response + + request = MagicMock() + + # Action & Assert + # Before the fix, this raised KeyError. Now it should raise RefreshError. + with self.assertRaises(exceptions.RefreshError) as cm: + creds.refresh(request) + + self.assertIn("No ID token in response", str(cm.exception)) + + def test_refresh_403_raises_refresh_error(self): + # Setup (same as before to ensure no regression) + mock_source_creds = MagicMock(spec=credentials.Credentials) + mock_target_creds = MagicMock(spec=impersonated_credentials.Credentials) + mock_target_creds._source_credentials = mock_source_creds + mock_target_creds.universe_domain = "googleapis.com" + mock_target_creds.signer_email = "signer@example.com" + mock_target_creds._delegates = [] + + creds = impersonated_credentials.IDTokenCredentials( + target_credentials=mock_target_creds, + target_audience="aud", + include_email=True + ) + + # Mock AuthorizedSession + with patch("google.auth.transport.requests.AuthorizedSession") as MockSession: + mock_session_instance = MockSession.return_value + + # Mock Response 403 + mock_response = MagicMock() + mock_response.status_code = 403 + mock_response.json.return_value = {"error": "Permission denied"} + mock_session_instance.post.return_value = mock_response + + request = MagicMock() + + with self.assertRaises(exceptions.RefreshError) as cm: + creds.refresh(request) + + self.assertIn("Error getting ID token", str(cm.exception)) + +if __name__ == "__main__": + unittest.main() From c746d1a3ee67961609cfe7d8b7dd3c890b255c0f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:59:12 +0000 Subject: [PATCH 2/2] fix: raise RefreshError for missing token in impersonated credentials Instead of crashing with a KeyError when the ID token is missing from the response (even if the status code is 200), raise a proper RefreshError. Fixes #1167 --- tests/test_impersonated_credentials.py | 22 ++++++++ tests/test_issue_1167.py | 77 -------------------------- 2 files changed, 22 insertions(+), 77 deletions(-) delete mode 100644 tests/test_issue_1167.py diff --git a/tests/test_impersonated_credentials.py b/tests/test_impersonated_credentials.py index ab92e0bd9..b4566576e 100644 --- a/tests/test_impersonated_credentials.py +++ b/tests/test_impersonated_credentials.py @@ -761,6 +761,28 @@ def test_refresh_failure(self): assert excinfo.match("Error getting ID token") + def test_refresh_failure_missing_token_in_200_response(self): + credentials = self.make_credentials(lifetime=None) + credentials.expiry = None + credentials.token = "token" + id_creds = impersonated_credentials.IDTokenCredentials( + credentials, target_audience="audience" + ) + + # Response has 200 OK status but is missing the "token" field + response = mock.create_autospec(transport.Response, instance=False) + response.status_code = http_client.OK + response.json = mock.Mock(return_value={"not_token": "something"}) + + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.post", + return_value=response, + ): + with pytest.raises(exceptions.RefreshError) as excinfo: + id_creds.refresh(None) + + assert excinfo.match("No ID token in response") + def test_refresh_failure_http_error(self, mock_donor_credentials): credentials = self.make_credentials(lifetime=None) diff --git a/tests/test_issue_1167.py b/tests/test_issue_1167.py deleted file mode 100644 index 787b5f50e..000000000 --- a/tests/test_issue_1167.py +++ /dev/null @@ -1,77 +0,0 @@ -import unittest -from unittest.mock import MagicMock, patch -import json -import http.client as http_client -from google.auth import exceptions -from google.auth import impersonated_credentials -from google.auth import credentials - -class TestIDTokenCredentialsFix(unittest.TestCase): - def test_refresh_200_missing_token_raises_refresh_error(self): - # Setup - mock_source_creds = MagicMock(spec=credentials.Credentials) - mock_target_creds = MagicMock(spec=impersonated_credentials.Credentials) - mock_target_creds._source_credentials = mock_source_creds - mock_target_creds.universe_domain = "googleapis.com" - mock_target_creds.signer_email = "signer@example.com" - mock_target_creds._delegates = [] - - creds = impersonated_credentials.IDTokenCredentials( - target_credentials=mock_target_creds, - target_audience="aud", - include_email=True - ) - - # Mock AuthorizedSession - with patch("google.auth.transport.requests.AuthorizedSession") as MockSession: - mock_session_instance = MockSession.return_value - - # Mock Response 200 but missing token - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.json.return_value = {"not_token": "something"} - mock_session_instance.post.return_value = mock_response - - request = MagicMock() - - # Action & Assert - # Before the fix, this raised KeyError. Now it should raise RefreshError. - with self.assertRaises(exceptions.RefreshError) as cm: - creds.refresh(request) - - self.assertIn("No ID token in response", str(cm.exception)) - - def test_refresh_403_raises_refresh_error(self): - # Setup (same as before to ensure no regression) - mock_source_creds = MagicMock(spec=credentials.Credentials) - mock_target_creds = MagicMock(spec=impersonated_credentials.Credentials) - mock_target_creds._source_credentials = mock_source_creds - mock_target_creds.universe_domain = "googleapis.com" - mock_target_creds.signer_email = "signer@example.com" - mock_target_creds._delegates = [] - - creds = impersonated_credentials.IDTokenCredentials( - target_credentials=mock_target_creds, - target_audience="aud", - include_email=True - ) - - # Mock AuthorizedSession - with patch("google.auth.transport.requests.AuthorizedSession") as MockSession: - mock_session_instance = MockSession.return_value - - # Mock Response 403 - mock_response = MagicMock() - mock_response.status_code = 403 - mock_response.json.return_value = {"error": "Permission denied"} - mock_session_instance.post.return_value = mock_response - - request = MagicMock() - - with self.assertRaises(exceptions.RefreshError) as cm: - creds.refresh(request) - - self.assertIn("Error getting ID token", str(cm.exception)) - -if __name__ == "__main__": - unittest.main()