Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
94a1d95
feat: Add retry for cert rotation handling
agrawalradhika-cell Aug 26, 2026
420447c
chore: Add tests for MTLS certificate rotation behavior
agrawalradhika-cell Aug 26, 2026
907cf00
Update packages/google-auth/tests/transport/aio/test_sessions_mtls.py
agrawalradhika-cell Aug 26, 2026
cc850b1
Improve error handling for mTLS reconfiguration
agrawalradhika-cell Aug 26, 2026
a44acb0
fix: Rename test_cert_rotation_failure to test_cert_rotation_failure_…
agrawalradhika-cell Aug 26, 2026
984e47c
chore: Refactor MTLS parameter check on unauthorized response o use a…
agrawalradhika-cell Aug 26, 2026
30341bc
chore: Reset mTLS init task upon client certificate change
agrawalradhika-cell Aug 27, 2026
1c068dc
fix: fix the lint errors
agrawalradhika-cell Aug 27, 2026
6fb1e86
chore: Refactor mTLS channel reconfiguration logic for adding mTLS ch…
agrawalradhika-cell Aug 27, 2026
2cdfe2d
chore: Add mTLS rotation lock for certificate management
agrawalradhika-cell Aug 27, 2026
d734731
chore: Log mTLS channel reconfiguration failure as error
agrawalradhika-cell Aug 27, 2026
97e91d0
chore: Refactor mTLS handling for unauthorized responses
agrawalradhika-cell Aug 28, 2026
d0da58b
fix: Remove unnecessary continue statement after mTLS configuration.
agrawalradhika-cell Aug 28, 2026
825426d
fix: Fix cert rotation tests and improve error handling
agrawalradhika-cell Aug 28, 2026
63e587c
fix: fix unit tests for the checks
agrawalradhika-cell Aug 28, 2026
71b3bf5
fix: Fix unit tests for the change
agrawalradhika-cell Aug 28, 2026
7d92d30
test: remove fragile async caplog assertions
agrawalradhika-cell Aug 28, 2026
8b2efcf
fix: Add error handling for credential refresh failures
agrawalradhika-cell Aug 28, 2026
a4d0405
fix: Fix lint errors
agrawalradhika-cell Aug 28, 2026
2d52a21
chore: Refactor mTLS endpoint handling in sessions.py
agrawalradhika-cell Aug 30, 2026
2806f4f
chore: Reorder response closing logic for clarity
agrawalradhika-cell Aug 30, 2026
d5426f2
chore: Handle additional exception during credential refresh
agrawalradhika-cell Aug 30, 2026
968a9fd
fix: Modify mTLS rotation lock initialization
agrawalradhika-cell Aug 30, 2026
9d1a690
fix: Handle response closure in mTLS error handling
agrawalradhika-cell Aug 30, 2026
85d4a76
Fix: Fix improperly falling through to the credential refresh logic.
agrawalradhika-cell Aug 30, 2026
d1c6512
chore: Track and close old auth requests in sessions.py
agrawalradhika-cell Aug 30, 2026
b28caea
fix: Adjust max_allowed_time based on elapsed time
agrawalradhika-cell Aug 30, 2026
f176eed
chore: Add client_cert_callback to transport session
agrawalradhika-cell Aug 31, 2026
a436abe
chore: Enhance check_parameters_for_unauthorized_response with callback
agrawalradhika-cell Aug 31, 2026
fbee990
fix: Add test for certificate rotation lock contention
agrawalradhika-cell Aug 31, 2026
b310e27
fix: Enhance MTLS session tests with various scenarios
agrawalradhika-cell Aug 31, 2026
55f1ad4
Fix: Fix lint and unit tetsts
agrawalradhika-cell Aug 31, 2026
554a571
fix: fix unit tests for tests_sessions
agrawalradhika-cell Aug 31, 2026
eb28f81
chore: Refactor mTLS channel configuration callback
agrawalradhika-cell Aug 31, 2026
221810e
fix: Import urllib.parse instead of urllib
agrawalradhika-cell Aug 31, 2026
445c576
fix: Format mTLS channel configuration for readability
agrawalradhika-cell Aug 31, 2026
6b0edd3
fix: Fix test name for mTLS certificate matching
agrawalradhika-cell Aug 31, 2026
b6e30b0
fix: Refactor type annotations and error handling
agrawalradhika-cell Aug 31, 2026
7c32ec9
chore: Change exception type in test for MTLS session
agrawalradhika-cell Aug 31, 2026
f98c347
Fix duplicate isinstance check for data type
agrawalradhika-cell Sep 1, 2026
c97bfdf
fix: Format error message for MutualTLSChannelError
agrawalradhika-cell Sep 1, 2026
2790119
chore: Refactor mTLS handling and improve timeout logic
agrawalradhika-cell Sep 1, 2026
498bcd6
chore: Implement mTLS check counter for configuration management
agrawalradhika-cell Sep 1, 2026
242359a
fix: Implement test for cert rotation lock contention
agrawalradhika-cell Sep 1, 2026
8c867af
fix: Refactor mTLS configuration and error handling
agrawalradhika-cell Sep 1, 2026
8942a12
fix: Fix indentation for asyncio test decorator
agrawalradhika-cell Sep 1, 2026
eb95f3c
fix: Enhance tests for MTLS session certificate rotation
agrawalradhika-cell Sep 1, 2026
50ba485
fix: Remove stale_cert assignment in sessions.py
agrawalradhika-cell Sep 1, 2026
0e8950f
fix: Update sessions.py for lint
agrawalradhika-cell Sep 1, 2026
bc8bebb
fix: fix lint errors in sessions.py
agrawalradhika-cell Sep 1, 2026
2b57c11
Rename MTLS_URL_PREFIXES to _MTLS_URL_PREFIXES and typecasting fix
agrawalradhika-cell Sep 1, 2026
a149eb7
fix: Refactor request headers handling in sessions.py
agrawalradhika-cell Sep 2, 2026
e66a723
fix: Fix client certificate callback handling in mTLS
agrawalradhika-cell Sep 2, 2026
314d974
feat: Implement mTLS parameter check and fingerprinting
agrawalradhika-cell Sep 2, 2026
cf308b8
fix: Refactor MTLS parameter checking in sessions.py
agrawalradhika-cell Sep 2, 2026
7794506
fix: Update mock patch for MTLS check parameters
agrawalradhika-cell Sep 2, 2026
931bdfd
fix: Refactor mTLS channel reconfiguration logic for lint
agrawalradhika-cell Sep 2, 2026
9bd8e8f
fix: Fix lint errors
agrawalradhika-cell Sep 2, 2026
e8ee7b6
fix: fix the unit tests based on the code changes
agrawalradhika-cell Sep 2, 2026
7379d8a
fix: Log when credentials do not implement refresh method
agrawalradhika-cell Sep 2, 2026
48cf5d2
chore: Refactor authentication retry logic in sessions.py
agrawalradhika-cell Sep 3, 2026
8b5d35d
fix: Implement concurrent credential refresh management
agrawalradhika-cell Sep 3, 2026
c7746a4
fix: Limit old auth requests to 2 and close oldest requests
agrawalradhika-cell Sep 3, 2026
d5c6746
fix: Enhance tests for 401 response handling
agrawalradhika-cell Sep 3, 2026
83095ea
Fix: Fix the lint and unit tests
agrawalradhika-cell Sep 3, 2026
da00b40
fix: Add .p.googleapis.com to MTLS URL prefixes
agrawalradhika-cell Sep 3, 2026
a355bc7
fix: Refactor close method calls to handle awaitables
agrawalradhika-cell Sep 3, 2026
0ae183f
fix: Change parameter order in check_parameters_for_unauthorized_resp…
agrawalradhika-cell Sep 3, 2026
c976c76
fix: Fix client cert callback assignment in sessions.py
agrawalradhika-cell Sep 3, 2026
3390024
fix: Update the tests based on chnages in sessions.py
agrawalradhika-cell Sep 3, 2026
377f6be
fix: Add unit tests for MTLS parameter checking
agrawalradhika-cell Sep 3, 2026
86640b2
fix: Refactor mock patches in test_mtls.py
agrawalradhika-cell Sep 3, 2026
fbb9e47
Fix: fix lint and unit tests
agrawalradhika-cell Sep 3, 2026
f1b18ad
Fix: Fix the lint errors
agrawalradhika-cell Sep 3, 2026
4cb7e05
chore: Modify check_parameters_for_unauthorized_response function
agrawalradhika-cell Sep 3, 2026
215857d
chore: Store refresh counter during request handling
agrawalradhika-cell Sep 3, 2026
f598432
fix: Refactor test_mtls.py for improved mock handling
agrawalradhika-cell Sep 3, 2026
aa0ee37
fix: Change mock responses to include 200 status code
agrawalradhika-cell Sep 3, 2026
97e2b1a
Fix: fix the unit tests in test_mtls
agrawalradhika-cell Sep 3, 2026
2708a53
chore: Improve management of old auth requests
agrawalradhika-cell Sep 6, 2026
94d92e9
Refactor certificate checking logic in sessions.py
agrawalradhika-cell Sep 6, 2026
24b2f4e
Refactor return types in mTLS client cert callback and fetch_fingerprint
agrawalradhika-cell Sep 6, 2026
48164e6
chore: Improve mTLS configuration logic in sessions.py
agrawalradhika-cell Sep 7, 2026
6687039
Removed the client_cert_callback parameter from the check_parameters_…
agrawalradhika-cell Sep 7, 2026
066ce77
fix: Enhance error handling with mTLS check counter
agrawalradhika-cell Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion packages/google-auth/google/auth/aio/transport/mtls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import ssl
from typing import Optional

