From 94a1d956a78acbc9e3e2391b8335a0907191a435 Mon Sep 17 00:00:00 2001 From: agrawalradhika-cell Date: Tue, 25 Aug 2026 20:56:24 -0700 Subject: [PATCH 1/7] feat: Add retry for cert rotation handling feat: Add retry for cert rotation handling --- .../google/auth/aio/transport/sessions.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/google-auth/google/auth/aio/transport/sessions.py b/packages/google-auth/google/auth/aio/transport/sessions.py index d88162667bda..c55be4670549 100644 --- a/packages/google-auth/google/auth/aio/transport/sessions.py +++ b/packages/google-auth/google/auth/aio/transport/sessions.py @@ -15,6 +15,8 @@ import asyncio from contextlib import asynccontextmanager import functools +import http.client as http_client +import logging import time from typing import Mapping, Optional, TYPE_CHECKING, Union import warnings @@ -26,6 +28,8 @@ from google.auth.exceptions import TimeoutError import google.auth.transport._mtls_helper +_LOGGER = logging.getLogger(__name__) + if TYPE_CHECKING: # pragma: NO COVER import aiohttp from aiohttp import ClientTimeout # type: ignore @@ -310,6 +314,32 @@ async def request( url, method, data, headers, actual_timeout, **kwargs ) ) + if response.status_code == http_client.UNAUTHORIZED: + if self.is_mtls: + call_cert_bytes, call_key_bytes, cached_fingerprint, current_cert_fingerprint = google.auth.transport._mtls_helper.check_parameters_for_unauthorized_response( + self._cached_cert + ) + if cached_fingerprint != current_cert_fingerprint: + try: + _LOGGER.info( + "Client certificate has changed, reconfiguring mTLS " + "channel." + ) + await self.configure_mtls_channel( + lambda: (call_cert_bytes, call_key_bytes) + ) + continue + except Exception as e: + _LOGGER.error("Failed to reconfigure mTLS channel: %s", e) + raise exceptions.MutualTLSChannelError( + "Failed to reconfigure mTLS channel" + ) from e + else: + _LOGGER.info( + "Skipping reconfiguration of mTLS channel because the client" + " certificate has not changed." + ) + if response.status_code not in transport.DEFAULT_RETRYABLE_STATUS_CODES: break return response From 420447c1d1bc71f29dc765c391f4bfb5ce6008bf Mon Sep 17 00:00:00 2001 From: agrawalradhika-cell Date: Tue, 25 Aug 2026 20:58:25 -0700 Subject: [PATCH 2/7] chore: Add tests for MTLS certificate rotation behavior --- .../tests/transport/aio/test_sessions_mtls.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/packages/google-auth/tests/transport/aio/test_sessions_mtls.py b/packages/google-auth/tests/transport/aio/test_sessions_mtls.py index b68766ca5b5d..740c6ac84ba2 100644 --- a/packages/google-auth/tests/transport/aio/test_sessions_mtls.py +++ b/packages/google-auth/tests/transport/aio/test_sessions_mtls.py @@ -344,3 +344,79 @@ async def test_configure_mtls_channel_close_exception_does_not_abort(self): assert session._is_mtls is True assert session._cached_cert == b"fake_cert_data" await session.close() + + @pytest.mark.asyncio + async def test_cert_rotation_failure_raises_error(self): + mock_creds = mock.AsyncMock(spec=credentials.Credentials) + mock_creds.before_request = mock.AsyncMock(return_value=None) + + mock_auth_req = mock.AsyncMock() + mock_resp = mock.Mock() + import http.client as http_client + mock_resp.status_code = http_client.UNAUTHORIZED + mock_auth_req.return_value = mock_resp + + session = sessions.AsyncAuthorizedSession(mock_creds, auth_request=mock_auth_req) + session._is_mtls = True + session._cached_cert = b"old_cert" + + new_cert = b"new_cert" + new_key = b"new_key" + + with mock.patch("google.auth.transport._mtls_helper.check_parameters_for_unauthorized_response") as mock_check, \ + mock.patch.object(session, "configure_mtls_channel", new_callable=mock.AsyncMock) as mock_conf: + + mock_check.return_value = (new_cert, new_key, b"old_fp", b"new_fp") + mock_conf.side_effect = Exception("Failed to reconfigure") + + with pytest.raises(exceptions.MutualTLSChannelError): + await session.request("GET", "http://example.com") + + mock_check.assert_called_once() + mock_conf.assert_called_once() + + @pytest.mark.asyncio + async def test_cert_rotation_check_params_fails(self): + mock_creds = mock.AsyncMock(spec=credentials.Credentials) + mock_auth_req = mock.AsyncMock() + mock_resp = mock.Mock() + import http.client as http_client + mock_resp.status_code = http_client.UNAUTHORIZED + mock_auth_req.return_value = mock_resp + + session = sessions.AsyncAuthorizedSession(mock_creds, auth_request=mock_auth_req) + session._is_mtls = True + session._cached_cert = b"cached_cert" + + with mock.patch( + "google.auth.transport._mtls_helper.check_parameters_for_unauthorized_response", + side_effect=Exception("check_params failed"), + ) as mock_check_params: + with pytest.raises(Exception, match="check_params failed"): + await session.request("GET", "http://example.com") + + mock_check_params.assert_called_once() + + @pytest.mark.asyncio + async def test_no_cert_rotation_when_cert_match_and_mTLS_enabled(self): + mock_creds = mock.AsyncMock(spec=credentials.Credentials) + mock_auth_req = mock.AsyncMock() + mock_resp = mock.Mock() + import http.client as http_client + mock_resp.status_code = http_client.UNAUTHORIZED + mock_auth_req.return_value = mock_resp + + session = sessions.AsyncAuthorizedSession(mock_creds, auth_request=mock_auth_req) + session._is_mtls = True + session._cached_cert = b"old_cert" + + with mock.patch( + "google.auth.transport._mtls_helper.check_parameters_for_unauthorized_response", + ) as mock_check, mock.patch.object(session, "configure_mtls_channel", new_callable=mock.AsyncMock) as mock_conf: + # same fingerprint, so no call to configure_mtls_channel + mock_check.return_value = (b"new_cert", b"new_key", b"same_fp", b"same_fp") + + await session.request("GET", "http://example.com") + + mock_check.assert_called_once() + mock_conf.assert_not_called() From 907cf0083a8120259ab10b2b7a503c8e8ea139b1 Mon Sep 17 00:00:00 2001 From: agrawalradhika-cell Date: Wed, 26 Aug 2026 10:34:10 -0700 Subject: [PATCH 3/7] Update packages/google-auth/tests/transport/aio/test_sessions_mtls.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../google-auth/tests/transport/aio/test_sessions_mtls.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/google-auth/tests/transport/aio/test_sessions_mtls.py b/packages/google-auth/tests/transport/aio/test_sessions_mtls.py index 740c6ac84ba2..f448c57073b2 100644 --- a/packages/google-auth/tests/transport/aio/test_sessions_mtls.py +++ b/packages/google-auth/tests/transport/aio/test_sessions_mtls.py @@ -392,9 +392,8 @@ async def test_cert_rotation_check_params_fails(self): "google.auth.transport._mtls_helper.check_parameters_for_unauthorized_response", side_effect=Exception("check_params failed"), ) as mock_check_params: - with pytest.raises(Exception, match="check_params failed"): - await session.request("GET", "http://example.com") - + resp = await session.request("GET", "http://example.com") + assert resp == mock_resp mock_check_params.assert_called_once() @pytest.mark.asyncio From cc850b13e16e2ed1b297a00d4e2ecd958eaa5b6c Mon Sep 17 00:00:00 2001 From: agrawalradhika-cell Date: Wed, 26 Aug 2026 10:38:02 -0700 Subject: [PATCH 4/7] Improve error handling for mTLS reconfiguration Handle exceptions during mTLS reconfiguration with warnings instead of errors. --- .../google/auth/aio/transport/sessions.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/google-auth/google/auth/aio/transport/sessions.py b/packages/google-auth/google/auth/aio/transport/sessions.py index c55be4670549..6ace722c2814 100644 --- a/packages/google-auth/google/auth/aio/transport/sessions.py +++ b/packages/google-auth/google/auth/aio/transport/sessions.py @@ -315,7 +315,7 @@ async def request( ) ) if response.status_code == http_client.UNAUTHORIZED: - if self.is_mtls: + try: call_cert_bytes, call_key_bytes, cached_fingerprint, current_cert_fingerprint = google.auth.transport._mtls_helper.check_parameters_for_unauthorized_response( self._cached_cert ) @@ -330,15 +330,20 @@ async def request( ) continue except Exception as e: - _LOGGER.error("Failed to reconfigure mTLS channel: %s", e) - raise exceptions.MutualTLSChannelError( - "Failed to reconfigure mTLS channel" - ) from e + _LOGGER.warning( + "Failed to reconfigure mTLS channel: %s. Proceeding with original response.", + e + ) else: - _LOGGER.info( + _LOGGER.info( "Skipping reconfiguration of mTLS channel because the client" " certificate has not changed." ) + except Exception as e: + _LOGGER.warning( + "Failed to check client certificate parameters: %s. Proceeding with original response.", + e, + ) if response.status_code not in transport.DEFAULT_RETRYABLE_STATUS_CODES: break From a44acb0654b0ec21cbe49e98aa8ad1c638001d38 Mon Sep 17 00:00:00 2001 From: agrawalradhika-cell Date: Wed, 26 Aug 2026 11:19:57 -0700 Subject: [PATCH 5/7] fix: Rename test_cert_rotation_failure to test_cert_rotation_failure_logs Updated test logic to assert response instead of expecting an error. --- .../google-auth/tests/transport/aio/test_sessions_mtls.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/google-auth/tests/transport/aio/test_sessions_mtls.py b/packages/google-auth/tests/transport/aio/test_sessions_mtls.py index f448c57073b2..0b0a0ec87229 100644 --- a/packages/google-auth/tests/transport/aio/test_sessions_mtls.py +++ b/packages/google-auth/tests/transport/aio/test_sessions_mtls.py @@ -346,7 +346,7 @@ async def test_configure_mtls_channel_close_exception_does_not_abort(self): await session.close() @pytest.mark.asyncio - async def test_cert_rotation_failure_raises_error(self): + async def test_cert_rotation_failure_logs(self): mock_creds = mock.AsyncMock(spec=credentials.Credentials) mock_creds.before_request = mock.AsyncMock(return_value=None) @@ -369,8 +369,8 @@ async def test_cert_rotation_failure_raises_error(self): mock_check.return_value = (new_cert, new_key, b"old_fp", b"new_fp") mock_conf.side_effect = Exception("Failed to reconfigure") - with pytest.raises(exceptions.MutualTLSChannelError): - await session.request("GET", "http://example.com") + resp = await session.request("GET", "http://example.com") + assert resp == mock_resp mock_check.assert_called_once() mock_conf.assert_called_once() From 984e47c3388abd43271ac9e0d7c6f198534db176 Mon Sep 17 00:00:00 2001 From: agrawalradhika-cell Date: Wed, 26 Aug 2026 14:54:48 -0700 Subject: [PATCH 6/7] chore: Refactor MTLS parameter check on unauthorized response o use async executor Refactor unauthorized response handling to use async executor for MTLS parameter checks. --- .../google-auth/google/auth/aio/transport/sessions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/google-auth/google/auth/aio/transport/sessions.py b/packages/google-auth/google/auth/aio/transport/sessions.py index 6ace722c2814..1a4b9c468f9a 100644 --- a/packages/google-auth/google/auth/aio/transport/sessions.py +++ b/packages/google-auth/google/auth/aio/transport/sessions.py @@ -316,7 +316,13 @@ async def request( ) if response.status_code == http_client.UNAUTHORIZED: try: - call_cert_bytes, call_key_bytes, cached_fingerprint, current_cert_fingerprint = google.auth.transport._mtls_helper.check_parameters_for_unauthorized_response( + ( + call_cert_bytes, + call_key_bytes, + cached_fingerprint, + current_cert_fingerprint + ) = await mtls._run_in_executor( + google.auth.transport._mtls_helper.check_parameters_for_unauthorized_response, self._cached_cert ) if cached_fingerprint != current_cert_fingerprint: From 30341bc5acfbc6155165c8062852e670acd7dedb Mon Sep 17 00:00:00 2001 From: agrawalradhika-cell Date: Wed, 26 Aug 2026 22:03:28 -0700 Subject: [PATCH 7/7] chore: Reset mTLS init task upon client certificate change chore: Reset mTLS init task upon client certificate change --- packages/google-auth/google/auth/aio/transport/sessions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/google-auth/google/auth/aio/transport/sessions.py b/packages/google-auth/google/auth/aio/transport/sessions.py index 1a4b9c468f9a..c4ef00528d0a 100644 --- a/packages/google-auth/google/auth/aio/transport/sessions.py +++ b/packages/google-auth/google/auth/aio/transport/sessions.py @@ -331,6 +331,8 @@ async def request( "Client certificate has changed, reconfiguring mTLS " "channel." ) + if self._mtls_init_task and self._mtls_init_task.done(): + self._mtls_init_task = None await self.configure_mtls_channel( lambda: (call_cert_bytes, call_key_bytes) )