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
119class Request(transport.Request):
120 """Requests request adapter.
121
122 This class is used internally for making requests using various transports
123 in a consistent way. If you use :class:`AuthorizedSession` you do not need
124 to construct or use this class directly.
125
126 This class can be useful if you want to manually refresh a
127 :class:`~google.auth.credentials.Credentials` instance::
128
129 import google.auth.transport.requests
130 import requests
131
132 request = google.auth.transport.requests.Request()
133
134 credentials.refresh(request)
135
136 Args:
137 session (requests.Session): An instance :class:`requests.Session` used
138 to make HTTP requests. If not specified, a session will be created.
139
140 .. automethod:: __call__
141 """
142
143 _mtls_adapter = None
144 _mtls_unavailable = False
145
146 def __init__(self, session: Optional[requests.Session] = None) -> None:
147 if not session:
148 session = requests.Session()
149
150 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
165
166 def __del__(self):
167 try:
168 if hasattr(self, "session") and self.session is not None:
169 self.session.close()
170 except TypeError:
171 # NOTE: For certain Python binary built, the queue.Empty exception
172 # might not be considered a normal Python exception causing
173 # TypeError.
174 pass
175
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
229 def __call__(
230 self,
231 url,
232 method="GET",
233 body=None,
234 headers=None,
235 timeout=_DEFAULT_TIMEOUT,
236 **kwargs,
237 ):
238 """Make an HTTP request using requests.
239
240 Args:
241 url (str): The URI to be requested.
242 method (str): The HTTP method to use for the request. Defaults
243 to 'GET'.
244 body (bytes): The payload or body in HTTP request.
245 headers (Mapping[str, str]): Request headers.
246 timeout (Optional[int]): The number of seconds to wait for a
247 response from the server. If not specified or if None, the
248 requests default timeout will be used.
249 kwargs: Additional arguments passed through to the underlying
250 requests :meth:`~requests.Session.request` method.
251
252 Returns:
253 google.auth.transport.Response: The HTTP response.
254
255 Raises:
256 google.auth.exceptions.TransportError: If any exception occurred.
257 """
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.
271 try:
272 _helpers.request_log(_LOGGER, method, url, body, headers)
273 response = self.session.request(
274 method, url, data=body, headers=headers, timeout=timeout, **kwargs
275 )
276 _helpers.response_log(_LOGGER, response)
277 return _Response(response)
278 except requests.exceptions.RequestException as caught_exc:
279 new_exc = exceptions.TransportError(caught_exc)
280 raise new_exc from caught_exc
281
282
283class _MutualTlsAdapter(requests.adapters.HTTPAdapter):
284 """
285 A TransportAdapter that enables mutual TLS.
286
287 Args:
288 cert (bytes): client certificate in PEM format
289 key (bytes): client private key in PEM format
290
291 Raises:
292 ImportError: if certifi is not installed
293 google.auth.exceptions.MutualTLSChannelError: If the cert or key is invalid.
294 """
295
296 def __init__(self, cert, key, **kwargs):
297 import ssl
298
299 import certifi
300
301 ctx_poolmanager = create_urllib3_context()
302 ctx_poolmanager.check_hostname = False
303 ctx_poolmanager.load_verify_locations(cafile=certifi.where())
304
305 ctx_proxymanager = create_urllib3_context()
306 ctx_proxymanager.check_hostname = False
307 ctx_proxymanager.load_verify_locations(cafile=certifi.where())
308
309 try:
310 with _mtls_helper.secure_cert_key_paths(cert, key) as (
311 cert_path,
312 key_path,
313 passphrase,
314 ):
315 password = passphrase
316 ctx_poolmanager.load_cert_chain(
317 certfile=cert_path,
318 keyfile=key_path,
319 password=password,
320 )
321 ctx_proxymanager.load_cert_chain(
322 certfile=cert_path,
323 keyfile=key_path,
324 password=password,
325 )
326 except (
327 ssl.SSLError,
328 OSError,
329 IOError,
330 ValueError,
331 RuntimeError,
332 TypeError,
333 ) as exc:
334 raise exceptions.MutualTLSChannelError(
335 "Failed to configure client certificate and key for mTLS."
336 ) from exc
337
338 self._ctx_poolmanager = ctx_poolmanager
339 self._ctx_proxymanager = ctx_proxymanager
340
341 super(_MutualTlsAdapter, self).__init__(**kwargs)
342
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
354 def init_poolmanager(self, *args, **kwargs):
355 kwargs["ssl_context"] = self._ctx_poolmanager
356 super(_MutualTlsAdapter, self).init_poolmanager(*args, **kwargs)
357
358 def proxy_manager_for(self, *args, **kwargs):
359 kwargs["ssl_context"] = self._ctx_proxymanager
360 return super(_MutualTlsAdapter, self).proxy_manager_for(*args, **kwargs)
361
362
363class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter):
364 """
365 A TransportAdapter that enables mutual TLS and offloads the client side
366 signing operation to the signing library.
367
368 Args:
369 enterprise_cert_file_path (str): the path to a enterprise cert JSON
370 file. The file should contain the following field:
371
372 {
373 "libs": {
374 "signer_library": "...",
375 "offload_library": "..."
376 }
377 }
378
379 Raises:
380 ImportError: if certifi is not installed
381 google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
382 creation failed for any reason.
383 """
384
385 def __init__(self, enterprise_cert_file_path):
386 import certifi
387
388 from google.auth.transport import _custom_tls_signer
389
390 self.signer = _custom_tls_signer.CustomTlsSigner(enterprise_cert_file_path)
391 self.signer.load_libraries()
392
393 poolmanager = create_urllib3_context()
394 poolmanager.load_verify_locations(cafile=certifi.where())
395 self.signer.attach_to_ssl_context(poolmanager)
396 self._ctx_poolmanager = poolmanager
397
398 proxymanager = create_urllib3_context()
399 proxymanager.load_verify_locations(cafile=certifi.where())
400 self.signer.attach_to_ssl_context(proxymanager)
401 self._ctx_proxymanager = proxymanager
402
403 super(_MutualTlsOffloadAdapter, self).__init__()
404
405 def init_poolmanager(self, *args, **kwargs):
406 kwargs["ssl_context"] = self._ctx_poolmanager
407 super(_MutualTlsOffloadAdapter, self).init_poolmanager(*args, **kwargs)
408
409 def proxy_manager_for(self, *args, **kwargs):
410 kwargs["ssl_context"] = self._ctx_proxymanager
411 return super(_MutualTlsOffloadAdapter, self).proxy_manager_for(*args, **kwargs)
412
413
414class AuthorizedSession(requests.Session):
415 """A Requests Session class with credentials.
416
417 This class is used to perform requests to API endpoints that require
418 authorization::
419
420 from google.auth.transport.requests import AuthorizedSession
421
422 authed_session = AuthorizedSession(credentials)
423
424 response = authed_session.request(
425 'GET', 'https://www.googleapis.com/storage/v1/b')
426
427
428 The underlying :meth:`request` implementation handles adding the
429 credentials' headers to the request and refreshing credentials as needed.
430
431 This class also supports mutual TLS via :meth:`configure_mtls_channel`
432 method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
433 environment variable must be explicitly set to ``true``, otherwise it does
434 nothing. Assume the environment is set to ``true``, the method behaves in the
435 following manner:
436
437 If client_cert_callback is provided, client certificate and private
438 key are loaded using the callback; if client_cert_callback is None,
439 application default SSL credentials will be used. Exceptions are raised if
440 there are problems with the certificate, private key, or the loading process,
441 so it should be called within a try/except block.
442
443 First we set the environment variable to ``true``, then create an :class:`AuthorizedSession`
444 instance and specify the endpoints::
445
446 regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
447 mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
448
449 authed_session = AuthorizedSession(credentials)
450
451 Now we can pass a callback to :meth:`configure_mtls_channel`::
452
453 def my_cert_callback():
454 # some code to load client cert bytes and private key bytes, both in
455 # PEM format.
456 some_code_to_load_client_cert_and_key()
457 if loaded:
458 return cert, key
459 raise MyClientCertFailureException()
460
461 # Always call configure_mtls_channel within a try/except block.
462 try:
463 authed_session.configure_mtls_channel(my_cert_callback)
464 except:
465 # handle exceptions.
466
467 if authed_session.is_mtls:
468 response = authed_session.request('GET', mtls_endpoint)
469 else:
470 response = authed_session.request('GET', regular_endpoint)
471
472
473 You can alternatively use application default SSL credentials like this::
474
475 try:
476 authed_session.configure_mtls_channel()
477 except:
478 # handle exceptions.
479
480 Args:
481 credentials (google.auth.credentials.Credentials): The credentials to
482 add to the request.
483 refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
484 that credentials should be refreshed and the request should be
485 retried.
486 max_refresh_attempts (int): The maximum number of times to attempt to
487 refresh the credentials and retry the request.
488 refresh_timeout (Optional[int]): The timeout value in seconds for
489 credential refresh HTTP requests.
490 auth_request (google.auth.transport.requests.Request):
491 (Optional) An instance of
492 :class:`~google.auth.transport.requests.Request` used when
493 refreshing credentials. If not passed,
494 an instance of :class:`~google.auth.transport.requests.Request`
495 is created.
496 default_host (Optional[str]): A host like "pubsub.googleapis.com".
497 This is used when a self-signed JWT is created from service
498 account credentials.
499 """
500
501 def __init__(
502 self,
503 credentials,
504 refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
505 max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
506 refresh_timeout=None,
507 auth_request=None,
508 default_host=None,
509 ):
510 super(AuthorizedSession, self).__init__()
511 self.credentials = credentials
512 self._refresh_status_codes = refresh_status_codes
513 self._max_refresh_attempts = max_refresh_attempts
514 self._refresh_timeout = refresh_timeout
515 self._is_mtls = False
516 self._default_host = default_host
517
518 if auth_request is None:
519 self._auth_request_session = requests.Session()
520
521 # Using an adapter to make HTTP requests robust to network errors.
522 # This adapter retrys HTTP requests when network errors occur
523 # and the requests seems safely retryable.
524 retry_adapter = requests.adapters.HTTPAdapter(max_retries=3)
525 self._auth_request_session.mount("https://", retry_adapter)
526
527 # Do not pass `self` as the session here, as it can lead to
528 # infinite recursion.
529 auth_request = Request(self._auth_request_session)
530 else:
531 self._auth_request_session = None
532
533 # Request instance used by internal methods (for example,
534 # credentials.refresh).
535 self._auth_request = auth_request
536
537 # https://google.aip.dev/auth/4111
538 # Attempt to use self-signed JWTs when a service account is used.
539 if isinstance(self.credentials, service_account.Credentials):
540 self.credentials._create_self_signed_jwt(
541 "https://{}/".format(self._default_host) if self._default_host else None
542 )
543
544 def configure_mtls_channel(self, client_cert_callback=None):
545 """Configure the client certificate and key for SSL connection.
546
547 This method configures mTLS if client certificates are explicitly enabled
548 (via GOOGLE_API_USE_CLIENT_CERTIFICATE=true) or auto-enabled (when the env
549 variable is unset and workload certificates are discovered). In these cases,
550 if the client certificate and key are successfully obtained, a
551 :class:`_MutualTlsAdapter` instance will be mounted to the "https://" prefix.
552
553 Args:
554 client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
555 The optional callback returns the client certificate and private
556 key bytes both in PEM format.
557 If the callback is None, application default SSL credentials
558 will be used.
559
560 .. warning::
561 Calling this method mutates the underlying `requests.Session` adapter
562 dictionary. It is not thread-safe to call this explicitly while other
563 threads are making requests.
564
565 Raises:
566 google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
567 creation failed for any reason. The existing session state (such
568 as adapter mounts) remains unmodified if this error is raised.
569 """
570 use_client_cert = google.auth.transport._mtls_helper.check_use_client_cert()
571 if not use_client_cert:
572 return
573
574 try:
575 (
576 is_mtls,
577 cert,
578 key,
579 ) = google.auth.transport._mtls_helper.get_client_cert_and_key(
580 client_cert_callback
581 )
582
583 old_adapter = self.adapters.get("https://")
584
585 kwargs = {}
586 if old_adapter is not None:
587 kwargs["max_retries"] = getattr(old_adapter, "max_retries", 0)
588 kwargs["pool_connections"] = getattr(
589 old_adapter, "_pool_connections", requests.adapters.DEFAULT_POOLSIZE
590 )
591 kwargs["pool_maxsize"] = getattr(
592 old_adapter, "_pool_maxsize", requests.adapters.DEFAULT_POOLSIZE
593 )
594 kwargs["pool_block"] = getattr(
595 old_adapter, "_pool_block", requests.adapters.DEFAULT_POOLBLOCK
596 )
597
598 old_auth_adapter = None
599 auth_kwargs = {}
600 if self._auth_request_session is not None:
601 old_auth_adapter = self._auth_request_session.adapters.get("https://")
602
603 if old_auth_adapter is not None:
604 auth_kwargs["max_retries"] = getattr(
605 old_auth_adapter, "max_retries", 0
606 )
607 auth_kwargs["pool_connections"] = getattr(
608 old_auth_adapter,
609 "_pool_connections",
610 requests.adapters.DEFAULT_POOLSIZE,
611 )
612 auth_kwargs["pool_maxsize"] = getattr(
613 old_auth_adapter,
614 "_pool_maxsize",
615 requests.adapters.DEFAULT_POOLSIZE,
616 )
617 auth_kwargs["pool_block"] = getattr(
618 old_auth_adapter,
619 "_pool_block",
620 requests.adapters.DEFAULT_POOLBLOCK,
621 )
622
623 if is_mtls:
624 new_adapter = _MutualTlsAdapter(cert, key, **kwargs)
625 if self._auth_request_session is not None:
626 new_auth_adapter = _MutualTlsAdapter(cert, key, **auth_kwargs)
627 else:
628 new_auth_adapter = None
629 else:
630 new_adapter = requests.adapters.HTTPAdapter(**kwargs)
631 if self._auth_request_session is not None:
632 new_auth_adapter = requests.adapters.HTTPAdapter(**auth_kwargs)
633 else:
634 new_auth_adapter = None
635 except (
636 exceptions.ClientCertError,
637 ImportError,
638 OSError,
639 ValueError,
640 ) as caught_exc:
641 new_exc = exceptions.MutualTLSChannelError(caught_exc)
642 raise new_exc from caught_exc
643
644 self.mount("https://", new_adapter)
645
646 if old_adapter is not None and old_adapter is not new_adapter:
647 old_adapter.close()
648
649 if self._auth_request_session is not None and new_auth_adapter is not None:
650 self._auth_request_session.mount("https://", new_auth_adapter)
651
652 if (
653 old_auth_adapter is not None
654 and old_auth_adapter is not new_auth_adapter
655 ):
656 old_auth_adapter.close()
657
658 self._is_mtls = is_mtls
659 if is_mtls:
660 self._cached_cert = cert
661 else:
662 if hasattr(self, "_cached_cert"):
663 del self._cached_cert
664
665 def request(
666 self,
667 method,
668 url,
669 data=None,
670 headers=None,
671 max_allowed_time=None,
672 timeout=_DEFAULT_TIMEOUT,
673 **kwargs,
674 ):
675 """Implementation of Requests' request.
676
677 Args:
678 timeout (Optional[Union[float, Tuple[float, float]]]):
679 The amount of time in seconds to wait for the server response
680 with each individual request. Can also be passed as a tuple
681 ``(connect_timeout, read_timeout)``. See :meth:`requests.Session.request`
682 documentation for details.
683 max_allowed_time (Optional[float]):
684 If the method runs longer than this, a ``Timeout`` exception is
685 automatically raised. Unlike the ``timeout`` parameter, this
686 value applies to the total method execution time, even if
687 multiple requests are made under the hood.
688
689 Mind that it is not guaranteed that the timeout error is raised
690 at ``max_allowed_time``. It might take longer, for example, if
691 an underlying request takes a lot of time, but the request
692 itself does not timeout, e.g. if a large file is being
693 transmitted. The timeout error will be raised after such
694 request completes.
695 Raises:
696 google.auth.exceptions.MutualTLSChannelError: If mutual TLS
697 channel creation fails for any reason.
698 ValueError: If the client certificate is invalid.
699 """
700 # pylint: disable=arguments-differ
701 # Requests has a ton of arguments to request, but only two
702 # (method, url) are required. We pass through all of the other
703 # arguments to super, so no need to exhaustively list them here.
704
705 # Use a kwarg for this instead of an attribute to maintain
706 # thread-safety.
707 _credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0)
708
709 # Make a copy of the headers. They will be modified by the credentials
710 # and we want to pass the original headers if we recurse.
711 request_headers = headers.copy() if headers is not None else {}
712
713 # Do not apply the timeout unconditionally in order to not override the
714 # _auth_request's default timeout.
715 auth_request = (
716 self._auth_request
717 if timeout is None
718 else functools.partial(self._auth_request, timeout=timeout)
719 )
720
721 remaining_time = max_allowed_time
722
723 with TimeoutGuard(remaining_time) as guard:
724 self.credentials.before_request(auth_request, method, url, request_headers)
725 remaining_time = guard.remaining_timeout
726
727 with TimeoutGuard(remaining_time) as guard:
728 _helpers.request_log(_LOGGER, method, url, data, headers)
729 response = super(AuthorizedSession, self).request(
730 method,
731 url,
732 data=data,
733 headers=request_headers,
734 timeout=timeout,
735 **kwargs,
736 )
737 remaining_time = guard.remaining_timeout
738
739 # If the response indicated that the credentials needed to be
740 # refreshed, then refresh the credentials and re-attempt the
741 # request.
742 # A stored token may expire between the time it is retrieved and
743 # the time the request is made, so we may need to try twice.
744 if (
745 response.status_code in self._refresh_status_codes
746 and _credential_refresh_attempt < self._max_refresh_attempts
747 ):
748 # Handle unauthorized permission error(401 status code)
749 if response.status_code == http_client.UNAUTHORIZED:
750 use_mtls = self.is_mtls and _mtls_helper.is_mtls_endpoint(url)
751 if use_mtls:
752 (
753 call_cert_bytes,
754 call_key_bytes,
755 cached_fingerprint,
756 current_cert_fingerprint,
757 ) = _mtls_helper.check_parameters_for_unauthorized_response(
758 self._cached_cert
759 )
760 if cached_fingerprint != current_cert_fingerprint:
761 try:
762 _LOGGER.info(
763 "Client certificate has changed, reconfiguring mTLS "
764 "channel."
765 )
766 self.configure_mtls_channel(
767 lambda: (call_cert_bytes, call_key_bytes)
768 )
769 except Exception as e:
770 _LOGGER.error("Failed to reconfigure mTLS channel: %s", e)
771 raise exceptions.MutualTLSChannelError(
772 "Failed to reconfigure mTLS channel"
773 ) from e
774 else:
775 _LOGGER.info(
776 "Skipping reconfiguration of mTLS channel because the client"
777 " certificate has not changed."
778 )
779 _LOGGER.info(
780 "Refreshing credentials due to a %s response. Attempt %s/%s.",
781 response.status_code,
782 _credential_refresh_attempt + 1,
783 self._max_refresh_attempts,
784 )
785
786 # Do not apply the timeout unconditionally in order to not override the
787 # _auth_request's default timeout.
788 auth_request = (
789 self._auth_request
790 if timeout is None
791 else functools.partial(self._auth_request, timeout=timeout)
792 )
793
794 with TimeoutGuard(remaining_time) as guard:
795 self.credentials.refresh(auth_request)
796 remaining_time = guard.remaining_timeout
797
798 # Recurse. Pass in the original headers, not our modified set, but
799 # do pass the adjusted max allowed time (i.e. the remaining total time).
800 return self.request(
801 method,
802 url,
803 data=data,
804 headers=headers,
805 max_allowed_time=remaining_time,
806 timeout=timeout,
807 _credential_refresh_attempt=_credential_refresh_attempt + 1,
808 **kwargs,
809 )
810
811 return response
812
813 @property
814 def is_mtls(self):
815 """Indicates if the created SSL channel is mutual TLS."""
816 return self._is_mtls
817
818 def close(self):
819 if self._auth_request_session is not None:
820 self._auth_request_session.close()
821 super(AuthorizedSession, self).close()