from google.auth import exceptions
from google.auth import _agent_identity_utils, exceptions
from google.auth.transport._mtls_helper import secure_cert_key_paths
import google.auth.transport.mtls

Expand Down Expand Up @@ -177,3 +177,44 @@ async def get_client_cert_and_key(client_cert_callback=None):

has_cert, cert, key, _ = await get_client_ssl_credentials()
return has_cert, cert, key


async def check_parameters_for_unauthorized_response(
cached_cert, client_cert_callback=None
):
"""Async helper to retrieve certs and compute fingerprints for mTLS rotation.

Args:
cached_cert (bytes): The cached client certificate.
client_cert_callback (Optional[Callable[[], (bytes, bytes)]]): An
optional callback which returns client certificate bytes and private
key bytes both in PEM format.

Returns:
Comment thread
agrawalradhika-cell marked this conversation as resolved.
Tuple[Optional[bytes], Optional[bytes], Optional[str], Optional[str]]:
call_cert_bytes, call_key_bytes, cached_fingerprint, current_cert_fingerprint.
Returns (None, None, None, None) if mTLS is disabled or no client certificate is present.
"""
is_mtls, call_cert_bytes, call_key_bytes = await get_client_cert_and_key(
client_cert_callback
)
if not is_mtls or not call_cert_bytes:
return None, None, None, None

