Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7877ee8
chore: Add threading lock for mTLS configuration
agrawalradhika-cell Aug 19, 2026
5a7ec5c
chore: For urllib3 Add reauth_lock to manage mTLS reconfiguration
agrawalradhika-cell Aug 19, 2026
496a3de
chore: Add test for MTLS reauth lock on unauthorized response
agrawalradhika-cell Aug 19, 2026
64c11a2
chore: Add test for reauth lock on unauthorized response
agrawalradhika-cell Aug 19, 2026
20bf451
fix: Reorder import statements in requests.py
agrawalradhika-cell Aug 19, 2026
0795590
fix: Modify test request URL for MTLS session
agrawalradhika-cell Aug 19, 2026
174d1e9
fix: Update URL in test for mTLS endpoint for urllib3
agrawalradhika-cell Aug 19, 2026
adc682d
fix: Fix the lint and unit test scoverage
agrawalradhika-cell Aug 20, 2026
1035172
chore: Implement MTLS URL prefix handling
agrawalradhika-cell Aug 31, 2026
fcdba65
chore: Refactor test for MTLS session reauthentication
agrawalradhika-cell Aug 31, 2026
dda49eb
fix: Add reauth_lock to session in test_requests.py
agrawalradhika-cell Aug 31, 2026
8376f33
fix: fix spacing for skipping lint error
agrawalradhika-cell Aug 31, 2026
aa4c535
fix: Clean up whitespace in test_requests.py
agrawalradhika-cell Aug 31, 2026
3581772
fix: Clean up whitespace in urllib3.py
agrawalradhika-cell Aug 31, 2026
85a5dab
fix: fix all the lint errors via blacken
agrawalradhika-cell Aug 31, 2026
a273fba
fix: Implement test for skipped reauth with matching cert
agrawalradhika-cell Sep 2, 2026
bcef1ea
fix: Refactor test_urllib3.py by removing unnecessary lines
agrawalradhika-cell Sep 2, 2026
3489c06
fix: Remove obsolete tests from test_urllib3.py
agrawalradhika-cell Sep 2, 2026
be7908b
fix: Implement timeout test for urllib3 Request
agrawalradhika-cell Sep 2, 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
55 changes: 30 additions & 25 deletions packages/google-auth/google/auth/transport/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import http.client as http_client
import logging
import numbers
import threading
import time
from typing import Optional

