1# Copyright 2016 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Transport adapter for Requests."""
16
17from __future__ import absolute_import
18
19import collections
20import copy
21import functools
22import http.client as http_client
23import logging
24import numbers
25import threading
26import time
27import urllib.parse as urllib_parse
28from typing import Optional
29
30try:
31 import requests
32except ImportError as caught_exc: # pragma: NO COVER
33 raise ImportError(
34 "The requests library is not installed from please install the requests package to use the requests transport."
35 ) from caught_exc
36import requests.adapters # pylint: disable=ungrouped-imports
37import requests.exceptions # pylint: disable=ungrouped-imports
38from requests.packages.urllib3.util.ssl_ import ( # type: ignore
39 create_urllib3_context,
40) # pylint: disable=ungrouped-imports
41
42import google.auth.transport._mtls_helper
43from google.auth import _helpers, exceptions, transport
44from google.auth.transport import _mtls_helper
45from google.oauth2 import service_account
46
47_LOGGER = logging.getLogger(__name__)
48
49_DEFAULT_TIMEOUT = 120 # in seconds
50
51
52class _Response(transport.Response):
53 """Requests transport response adapter.
54
55 Args:
56 response (requests.Response): The raw Requests response.
57 """
58
59 def __init__(self, response):
60 self._response = response
61
62 @property
63 def status(self):
64 return self._response.status_code
65
66 @property
67 def headers(self):
68 return self._response.headers
69
70 @property
71 def data(self):
72 return self._response.content
73
74
75class TimeoutGuard(object):
76 """A context manager raising an error if the suite execution took too long.
77
78 Args:
79 timeout (Union[None, Union[float, Tuple[float, float]]]):
80 The maximum number of seconds a suite can run without the context
81 manager raising a timeout exception on exit. If passed as a tuple,
82 the smaller of the values is taken as a timeout. If ``None``, a
83 timeout error is never raised.
84 timeout_error_type (Optional[Exception]):
85 The type of the error to raise on timeout. Defaults to
86 :class:`requests.exceptions.Timeout`.
87 """
88
89 def __init__(self, timeout, timeout_error_type=requests.exceptions.Timeout):
90 self._timeout = timeout
91 self.remaining_timeout = timeout
92 self._timeout_error_type = timeout_error_type
93
94 def __enter__(self):
95 self._start = time.time()
96 return self
97
98 def __exit__(self, exc_type, exc_value, traceback):
99 if exc_value:
100 return # let the error bubble up automatically
101
102 if self._timeout is None:
103 return # nothing to do, the timeout was not specified
104
105 elapsed = time.time() - self._start
106 deadline_hit = False
107
108 if isinstance(self._timeout, numbers.Number):
109 self.remaining_timeout = self._timeout - elapsed
110 deadline_hit = self.remaining_timeout <= 0
111 else:
112 self.remaining_timeout = tuple(x - elapsed for x in self._timeout)
113 deadline_hit = min(self.remaining_timeout) <= 0
114
115 if deadline_hit:
116 raise self._timeout_error_type()
117
118
119def _replace_adapter(session, prefix, adapter):
120 """Mounts an adapter on a session without mutating its adapters in place.
121
122 ``adapter`` is mounted on ``session`` for ``prefix``.
123 :meth:`requests.Session.mount` mutates the ``session.adapters``
124 OrderedDict in place. If another thread is iterating over it in
125 :meth:`requests.Session.get_adapter` at the same time, that thread fails
126 with ``RuntimeError: OrderedDict mutated during iteration``. This builds
127 an updated copy and assigns it in one step instead, so threads that are
128 already iterating over the old dictionary are not affected.
129
130 Args:
131 session (requests.Session): The session to mount the adapter on.
132 prefix (str): The URL prefix to mount the adapter for.
133 adapter (requests.adapters.BaseAdapter): The adapter to mount.
134 """
135 new_adapters = session.adapters.copy()
136 new_adapters[prefix] = adapter
137 # Keep the longest prefixes first, as requests.Session.mount() does, so
138 # that get_adapter() matches the most specific prefix.
139 session.adapters = collections.OrderedDict(
140 sorted(new_adapters.items(), key=lambda item: len(item[0]), reverse=True)
141 )
142
143
144class Request(transport.Request):
145 """Requests request adapter.
146
147 This class is used internally for making requests using various transports
148 in a consistent way. If you use :class:`AuthorizedSession` you do not need
149 to construct or use this class directly.
150
151 This class can be useful if you want to manually refresh a
152 :class:`~google.auth.credentials.Credentials` instance::
153
154 import google.auth.transport.requests
155 import requests
156
157 request = google.auth.transport.requests.Request()
158
159 credentials.refresh(request)
160
161 Args:
162 session (requests.Session): An instance :class:`requests.Session` used
163 to make HTTP requests. If not specified, a session will be created.
164
165 .. automethod:: __call__
166 """
167
168 _mtls_adapter = None
169 _mtls_unavailable = False
170
171 def __init__(self, session: Optional[requests.Session] = None) -> None:
172 if not session:
173 session = requests.Session()
174
175 self.session = session
176 self._mtls_adapter = None
177 self._mtls_unavailable = False
178 self._mtls_lock = threading.Lock()
179
180 def __getstate__(self):
181 state = self.__dict__.copy()
182 state["_mtls_lock"] = None
183 return state
184
185 def __setstate__(self, state):
186 self.__dict__.update(state)
187 self._mtls_lock = threading.Lock()
188 if not isinstance(self._mtls_adapter, _MutualTlsAdapter):
189 self._mtls_adapter = None
190
191 def __del__(self):
192 try:
193 if hasattr(self, "session") and self.session is not None:
194 self.session.close()
195 except TypeError:
196 # NOTE: For certain Python binary built, the queue.Empty exception
197 # might not be considered a normal Python exception causing
198 # TypeError.
199 pass
200
201 def _configure_mtls_if_needed(self, url):
202 """Lazily mounts a mutual TLS adapter onto the session for mTLS endpoints.
203
204 Args:
205 url (str): The target request URL.
206 """
207 if not _mtls_helper.is_mtls_endpoint(url):
208 return
209
210 # Mount on the specific origin rather than globally on "https://" so client
211 # certificates are not sent to other hosts sharing this session. The trailing
212 # slash prevents requests' startswith() matching from matching lookalike domains.
213 parsed = urllib_parse.urlparse(url)
214 prefix = f"{parsed.scheme}://{parsed.netloc.lower()}/"
215 with self._mtls_lock:
216 session_adapter = self.session.get_adapter(url if parsed.path else prefix)
217 # Skip if _MutualTlsAdapter is already mounted or the caller mounted a custom adapter.
218 if (
219 type(session_adapter) is not requests.adapters.HTTPAdapter
220 or self.session.get_adapter(prefix) is not session_adapter
221 ):
222 return
223
224 if self._mtls_adapter is None:
225 if self._mtls_unavailable or not _mtls_helper.check_use_client_cert():
226 return
227 has_cert, cert, key = _mtls_helper.get_client_cert_and_key()
228 if not has_cert:
229 self._mtls_unavailable = True
230 return
231
232 self._mtls_adapter = _MutualTlsAdapter(
233 cert,
234 key,
235 max_retries=session_adapter.max_retries,
236 pool_connections=session_adapter._pool_connections,
237 pool_maxsize=session_adapter._pool_maxsize,
238 pool_block=session_adapter._pool_block,
239 )
240
241 _replace_adapter(self.session, prefix, self._mtls_adapter)
242
243 def __call__(
244 self,
245 url,
246 method="GET",
247 body=None,
248 headers=None,
249 timeout=_DEFAULT_TIMEOUT,
250 **kwargs,
251 ):
252 """Make an HTTP request using requests.
253
254 Args:
255 url (str): The URI to be requested.
256 method (str): The HTTP method to use for the request. Defaults
257 to 'GET'.
258 body (bytes): The payload or body in HTTP request.
259 headers (Mapping[str, str]): Request headers.
260 timeout (Optional[int]): The number of seconds to wait for a
261 response from the server. If not specified or if None, the
262 requests default timeout will be used.
263 kwargs: Additional arguments passed through to the underlying
264 requests :meth:`~requests.Session.request` method.
265
266 Returns:
267 google.auth.transport.Response: The HTTP response.
268
269 Raises:
270 google.auth.exceptions.TransportError: If any exception occurred.
271 """
272 if not kwargs.get("cert"):
273 try:
274 self._configure_mtls_if_needed(url)
275 except (
276 exceptions.MutualTLSChannelError,
277 exceptions.ClientCertError,
278 OSError,
279 ) as caught_exc:
280 new_exc = exceptions.TransportError(caught_exc)
281 raise new_exc from caught_exc
282
283 # Missing CA bundles or cert paths raised by requests must bubble up
284 # unwrapped to fail immediately rather than triggering client retries.
285 try:
286 _helpers.request_log(_LOGGER, method, url, body, headers)
287 response = self.session.request(
288 method, url, data=body, headers=headers, timeout=timeout, **kwargs
289 )
290 _helpers.response_log(_LOGGER, response)
291 return _Response(response)
292 except requests.exceptions.RequestException as caught_exc:
293 new_exc = exceptions.TransportError(caught_exc)
294 raise new_exc from caught_exc
295
296
297class _MutualTlsAdapter(requests.adapters.HTTPAdapter):
298 """
299 A TransportAdapter that enables mutual TLS.
300
301 Args:
302 cert (bytes): client certificate in PEM format
303 key (bytes): client private key in PEM format
304
305 Raises:
306 ImportError: if certifi is not installed
307 google.auth.exceptions.MutualTLSChannelError: If the cert or key is invalid.
308 """
309
310 def __init__(self, cert, key, **kwargs):
311 import ssl
312
313 import certifi
314
315 ctx_poolmanager = create_urllib3_context()
316 ctx_poolmanager.check_hostname = False
317 ctx_poolmanager.load_verify_locations(cafile=certifi.where())
318
319 ctx_proxymanager = create_urllib3_context()
320 ctx_proxymanager.check_hostname = False
321 ctx_proxymanager.load_verify_locations(cafile=certifi.where())
322
323 try:
324 with _mtls_helper.secure_cert_key_paths(cert, key) as (
325 cert_path,
326 key_path,
327 passphrase,
328 ):
329 password = passphrase
330 ctx_poolmanager.load_cert_chain(
331 certfile=cert_path,
332 keyfile=key_path,
333 password=password,
334 )
335 ctx_proxymanager.load_cert_chain(
336 certfile=cert_path,
337 keyfile=key_path,
338 password=password,
339 )
340 except (
341 ssl.SSLError,
342 OSError,
343 IOError,
344 ValueError,
345 RuntimeError,
346 TypeError,
347 ) as exc:
348 raise exceptions.MutualTLSChannelError(
349 "Failed to configure client certificate and key for mTLS."
350 ) from exc
351
352 self._ctx_poolmanager = ctx_poolmanager
353 self._ctx_proxymanager = ctx_proxymanager
354
355 super(_MutualTlsAdapter, self).__init__(**kwargs)
356
357 def __deepcopy__(self, memo):
358 new_adapter = object.__new__(type(self))
359 memo[id(self)] = new_adapter
360 new_adapter._ctx_poolmanager = self._ctx_poolmanager
361 new_adapter._ctx_proxymanager = self._ctx_proxymanager
362 new_adapter.__setstate__(copy.deepcopy(self.__getstate__(), memo))
363 return new_adapter
364
365 def __reduce_ex__(self, protocol):
366 return (requests.adapters.HTTPAdapter, (), self.__getstate__())
367
368 def init_poolmanager(self, *args, **kwargs):
369 kwargs["ssl_context"] = self._ctx_poolmanager
370 super(_MutualTlsAdapter, self).init_poolmanager(*args, **kwargs)
371
372 def proxy_manager_for(self, *args, **kwargs):
373 kwargs["ssl_context"] = self._ctx_proxymanager
374 return super(_MutualTlsAdapter, self).proxy_manager_for(*args, **kwargs)
375
376
377class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter):
378 """
379 A TransportAdapter that enables mutual TLS and offloads the client side
380 signing operation to the signing library.
381
382 Args:
383 enterprise_cert_file_path (str): the path to a enterprise cert JSON
384 file. The file should contain the following field:
385
386 {
387 "libs": {
388 "signer_library": "...",
389 "offload_library": "..."
390 }
391 }
392
393 Raises:
394 ImportError: if certifi is not installed
395 google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
396 creation failed for any reason.
397 """
398
399 def __init__(self, enterprise_cert_file_path):
400 import certifi
401
402 from google.auth.transport import _custom_tls_signer
403
404 self.signer = _custom_tls_signer.CustomTlsSigner(enterprise_cert_file_path)
405 self.signer.load_libraries()
406
407 poolmanager = create_urllib3_context()
408 poolmanager.load_verify_locations(cafile=certifi.where())
409 self.signer.attach_to_ssl_context(poolmanager)
410 self._ctx_poolmanager = poolmanager
411
412 proxymanager = create_urllib3_context()
413 proxymanager.load_verify_locations(cafile=certifi.where())
414 self.signer.attach_to_ssl_context(proxymanager)
415 self._ctx_proxymanager = proxymanager
416
417 super(_MutualTlsOffloadAdapter, self).__init__()
418
419 def init_poolmanager(self, *args, **kwargs):
420 kwargs["ssl_context"] = self._ctx_poolmanager
421 super(_MutualTlsOffloadAdapter, self).init_poolmanager(*args, **kwargs)
422
423 def proxy_manager_for(self, *args, **kwargs):
424 kwargs["ssl_context"] = self._ctx_proxymanager
425 return super(_MutualTlsOffloadAdapter, self).proxy_manager_for(*args, **kwargs)
426
427
428class AuthorizedSession(requests.Session):
429 """A Requests Session class with credentials.
430
431 This class is used to perform requests to API endpoints that require
432 authorization::
433
434 from google.auth.transport.requests import AuthorizedSession
435
436 authed_session = AuthorizedSession(credentials)
437
438 response = authed_session.request(
439 'GET', 'https://www.googleapis.com/storage/v1/b')
440
441
442 The underlying :meth:`request` implementation handles adding the
443 credentials' headers to the request and refreshing credentials as needed.
444
445 This class also supports mutual TLS via :meth:`configure_mtls_channel`
446 method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
447 environment variable must be explicitly set to ``true``, otherwise it does
448 nothing. Assume the environment is set to ``true``, the method behaves in the
449 following manner:
450
451 If client_cert_callback is provided, client certificate and private
452 key are loaded using the callback; if client_cert_callback is None,
453 application default SSL credentials will be used. Exceptions are raised if
454 there are problems with the certificate, private key, or the loading process,
455 so it should be called within a try/except block.
456
457 First we set the environment variable to ``true``, then create an :class:`AuthorizedSession`
458 instance and specify the endpoints::
459
460 regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
461 mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
462
463 authed_session = AuthorizedSession(credentials)
464
465 Now we can pass a callback to :meth:`configure_mtls_channel`::
466
467 def my_cert_callback():
468 # some code to load client cert bytes and private key bytes, both in
469 # PEM format.
470 some_code_to_load_client_cert_and_key()
471 if loaded:
472 return cert, key
473 raise MyClientCertFailureException()
474
475 # Always call configure_mtls_channel within a try/except block.
476 try:
477 authed_session.configure_mtls_channel(my_cert_callback)
478 except:
479 # handle exceptions.
480
481 if authed_session.is_mtls:
482 response = authed_session.request('GET', mtls_endpoint)
483 else:
484 response = authed_session.request('GET', regular_endpoint)
485
486
487 You can alternatively use application default SSL credentials like this::
488
489 try:
490 authed_session.configure_mtls_channel()
491 except:
492 # handle exceptions.
493
494 Args:
495 credentials (google.auth.credentials.Credentials): The credentials to
496 add to the request.
497 refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
498 that credentials should be refreshed and the request should be
499 retried.
500 max_refresh_attempts (int): The maximum number of times to attempt to
501 refresh the credentials and retry the request.
502 refresh_timeout (Optional[int]): The timeout value in seconds for
503 credential refresh HTTP requests.
504 auth_request (google.auth.transport.requests.Request):
505 (Optional) An instance of
506 :class:`~google.auth.transport.requests.Request` used when
507 refreshing credentials. If not passed,
508 an instance of :class:`~google.auth.transport.requests.Request`
509 is created.
510 default_host (Optional[str]): A host like "pubsub.googleapis.com".
511 This is used when a self-signed JWT is created from service
512 account credentials.
513 """
514
515 def __init__(
516 self,
517 credentials,
518 refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
519 max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
520 refresh_timeout=None,
521 auth_request=None,
522 default_host=None,
523 ):
524 super(AuthorizedSession, self).__init__()
525 self.credentials = credentials
526 self._refresh_status_codes = refresh_status_codes
527 self._max_refresh_attempts = max_refresh_attempts
528 self._refresh_timeout = refresh_timeout
529 self._is_mtls = False
530 self._default_host = default_host
531 self._mtls_reauth_lock = threading.Lock()
532
533 if auth_request is None:
534 self._auth_request_session = requests.Session()
535
536 # Using an adapter to make HTTP requests robust to network errors.
537 # This adapter retrys HTTP requests when network errors occur
538 # and the requests seems safely retryable.
539 retry_adapter = requests.adapters.HTTPAdapter(max_retries=3)
540 self._auth_request_session.mount("https://", retry_adapter)
541
542 # Do not pass `self` as the session here, as it can lead to
543 # infinite recursion.
544 auth_request = Request(self._auth_request_session)
545 else:
546 self._auth_request_session = None
547
548 # Request instance used by internal methods (for example,
549 # credentials.refresh).
550 self._auth_request = auth_request
551
552 # https://google.aip.dev/auth/4111
553 # Attempt to use self-signed JWTs when a service account is used.
554 if isinstance(self.credentials, service_account.Credentials):
555 self.credentials._create_self_signed_jwt(
556 "https://{}/".format(self._default_host) if self._default_host else None
557 )
558
559 def configure_mtls_channel(self, client_cert_callback=None):
560 """Configure the client certificate and key for SSL connection.
561
562 This method configures mTLS if client certificates are explicitly enabled
563 (via GOOGLE_API_USE_CLIENT_CERTIFICATE=true) or auto-enabled (when the env
564 variable is unset and workload certificates are discovered). In these cases,
565 if the client certificate and key are successfully obtained, a
566 :class:`_MutualTlsAdapter` instance will be mounted to the "https://" prefix.
567
568 Args:
569 client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
570 The optional callback returns the client certificate and private
571 key bytes both in PEM format.
572 If the callback is None, application default SSL credentials
573 will be used.
574
575 .. warning::
576 Calling this method replaces the underlying `requests.Session`
577 adapters and closes the previous ones. It is not thread-safe to call
578 this explicitly while other threads are making requests.
579
580 Raises:
581 google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
582 creation failed for any reason. The existing session state (such
583 as adapter mounts) remains unmodified if this error is raised.
584 """
585 use_client_cert = google.auth.transport._mtls_helper.check_use_client_cert()
586 if not use_client_cert:
587 return
588
589 try:
590 (
591 is_mtls,
592 cert,
593 key,
594 ) = google.auth.transport._mtls_helper.get_client_cert_and_key(
595 client_cert_callback
596 )
597
598 old_adapter = self.adapters.get("https://")
599
600 kwargs = {}
601 if old_adapter is not None:
602 kwargs["max_retries"] = getattr(old_adapter, "max_retries", 0)
603 kwargs["pool_connections"] = getattr(
604 old_adapter, "_pool_connections", requests.adapters.DEFAULT_POOLSIZE
605 )
606 kwargs["pool_maxsize"] = getattr(
607 old_adapter, "_pool_maxsize", requests.adapters.DEFAULT_POOLSIZE
608 )
609 kwargs["pool_block"] = getattr(
610 old_adapter, "_pool_block", requests.adapters.DEFAULT_POOLBLOCK
611 )
612
613 old_auth_adapter = None
614 auth_kwargs = {}
615 if self._auth_request_session is not None:
616 old_auth_adapter = self._auth_request_session.adapters.get("https://")
617
618 if old_auth_adapter is not None:
619 auth_kwargs["max_retries"] = getattr(
620 old_auth_adapter, "max_retries", 0
621 )
622 auth_kwargs["pool_connections"] = getattr(
623 old_auth_adapter,
624 "_pool_connections",
625 requests.adapters.DEFAULT_POOLSIZE,
626 )
627 auth_kwargs["pool_maxsize"] = getattr(
628 old_auth_adapter,
629 "_pool_maxsize",
630 requests.adapters.DEFAULT_POOLSIZE,
631 )
632 auth_kwargs["pool_block"] = getattr(
633 old_auth_adapter,
634 "_pool_block",
635 requests.adapters.DEFAULT_POOLBLOCK,
636 )
637
638 if is_mtls:
639 new_adapter = _MutualTlsAdapter(cert, key, **kwargs)
640 if self._auth_request_session is not None:
641 new_auth_adapter = _MutualTlsAdapter(cert, key, **auth_kwargs)
642 else:
643 new_auth_adapter = None
644 else:
645 new_adapter = requests.adapters.HTTPAdapter(**kwargs)
646 if self._auth_request_session is not None:
647 new_auth_adapter = requests.adapters.HTTPAdapter(**auth_kwargs)
648 else:
649 new_auth_adapter = None
650 except (
651 exceptions.ClientCertError,
652 ImportError,
653 OSError,
654 ValueError,
655 ) as caught_exc:
656 new_exc = exceptions.MutualTLSChannelError(caught_exc)
657 raise new_exc from caught_exc
658
659 _replace_adapter(self, "https://", new_adapter)
660
661 if old_adapter is not None and old_adapter is not new_adapter:
662 old_adapter.close()
663
664 if self._auth_request_session is not None and new_auth_adapter is not None:
665 _replace_adapter(self._auth_request_session, "https://", new_auth_adapter)
666
667 if (
668 old_auth_adapter is not None
669 and old_auth_adapter is not new_auth_adapter
670 ):
671 old_auth_adapter.close()
672
673 self._is_mtls = is_mtls
674 if is_mtls:
675 self._cached_cert = cert
676 else:
677 if hasattr(self, "_cached_cert"):
678 del self._cached_cert
679
680 def request(
681 self,
682 method,
683 url,
684 data=None,
685 headers=None,
686 max_allowed_time=None,
687 timeout=_DEFAULT_TIMEOUT,
688 **kwargs,
689 ):
690 """Implementation of Requests' request.
691
692 Args:
693 timeout (Optional[Union[float, Tuple[float, float]]]):
694 The amount of time in seconds to wait for the server response
695 with each individual request. Can also be passed as a tuple
696 ``(connect_timeout, read_timeout)``. See :meth:`requests.Session.request`
697 documentation for details.
698 max_allowed_time (Optional[float]):
699 If the method runs longer than this, a ``Timeout`` exception is
700 automatically raised. Unlike the ``timeout`` parameter, this
701 value applies to the total method execution time, even if
702 multiple requests are made under the hood.
703
704 Mind that it is not guaranteed that the timeout error is raised
705 at ``max_allowed_time``. It might take longer, for example, if
706 an underlying request takes a lot of time, but the request
707 itself does not timeout, e.g. if a large file is being
708 transmitted. The timeout error will be raised after such
709 request completes.
710 Raises:
711 google.auth.exceptions.MutualTLSChannelError: If mutual TLS
712 channel creation fails for any reason.
713 ValueError: If the client certificate is invalid.
714 """
715 # pylint: disable=arguments-differ
716 # Requests has a ton of arguments to request, but only two
717 # (method, url) are required. We pass through all of the other
718 # arguments to super, so no need to exhaustively list them here.
719
720 # Use a kwarg for this instead of an attribute to maintain
721 # thread-safety.
722 _credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0)
723
724 # Make a copy of the headers. They will be modified by the credentials
725 # and we want to pass the original headers if we recurse.
726 request_headers = headers.copy() if headers is not None else {}
727
728 # Do not apply the timeout unconditionally in order to not override the
729 # _auth_request's default timeout.
730 auth_request = (
731 self._auth_request
732 if timeout is None
733 else functools.partial(self._auth_request, timeout=timeout)
734 )
735
736 remaining_time = max_allowed_time
737
738 with TimeoutGuard(remaining_time) as guard:
739 self.credentials.before_request(auth_request, method, url, request_headers)
740 remaining_time = guard.remaining_timeout
741
742 # Remember the client certificate that this request is sent with. If
743 # the request receives a 401 response, this tells whether another
744 # thread has already rotated the certificate in the meantime.
745 cert_at_request = getattr(self, "_cached_cert", None)
746
747 with TimeoutGuard(remaining_time) as guard:
748 _helpers.request_log(_LOGGER, method, url, data, headers)
749 response = super(AuthorizedSession, self).request(
750 method,
751 url,
752 data=data,
753 headers=request_headers,
754 timeout=timeout,
755 **kwargs,
756 )
757 remaining_time = guard.remaining_timeout
758
759 # If the response indicated that the credentials needed to be
760 # refreshed, then refresh the credentials and re-attempt the
761 # request.
762 # A stored token may expire between the time it is retrieved and
763 # the time the request is made, so we may need to try twice.
764 if (
765 response.status_code in self._refresh_status_codes
766 and _credential_refresh_attempt < self._max_refresh_attempts
767 ):
768 # Handle unauthorized permission error(401 status code)
769 if response.status_code == http_client.UNAUTHORIZED:
770 use_mtls = self.is_mtls and _mtls_helper.is_mtls_endpoint(url)
771 if use_mtls:
772 with TimeoutGuard(remaining_time) as guard:
773 self._reconfigure_mtls_channel_if_cert_changed(cert_at_request)
774 remaining_time = guard.remaining_timeout
775 _LOGGER.info(
776 "Refreshing credentials due to a %s response. Attempt %s/%s.",
777 response.status_code,
778 _credential_refresh_attempt + 1,
779 self._max_refresh_attempts,
780 )
781
782 # Do not apply the timeout unconditionally in order to not override the
783 # _auth_request's default timeout.
784 auth_request = (
785 self._auth_request
786 if timeout is None
787 else functools.partial(self._auth_request, timeout=timeout)
788 )
789
790 with TimeoutGuard(remaining_time) as guard:
791 self.credentials.refresh(auth_request)
792 remaining_time = guard.remaining_timeout
793
794 # Recurse. Pass in the original headers, not our modified set, but
795 # do pass the adjusted max allowed time (i.e. the remaining total time).
796 return self.request(
797 method,
798 url,
799 data=data,
800 headers=headers,
801 max_allowed_time=remaining_time,
802 timeout=timeout,
803 _credential_refresh_attempt=_credential_refresh_attempt + 1,
804 **kwargs,
805 )
806
807 return response
808
809 def _reconfigure_mtls_channel_if_cert_changed(self, cert_at_request):
810 """Reconfigures the mTLS channel if the client certificate has changed.
811
812 This is called when a request to an mTLS endpoint receives a 401
813 response. The certificate check and the channel reconfiguration run
814 while holding ``_mtls_reauth_lock``. When several threads receive a 401
815 response at the same time, only the first one reads the current
816 certificate and remounts the adapters. The other threads see that the
817 channel has already been reconfigured and reuse it.
818
819 Args:
820 cert_at_request (Optional[bytes]): The cached client certificate at
821 the time the request that received the 401 response was sent.
822
823 Raises:
824 google.auth.exceptions.MutualTLSChannelError: If the client
825 certificate has changed but the mTLS channel could not be
826 reconfigured.
827 """
828 with self._mtls_reauth_lock:
829 if not self.is_mtls:
830 # mTLS was disabled by another thread while this one waited.
831 return
832
833 if getattr(self, "_cached_cert", None) != cert_at_request:
834 _LOGGER.info(
835 "Skipping reconfiguration of mTLS channel because it has already"
836 " been reconfigured with a new client certificate."
837 )
838 return
839
840 try:
841 (
842 call_cert_bytes,
843 call_key_bytes,
844 cached_fingerprint,
845 current_cert_fingerprint,
846 ) = _mtls_helper.check_parameters_for_unauthorized_response(
847 cert_at_request
848 )
849 except Exception as e:
850 # Failing to load the current certificate (for example, because
851 # the certificate file is missing or the certificate provider
852 # failed) must not prevent the token refresh that follows.
853 _LOGGER.warning(
854 "Failed to check whether the client certificate has changed: %s",
855 e,
856 )
857 return
858
859 if cached_fingerprint == current_cert_fingerprint:
860 _LOGGER.info(
861 "Skipping reconfiguration of mTLS channel because the client"
862 " certificate has not changed."
863 )
864 return
865
866 try:
867 _LOGGER.info(
868 "Client certificate has changed, reconfiguring mTLS channel."
869 )
870 self.configure_mtls_channel(lambda: (call_cert_bytes, call_key_bytes))
871 except Exception as e:
872 _LOGGER.error("Failed to reconfigure mTLS channel: %s", e)
873 raise exceptions.MutualTLSChannelError(
874 "Failed to reconfigure mTLS channel"
875 ) from e
876
877 @property
878 def is_mtls(self):
879 """Indicates if the created SSL channel is mutual TLS."""
880 return self._is_mtls
881
882 def close(self):
883 if self._auth_request_session is not None:
884 self._auth_request_session.close()
885 super(AuthorizedSession, self).close()