def _fetch_fingerprints():
cert_obj = _agent_identity_utils.parse_certificate(call_cert_bytes)
current_fingerprint = _agent_identity_utils.calculate_certificate_fingerprint(
cert_obj
)
if cached_cert:
cached_fingerprint = _agent_identity_utils.get_cached_cert_fingerprint(
cached_cert
)
else:
cached_fingerprint = None
return cached_fingerprint, current_fingerprint

cached_fingerprint, current_cert_fingerprint = await _run_in_executor(
_fetch_fingerprints
)
return call_cert_bytes, call_key_bytes, cached_fingerprint, current_cert_fingerprint
Comment thread
agrawalradhika-cell marked this conversation as resolved.
250 changes: 240 additions & 10 deletions packages/google-auth/google/auth/aio/transport/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@
# limitations under the License.

import asyncio
import collections.abc
from contextlib import asynccontextmanager
import functools
import http.client as http_client
import inspect
import logging
import time
from typing import Mapping, Optional, TYPE_CHECKING, Union
import urllib.parse
import warnings

from google.auth import _exponential_backoff, exceptions
Expand All @@ -37,6 +42,12 @@
except (ImportError, AttributeError):
ClientTimeout = None

_LOGGER = logging.getLogger(__name__)
_MTLS_URL_PREFIXES = [
"mtls.googleapis.com",
"mtls.sandbox.googleapis.com",
"p.googleapis.com",
]

# Tracks the internal aiohttp installation and usage
try:
Expand Down Expand Up @@ -66,6 +77,8 @@ async def timeout_guard(timeout):
total_timeout = timeout

