Skip to content

Commit 590376d

Browse files
authored
fix(auth): support mTLS in requests.Request for token refresh and impersonation (#18486)
Credential token refreshes and Regional Access Boundary lookups (such as `impersonated_credentials` calling `iamcredentials.mtls.googleapis.com`) make HTTP requests through `google.auth.transport.requests.Request`. Because only `AuthorizedSession` previously configured `_MutualTlsAdapter`, bare `Request` calls to `.mtls.googleapis.com` endpoints did not present a client certificate and failed with `401 Unauthorized` when using certificate-bound tokens. This change lazily mounts `_MutualTlsAdapter` per mTLS origin (`https://<host>/`) on `requests.Request` when client certificates are enabled, preserving existing `HTTPAdapter` settings and supporting `copy.deepcopy` so background RAB refresh threads can clone the transport without reloading certificates. Fixes: b/555142182 Note: This change is based on #18381, which will be broken into 4 smaller PRs. This first PR is scoped to google.auth.transport.requests.Request to unblock the primary request path. Equivalent support for google.auth.transport.urllib3.Request and certificate rotation support for both transports will follow in separate PRs.
1 parent 54dbd12 commit 590376d

2 files changed

Lines changed: 441 additions & 0 deletions

File tree

‎packages/google-auth/google/auth/transport/requests.py‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@
1616

1717
from __future__ import absolute_import
1818

19+
import collections
20+
import copy
1921
import functools
2022
import http.client as http_client
2123
import logging
2224
import numbers
25+
import threading
2326
import time
27+
import urllib.parse as urllib_parse
2428
from typing import Optional
2529

2630
try:
@@ -136,11 +140,28 @@ class Request(transport.Request):
136140
.. automethod:: __call__
137141
"""
138142

143+
_mtls_adapter = None
144+
_mtls_unavailable = False
145+
139146
def __init__(self, session: Optional[requests.Session] = None) -> None:
140147
if not session:
141148
session = requests.Session()
142149

143150
self.session = session
151+
self._mtls_adapter = None
152+
self._mtls_unavailable = False
153+
self._mtls_lock = threading.Lock()
154+
155+
def __getstate__(self):
156+
state = self.__dict__.copy()
157+
state["_mtls_lock"] = None
158+
return state
159+
160+
def __setstate__(self, state):
161+
self.__dict__.update(state)
162+
self._mtls_lock = threading.Lock()
163+
if not isinstance(self._mtls_adapter, _MutualTlsAdapter):
164+
self._mtls_adapter = None
144165

145166
def __del__(self):
146167
try:
@@ -152,6 +173,59 @@ def __del__(self):
152173
# TypeError.
153174
pass
154175

176+
def _configure_mtls_if_needed(self, url):
177+
"""Lazily mounts a mutual TLS adapter onto the session for mTLS endpoints.
178+
179+
Args:
180+
url (str): The target request URL.
181+
"""
182+
if not _mtls_helper.is_mtls_endpoint(url):
183+
return
184+
185+
# Mount on the specific origin rather than globally on "https://" so client
186+
# certificates are not sent to other hosts sharing this session. The trailing
187+
# slash prevents requests' startswith() matching from matching lookalike domains.
188+
parsed = urllib_parse.urlparse(url)
189+
prefix = f"{parsed.scheme}://{parsed.netloc.lower()}/"
190+
with self._mtls_lock:
191+
session_adapter = self.session.get_adapter(url if parsed.path else prefix)
192+
# Skip if _MutualTlsAdapter is already mounted or the caller mounted a custom adapter.
193+
if (
194+
type(session_adapter) is not requests.adapters.HTTPAdapter
195+
or self.session.get_adapter(prefix) is not session_adapter
196+
):
197+
return
198+
199+
if self._mtls_adapter is None:
200+
if self._mtls_unavailable or not _mtls_helper.check_use_client_cert():
201+
return
202+
has_cert, cert, key = _mtls_helper.get_client_cert_and_key()
203+
if not has_cert:
204+
self._mtls_unavailable = True
205+
return
206+
207+
self._mtls_adapter = _MutualTlsAdapter(
208+
cert,
209+
key,
210+
max_retries=session_adapter.max_retries,
211+
pool_connections=session_adapter._pool_connections,
212+
pool_maxsize=session_adapter._pool_maxsize,
213+
pool_block=session_adapter._pool_block,
214+
)
215+
216+
# Replace session.adapters atomically instead of calling session.mount(),
217+
# which mutates the OrderedDict in place and can raise RuntimeError if
218+
# another thread is iterating over session.adapters in get_adapter().
219+
new_adapters = self.session.adapters.copy()
220+
new_adapters[prefix] = self._mtls_adapter
221+
self.session.adapters = collections.OrderedDict(
222+
sorted(
223+
new_adapters.items(),
224+
key=lambda item: len(item[0]),
225+
reverse=True,
226+
)
227+
)
228+
155229
def __call__(
156230
self,
157231
url,
@@ -181,6 +255,19 @@ def __call__(
181255
Raises:
182256
google.auth.exceptions.TransportError: If any exception occurred.
183257
"""
258+
if not kwargs.get("cert"):
259+
try:
260+
self._configure_mtls_if_needed(url)
261+
except (
262+
exceptions.MutualTLSChannelError,
263+
exceptions.ClientCertError,
264+
OSError,
265+
) as caught_exc:
266+
new_exc = exceptions.TransportError(caught_exc)
267+
raise new_exc from caught_exc
268+
269+
# Missing CA bundles or cert paths raised by requests must bubble up
270+
# unwrapped to fail immediately rather than triggering client retries.
184271
try:
185272
_helpers.request_log(_LOGGER, method, url, body, headers)
186273
response = self.session.request(
@@ -212,9 +299,11 @@ def __init__(self, cert, key, **kwargs):
212299
import certifi
213300

214301
ctx_poolmanager = create_urllib3_context()
302+
ctx_poolmanager.check_hostname = False
215303
ctx_poolmanager.load_verify_locations(cafile=certifi.where())
216304

217305
ctx_proxymanager = create_urllib3_context()
306+
ctx_proxymanager.check_hostname = False
218307
ctx_proxymanager.load_verify_locations(cafile=certifi.where())
219308

220309
try:
@@ -251,6 +340,17 @@ def __init__(self, cert, key, **kwargs):
251340

252341
super(_MutualTlsAdapter, self).__init__(**kwargs)
253342

343+
def __deepcopy__(self, memo):
344+
new_adapter = object.__new__(type(self))
345+
memo[id(self)] = new_adapter
346+
new_adapter._ctx_poolmanager = self._ctx_poolmanager
347+
new_adapter._ctx_proxymanager = self._ctx_proxymanager
348+
new_adapter.__setstate__(copy.deepcopy(self.__getstate__(), memo))
349+
return new_adapter
350+
351+
def __reduce_ex__(self, protocol):
352+
return (requests.adapters.HTTPAdapter, (), self.__getstate__())
353+
254354
def init_poolmanager(self, *args, **kwargs):
255355
kwargs["ssl_context"] = self._ctx_poolmanager
256356
super(_MutualTlsAdapter, self).init_poolmanager(*args, **kwargs)

0 commit comments

Comments
 (0)