Expand Down Expand Up @@ -414,6 +415,7 @@ def __init__(
self._refresh_timeout = refresh_timeout
self._is_mtls = False
self._default_host = default_host
self._reauth_lock = threading.Lock()

if auth_request is None:
self._auth_request_session = requests.Session()
Expand Down Expand Up @@ -655,33 +657,36 @@ def request(
prefix in url for prefix in MTLS_URL_PREFIXES
)
if use_mtls:
(
call_cert_bytes,
call_key_bytes,
cached_fingerprint,
current_cert_fingerprint,
) = _mtls_helper.check_parameters_for_unauthorized_response(
self._cached_cert
)
if cached_fingerprint != current_cert_fingerprint:
try:
with self._reauth_lock:
(
call_cert_bytes,
call_key_bytes,
cached_fingerprint,
current_cert_fingerprint,
) = _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."
)
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
else:
_LOGGER.info(
"Client certificate has changed, reconfiguring mTLS "
"channel."
"Skipping reconfiguration of mTLS channel because the client"
" certificate has not changed."
)
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
else:
_LOGGER.info(
"Skipping reconfiguration of mTLS channel because the client"
" certificate has not changed."
)
_LOGGER.info(
"Refreshing credentials due to a %s response. Attempt %s/%s.",
response.status_code,
Expand Down
70 changes: 41 additions & 29 deletions packages/google-auth/google/auth/transport/urllib3.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import http.client as http_client
import logging
import threading
import warnings

# Certifi is Mozilla's certificate bundle. Urllib3 needs a certificate bundle
Expand Down Expand Up @@ -309,6 +310,7 @@ def __init__(
# credentials.refresh).
self._request = Request(self.http)
self._is_mtls = False
self._reauth_lock = threading.Lock()

# https://google.aip.dev/auth/4111
# Attempt to use self-signed JWTs when a service account is used.
Expand Down Expand Up @@ -436,38 +438,48 @@ def urlopen(self, method, url, body=None, headers=None, **kwargs):
and _credential_refresh_attempt < self._max_refresh_attempts
):
if response.status == http_client.UNAUTHORIZED:
MTLS_URL_PREFIXES = [
"mtls.googleapis.com",
"mtls.sandbox.googleapis.com",
]
use_mtls = getattr(self, "_is_mtls", False) and any(
prefix in url for prefix in MTLS_URL_PREFIXES
)

if use_mtls:
Comment thread
agrawalradhika-cell marked this conversation as resolved.
(
call_cert_bytes,
call_key_bytes,
cached_fingerprint,
current_cert_fingerprint,
) = _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."
)
self.configure_mtls_channel(
client_cert_callback=lambda: (
call_cert_bytes,
call_key_bytes,
with self._reauth_lock:
(
call_cert_bytes,
call_key_bytes,
cached_fingerprint,
current_cert_fingerprint,
) = _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."
)
self.configure_mtls_channel(
client_cert_callback=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
else:
_LOGGER.info(
"Skipping reconfiguration of mTLS channel because the "
"client certificate has not changed."
)
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."
)

_LOGGER.info(
"Refreshing credentials due to a %s response. Attempt %s/%s.",
Expand Down
64 changes: 64 additions & 0 deletions packages/google-auth/tests/transport/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import functools
import http.client as http_client
import os
import threading
from unittest import mock

import freezegun
Expand Down Expand Up @@ -1109,3 +1110,66 @@ def test_success_should_use_provider(

adapter.proxy_manager_for()
mock_proxy_manager_for.assert_called_with(ssl_context=adapter._ctx_proxymanager)


class TestAuthorizedSessionMTLSReauth:
Comment thread
agrawalradhika-cell marked this conversation as resolved.
@mock.patch(
"google.auth.transport._mtls_helper.check_parameters_for_unauthorized_response"
)
@mock.patch("google.auth.transport.requests.requests.Session.request")
def test_reauth_lock_acquired_on_unauthorized(
self, mock_session_request, mock_check_params
):
credentials = mock.Mock()
session = google.auth.transport.requests.AuthorizedSession(credentials)
session._is_mtls = True
session._cached_cert = b"cert"
mock_response = mock.Mock(status_code=http_client.UNAUTHORIZED)
mock_success_response = mock.Mock(status_code=http_client.OK)
mock_session_request.side_effect = [mock_response, mock_success_response]
real_lock = threading.Lock()
session._reauth_lock = real_lock
Comment thread
agrawalradhika-cell marked this conversation as resolved.
mock_check_params.return_value = (
b"new_cert_bytes",
b"new_key_bytes",
"old_fingerprint",
"new_fingerprint",
)
lock_held_during_call = {"held": False}

def verify_lock_held(*args, **kwargs):
lock_held_during_call["held"] = session._reauth_lock.locked()

session.configure_mtls_channel = mock.Mock(side_effect=verify_lock_held)
session.request("GET", "https://example.mtls.googleapis.com/")

session.configure_mtls_channel.assert_called()
assert lock_held_during_call["held"] is True

@mock.patch(
"google.auth.transport._mtls_helper.check_parameters_for_unauthorized_response"
)
@mock.patch("google.auth.transport.requests.requests.Session.request")
def test_reauth_skipped_when_cert_fingerprint_matches(
self, mock_session_request, mock_check_params
):
credentials = mock.Mock()
session = google.auth.transport.requests.AuthorizedSession(credentials)
session._is_mtls = True
session._cached_cert = b"cert"

mock_session_request.side_effect = [
mock.Mock(status_code=http_client.UNAUTHORIZED),
mock.Mock(status_code=http_client.OK),
]
mock_check_params.return_value = (
b"same_cert_bytes",
b"same_key_bytes",
"same_fingerprint",
"same_fingerprint",
)
session.configure_mtls_channel = mock.Mock()

session.request("GET", "https://example.mtls.googleapis.com/")

session.configure_mtls_channel.assert_not_called()
57 changes: 57 additions & 0 deletions packages/google-auth/tests/transport/test_urllib3.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import http.client as http_client
import os
import threading
from unittest import mock

import pytest # type: ignore
Expand Down Expand Up @@ -723,3 +724,59 @@ def test_configure_mtls_channel_subsequent_disabled(
assert not is_mtls
assert not authed_http._is_mtls
assert isinstance(authed_http.http, urllib3.PoolManager)


class TestAuthorizedHttpMTLSReauth:
@mock.patch(
"google.auth.transport._mtls_helper.check_parameters_for_unauthorized_response"
)
def test_reauth_lock_acquired_on_unauthorized(self, mock_check_params):
credentials = mock.Mock()
http_obj = google.auth.transport.urllib3.AuthorizedHttp(credentials)
http_obj._is_mtls = True
http_obj._cached_cert = b"cert"
mock_response = mock.Mock()
mock_response.status = http_client.UNAUTHORIZED
http_obj.http.urlopen = mock.Mock(return_value=mock_response)
real_lock = threading.Lock()
http_obj._reauth_lock = real_lock
mock_check_params.return_value = (
b"new_cert_bytes",
b"new_key_bytes",
"old_fingerprint",
"new_fingerprint",
)
lock_held_during_call = {"held": False}

def verify_lock_held(*args, **kwargs):
lock_held_during_call["held"] = real_lock.locked()

http_obj.configure_mtls_channel = mock.Mock(side_effect=verify_lock_held)
http_obj.request("GET", "https://example.mtls.googleapis.com/")
http_obj.configure_mtls_channel.assert_called()
assert lock_held_during_call["held"] is True

@mock.patch(
"google.auth.transport._mtls_helper.check_parameters_for_unauthorized_response"
)
def test_reauth_skipped_when_cert_fingerprint_matches(self, mock_check_params):
credentials = mock.Mock()
http_obj = google.auth.transport.urllib3.AuthorizedHttp(credentials)
http_obj._is_mtls = True
http_obj._cached_cert = b"cert"
mock_response_unauth = mock.Mock()
mock_response_unauth.status = http_client.UNAUTHORIZED
mock_response_ok = mock.Mock()
mock_response_ok.status = http_client.OK
http_obj.http.urlopen = mock.Mock(
side_effect=[mock_response_unauth, mock_response_ok]
)
mock_check_params.return_value = (
b"same_cert_bytes",
b"same_key_bytes",
"same_fingerprint",
"same_fingerprint",
)
http_obj.configure_mtls_channel = mock.Mock()
http_obj.request("GET", "https://example.mtls.googleapis.com/")
http_obj.configure_mtls_channel.assert_not_called()
Loading