def _remaining_time():
if total_timeout is None:
return None
elapsed = time.monotonic() - start
remaining = total_timeout - elapsed
if remaining <= 0:
Expand Down Expand Up @@ -143,11 +156,17 @@ def __init__(
self._is_mtls = False
self._mtls_init_task = None
self._cached_cert = None
self._client_cert_callback = None
self._old_auth_requests: list[transport.Request] = []
if _auth_request is None:
raise exceptions.TransportError(
"`auth_request` must either be configured or the external package `aiohttp` must be installed to use the default value."
)
self._auth_request = _auth_request
self._mtls_rotation_lock: Optional[asyncio.Lock] = None
self._mtls_check_counter = 0
self._refresh_lock: Optional[asyncio.Lock] = None
self._refresh_counter = 0

async def configure_mtls_channel(self, client_cert_callback=None):
"""Configure the client certificate and key for SSL connection.
Expand Down Expand Up @@ -175,6 +194,7 @@ async def configure_mtls_channel(self, client_cert_callback=None):
creation failed for any reason.
"""
if self._mtls_init_task is None:
self._client_cert_callback = client_cert_callback

async def _do_configure():
# Run the blocking check in an executor
Expand Down Expand Up @@ -204,12 +224,19 @@ async def _do_configure():

old_auth_request = self._auth_request
self._auth_request = AiohttpRequest(session=new_session)
Comment thread
agrawalradhika-cell marked this conversation as resolved.
self._old_auth_requests.append(old_auth_request)

while len(self._old_auth_requests) > 2:
oldest_auth_request = self._old_auth_requests[0]
try:
if hasattr(oldest_auth_request, "close"):
res = oldest_auth_request.close()
if inspect.isawaitable(res):
await res
except Exception:
pass
self._old_auth_requests.pop(0)

try:
await old_auth_request.close()
except Exception:
# Suppress so it doesn't abort the mTLS configuration
pass
else:
is_mtls = False
warnings.warn(
Expand Down Expand Up @@ -277,7 +304,10 @@ async def request(
google.auth.exceptions.TimeoutError: If the method does not complete within
the configured `max_allowed_time` or the request exceeds the configured
`timeout`.
google.auth.exceptions.MutualTLSChannelError: If mutual TLS
channel reconfiguration fails for any reason during certificate rotation.
"""
_auth_retry_count = kwargs.pop("_auth_retry_count", 0)
if self._mtls_init_task:
try:
await self._mtls_init_task
Expand All @@ -288,13 +318,15 @@ async def request(
retries = _exponential_backoff.AsyncExponentialBackoff(
total_attempts=total_attempts,
)
if headers is None:
headers = {}
request_headers = dict(headers) if headers is not None else {}
start_time = time.monotonic()
refresh_counter_at_error = self._refresh_counter
check_counter_at_error = self._mtls_check_counter
async with timeout_guard(max_allowed_time) as with_timeout:
await with_timeout(
# Note: before_request will attempt to refresh credentials if expired.
self._credentials.before_request(
self._auth_request, method, url, headers
self._auth_request, method, url, request_headers
)
)
actual_timeout: float = 0.0
Expand All @@ -307,11 +339,195 @@ async def request(
async for _ in retries: # pragma: no branch
response = await with_timeout(
self._auth_request(
url, method, data, headers, actual_timeout, **kwargs
url, method, data, request_headers, actual_timeout, **kwargs
)
)

if response.status_code not in transport.DEFAULT_RETRYABLE_STATUS_CODES:
break

if response.status_code == http_client.UNAUTHORIZED:
Comment thread
agrawalradhika-cell marked this conversation as resolved.
Comment thread
agrawalradhika-cell marked this conversation as resolved.
if _auth_retry_count < 2:
try:
if max_allowed_time is not None:
elapsed = time.monotonic() - start_time
remaining_time = max(0.0, max_allowed_time - elapsed)
if remaining_time == 0.0:
raise google.auth.exceptions.TimeoutError(
"Timeout exceeded before credential refresh could begin"
)
else:
remaining_time = None
is_streaming = data is not None and (
isinstance(
data,
(collections.abc.Iterator, collections.abc.AsyncIterable),
)
or hasattr(data, "read")
)

async def _recover_auth_state():
is_mtls_endpoint = False
if self._is_mtls:
hostname = urllib.parse.urlsplit(url).hostname
if hostname:
is_mtls_endpoint = any(
Comment thread
agrawalradhika-cell marked this conversation as resolved.
hostname == prefix
or hostname.endswith("." + prefix)
for prefix in _MTLS_URL_PREFIXES
)
# Snapshot the stale certificate state BEFORE acquiring the lock.
# This represents the cert that caused the 401 rejection.
if is_mtls_endpoint:
if self._mtls_rotation_lock is None:
self._mtls_rotation_lock = asyncio.Lock()
async with self._mtls_rotation_lock:
# Check if another coroutine already reconfigured mTLS or
# ran the validation check.
if (
self._mtls_check_counter
> check_counter_at_error
):
pass
else:
try:
(
call_cert_bytes,
call_key_bytes,
cached_fingerprint,
current_cert_fingerprint,
) = await mtls.check_parameters_for_unauthorized_response(
self._cached_cert,
self._client_cert_callback,
)
except (
exceptions.ClientCertError,
exceptions.MutualTLSChannelError,
OSError,
ValueError,
ImportError,
) as e:
_LOGGER.warning(
"Failed to check client certificate parameters: %s. Proceeding with original response.",
e,
)
else:
if (
current_cert_fingerprint is not None
and cached_fingerprint
!= current_cert_fingerprint
):
saved_callback = (
self._client_cert_callback
)
try:
_LOGGER.info(
"Client certificate has changed, reconfiguring mTLS "
"channel."
)
if self._mtls_init_task is not None:
if not self._mtls_init_task.done():
try:
await self._mtls_init_task
except Exception:
pass
self._mtls_init_task = None
await self.configure_mtls_channel(
lambda: (call_cert_bytes, call_key_bytes)
)
except Exception as e:
_LOGGER.error(
"Failed to reconfigure mTLS channel: %s",
e,
)
raise exceptions.MutualTLSChannelError(
"Failed to reconfigure mTLS channel"
) from e
finally:
self._client_cert_callback = (
saved_callback
)
else:
_LOGGER.info(
"Skipping reconfiguration of mTLS channel because the client"
" certificate has not changed."
)
# Always increment so waiting tasks skip the check block
self._mtls_check_counter += 1
if self._refresh_lock is None:
self._refresh_lock = asyncio.Lock()

async with self._refresh_lock:
# Check if another task already refreshed credentials while we were waiting
if self._refresh_counter > refresh_counter_at_error:
_LOGGER.debug(
"Credentials were already refreshed by a concurrent task. Skipping duplicate refresh."
)
else:
try:
await self._credentials.refresh(self._auth_request)
except NotImplementedError:
Comment thread
agrawalradhika-cell marked this conversation as resolved.
_LOGGER.debug("Credentials do not implement refresh().")
return response
except (
exceptions.RefreshError,
getattr(exceptions, "InvalidOperation", Exception),
) as e:
_LOGGER.debug(
"Credential refresh failed, returning 401 response. Error: %s",
e,
)
return response
else:
self._refresh_counter += 1

if is_streaming:
return response
# Return None to explicitly signal successful recovery & trigger retry if needed
return None

async with timeout_guard(remaining_time) as auth_with_timeout:
early_return_response = await auth_with_timeout(
_recover_auth_state()
)
except (Exception, asyncio.CancelledError):
if hasattr(response, "close"):
try:
res = response.close()
if inspect.isawaitable(res):
await res
except Exception:
pass
raise
# If it returned a response (meaning streaming or error), bail out
if early_return_response is not None:
return early_return_response
if hasattr(response, "close"):
try:
res = response.close()
if inspect.isawaitable(res):
await res
except Exception:
pass
if max_allowed_time is not None:
remaining_time = max(
0.0, max_allowed_time - (time.monotonic() - start_time)
)
if remaining_time == 0.0:
raise google.auth.exceptions.TimeoutError(
"Timeout exceeded before retrying the request"
)
kwargs["_auth_retry_count"] = _auth_retry_count + 1
return await self.request(
method,
url,
data=data,
headers=headers,
Comment thread
agrawalradhika-cell marked this conversation as resolved.
max_allowed_time=remaining_time,
timeout=timeout,
total_attempts=total_attempts,
**kwargs,
)
return response

@functools.wraps(request)
Expand Down Expand Up @@ -594,4 +810,18 @@ async def close(self) -> None:
await self._mtls_init_task
except asyncio.CancelledError:
pass
await self._auth_request.close()
try:
if hasattr(self._auth_request, "close"):
res = self._auth_request.close()
if inspect.isawaitable(res):
await res
finally:
for old_request in self._old_auth_requests:
try:
if hasattr(old_request, "close"):
res = old_request.close()
if inspect.isawaitable(res):
await res
except Exception:
pass
self._old_auth_requests.clear()
Loading
Loading