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 functools
20import http.client as http_client
21import logging
22import numbers
23import time
24from typing import Optional
25
26try:
27 import requests
28except ImportError as caught_exc: # pragma: NO COVER
29 raise ImportError(
30 "The requests library is not installed from please install the requests package to use the requests transport."
31 ) from caught_exc
32import requests.adapters # pylint: disable=ungrouped-imports
33import requests.exceptions # pylint: disable=ungrouped-imports
34from requests.packages.urllib3.util.ssl_ import ( # type: ignore
35 create_urllib3_context,
36) # pylint: disable=ungrouped-imports
37
38from google.auth import _helpers
39from google.auth import exceptions
40from google.auth import transport
41from google.auth.transport import _mtls_helper
42import google.auth.transport._mtls_helper
43from google.oauth2 import service_account
44
45_LOGGER = logging.getLogger(__name__)
46
47_DEFAULT_TIMEOUT = 120 # in seconds
48
49
50class _Response(transport.Response):
51 """Requests transport response adapter.
52
53 Args:
54 response (requests.Response): The raw Requests response.
55 """
56
57 def __init__(self, response):
58 self._response = response
59
60 @property
61 def status(self):
62 return self._response.status_code
63
64 @property
65 def headers(self):
66 return self._response.headers
67
68 @property
69 def data(self):
70 return self._response.content
71
72
73class TimeoutGuard(object):
74 """A context manager raising an error if the suite execution took too long.
75
76 Args:
77 timeout (Union[None, Union[float, Tuple[float, float]]]):
78 The maximum number of seconds a suite can run without the context
79 manager raising a timeout exception on exit. If passed as a tuple,
80 the smaller of the values is taken as a timeout. If ``None``, a
81 timeout error is never raised.
82 timeout_error_type (Optional[Exception]):
83 The type of the error to raise on timeout. Defaults to
84 :class:`requests.exceptions.Timeout`.
85 """
86
87 def __init__(self, timeout, timeout_error_type=requests.exceptions.Timeout):
88 self._timeout = timeout
89 self.remaining_timeout = timeout
90 self._timeout_error_type = timeout_error_type
91
92 def __enter__(self):
93 self._start = time.time()
94 return self
95
96 def __exit__(self, exc_type, exc_value, traceback):
97 if exc_value:
98 return # let the error bubble up automatically
99
100 if self._timeout is None:
101 return # nothing to do, the timeout was not specified
102
103 elapsed = time.time() - self._start
104 deadline_hit = False
105
106 if isinstance(self._timeout, numbers.Number):
107 self.remaining_timeout = self._timeout - elapsed
108 deadline_hit = self.remaining_timeout <= 0
109 else:
110 self.remaining_timeout = tuple(x - elapsed for x in self._timeout)
111 deadline_hit = min(self.remaining_timeout) <= 0
112
113 if deadline_hit:
114 raise self._timeout_error_type()
115
116
117class Request(transport.Request):
118 """Requests request adapter.
119
120 This class is used internally for making requests using various transports
121 in a consistent way. If you use :class:`AuthorizedSession` you do not need
122 to construct or use this class directly.
123
124 This class can be useful if you want to manually refresh a
125 :class:`~google.auth.credentials.Credentials` instance::
126
127 import google.auth.transport.requests
128 import requests
129
130 request = google.auth.transport.requests.Request()
131
132 credentials.refresh(request)
133
134 Args:
135 session (requests.Session): An instance :class:`requests.Session` used
136 to make HTTP requests. If not specified, a session will be created.
137
138 .. automethod:: __call__
139 """
140
141 def __init__(self, session: Optional[requests.Session] = None) -> None:
142 if not session:
143 session = requests.Session()
144
145 self.session = session
146
147 def __del__(self):
148 try:
149 if hasattr(self, "session") and self.session is not None:
150 self.session.close()
151 except TypeError:
152 # NOTE: For certain Python binary built, the queue.Empty exception
153 # might not be considered a normal Python exception causing
154 # TypeError.
155 pass
156
157 def __call__(
158 self,
159 url,
160 method="GET",
161 body=None,
162 headers=None,
163 timeout=_DEFAULT_TIMEOUT,
164 **kwargs
165 ):
166 """Make an HTTP request using requests.
167
168 Args:
169 url (str): The URI to be requested.
170 method (str): The HTTP method to use for the request. Defaults
171 to 'GET'.
172 body (bytes): The payload or body in HTTP request.
173 headers (Mapping[str, str]): Request headers.
174 timeout (Optional[int]): The number of seconds to wait for a
175 response from the server. If not specified or if None, the
176 requests default timeout will be used.
177 kwargs: Additional arguments passed through to the underlying
178 requests :meth:`~requests.Session.request` method.
179
180 Returns:
181 google.auth.transport.Response: The HTTP response.
182
183 Raises:
184 google.auth.exceptions.TransportError: If any exception occurred.
185 """
186 try:
187 _helpers.request_log(_LOGGER, method, url, body, headers)
188 response = self.session.request(
189 method, url, data=body, headers=headers, timeout=timeout, **kwargs
190 )
191 _helpers.response_log(_LOGGER, response)
192 return _Response(response)
193 except requests.exceptions.RequestException as caught_exc:
194 new_exc = exceptions.TransportError(caught_exc)
195 raise new_exc from caught_exc
196
197
198class _MutualTlsAdapter(requests.adapters.HTTPAdapter):
199 """
200 A TransportAdapter that enables mutual TLS.
201
202 Args:
203 cert (bytes): client certificate in PEM format
204 key (bytes): client private key in PEM format
205
206 Raises:
207 ImportError: if certifi is not installed
208 google.auth.exceptions.MutualTLSChannelError: If the cert or key is invalid.
209 """
210
211 def __init__(self, cert, key, **kwargs):
212 import certifi
213 import ssl
214
215 ctx_poolmanager = create_urllib3_context()
216 ctx_poolmanager.load_verify_locations(cafile=certifi.where())
217
218 ctx_proxymanager = create_urllib3_context()
219 ctx_proxymanager.load_verify_locations(cafile=certifi.where())
220
221 try:
222 with _mtls_helper.secure_cert_key_paths(cert, key) as (
223 cert_path,
224 key_path,
225 passphrase,
226 ):
227 password = passphrase
228 ctx_poolmanager.load_cert_chain(
229 certfile=cert_path,
230 keyfile=key_path,
231 password=password,
232 )
233 ctx_proxymanager.load_cert_chain(
234 certfile=cert_path,
235 keyfile=key_path,
236 password=password,
237 )
238 except (
239 ssl.SSLError,
240 OSError,
241 IOError,
242 ValueError,
243 RuntimeError,
244 TypeError,
245 ) as exc:
246 raise exceptions.MutualTLSChannelError(
247 "Failed to configure client certificate and key for mTLS."
248 ) from exc
249
250 self._ctx_poolmanager = ctx_poolmanager
251 self._ctx_proxymanager = ctx_proxymanager
252
253 super(_MutualTlsAdapter, self).__init__(**kwargs)
254
255 def init_poolmanager(self, *args, **kwargs):
256 kwargs["ssl_context"] = self._ctx_poolmanager
257 super(_MutualTlsAdapter, self).init_poolmanager(*args, **kwargs)
258
259 def proxy_manager_for(self, *args, **kwargs):
260 kwargs["ssl_context"] = self._ctx_proxymanager
261 return super(_MutualTlsAdapter, self).proxy_manager_for(*args, **kwargs)
262
263
264class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter):
265 """
266 A TransportAdapter that enables mutual TLS and offloads the client side
267 signing operation to the signing library.
268
269 Args:
270 enterprise_cert_file_path (str): the path to a enterprise cert JSON
271 file. The file should contain the following field:
272
273 {
274 "libs": {
275 "signer_library": "...",
276 "offload_library": "..."
277 }
278 }
279
280 Raises:
281 ImportError: if certifi is not installed
282 google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
283 creation failed for any reason.
284 """
285
286 def __init__(self, enterprise_cert_file_path):
287 import certifi
288 from google.auth.transport import _custom_tls_signer
289
290 self.signer = _custom_tls_signer.CustomTlsSigner(enterprise_cert_file_path)
291 self.signer.load_libraries()
292
293 poolmanager = create_urllib3_context()
294 poolmanager.load_verify_locations(cafile=certifi.where())
295 self.signer.attach_to_ssl_context(poolmanager)
296 self._ctx_poolmanager = poolmanager
297
298 proxymanager = create_urllib3_context()
299 proxymanager.load_verify_locations(cafile=certifi.where())
300 self.signer.attach_to_ssl_context(proxymanager)
301 self._ctx_proxymanager = proxymanager
302
303 super(_MutualTlsOffloadAdapter, self).__init__()
304
305 def init_poolmanager(self, *args, **kwargs):
306 kwargs["ssl_context"] = self._ctx_poolmanager
307 super(_MutualTlsOffloadAdapter, self).init_poolmanager(*args, **kwargs)
308
309 def proxy_manager_for(self, *args, **kwargs):
310 kwargs["ssl_context"] = self._ctx_proxymanager
311 return super(_MutualTlsOffloadAdapter, self).proxy_manager_for(*args, **kwargs)
312
313
314class AuthorizedSession(requests.Session):
315 """A Requests Session class with credentials.
316
317 This class is used to perform requests to API endpoints that require
318 authorization::
319
320 from google.auth.transport.requests import AuthorizedSession
321
322 authed_session = AuthorizedSession(credentials)
323
324 response = authed_session.request(
325 'GET', 'https://www.googleapis.com/storage/v1/b')
326
327
328 The underlying :meth:`request` implementation handles adding the
329 credentials' headers to the request and refreshing credentials as needed.
330
331 This class also supports mutual TLS via :meth:`configure_mtls_channel`
332 method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
333 environment variable must be explicitly set to ``true``, otherwise it does
334 nothing. Assume the environment is set to ``true``, the method behaves in the
335 following manner:
336
337 If client_cert_callback is provided, client certificate and private
338 key are loaded using the callback; if client_cert_callback is None,
339 application default SSL credentials will be used. Exceptions are raised if
340 there are problems with the certificate, private key, or the loading process,
341 so it should be called within a try/except block.
342
343 First we set the environment variable to ``true``, then create an :class:`AuthorizedSession`
344 instance and specify the endpoints::
345
346 regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
347 mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
348
349 authed_session = AuthorizedSession(credentials)
350
351 Now we can pass a callback to :meth:`configure_mtls_channel`::
352
353 def my_cert_callback():
354 # some code to load client cert bytes and private key bytes, both in
355 # PEM format.
356 some_code_to_load_client_cert_and_key()
357 if loaded:
358 return cert, key
359 raise MyClientCertFailureException()
360
361 # Always call configure_mtls_channel within a try/except block.
362 try:
363 authed_session.configure_mtls_channel(my_cert_callback)
364 except:
365 # handle exceptions.
366
367 if authed_session.is_mtls:
368 response = authed_session.request('GET', mtls_endpoint)
369 else:
370 response = authed_session.request('GET', regular_endpoint)
371
372
373 You can alternatively use application default SSL credentials like this::
374
375 try:
376 authed_session.configure_mtls_channel()
377 except:
378 # handle exceptions.
379
380 Args:
381 credentials (google.auth.credentials.Credentials): The credentials to
382 add to the request.
383 refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
384 that credentials should be refreshed and the request should be
385 retried.
386 max_refresh_attempts (int): The maximum number of times to attempt to
387 refresh the credentials and retry the request.
388 refresh_timeout (Optional[int]): The timeout value in seconds for
389 credential refresh HTTP requests.
390 auth_request (google.auth.transport.requests.Request):
391 (Optional) An instance of
392 :class:`~google.auth.transport.requests.Request` used when
393 refreshing credentials. If not passed,
394 an instance of :class:`~google.auth.transport.requests.Request`
395 is created.
396 default_host (Optional[str]): A host like "pubsub.googleapis.com".
397 This is used when a self-signed JWT is created from service
398 account credentials.
399 """
400
401 def __init__(
402 self,
403 credentials,
404 refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
405 max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
406 refresh_timeout=None,
407 auth_request=None,
408 default_host=None,
409 ):
410 super(AuthorizedSession, self).__init__()
411 self.credentials = credentials
412 self._refresh_status_codes = refresh_status_codes
413 self._max_refresh_attempts = max_refresh_attempts
414 self._refresh_timeout = refresh_timeout
415 self._is_mtls = False
416 self._default_host = default_host
417
418 if auth_request is None:
419 self._auth_request_session = requests.Session()
420
421 # Using an adapter to make HTTP requests robust to network errors.
422 # This adapter retrys HTTP requests when network errors occur
423 # and the requests seems safely retryable.
424 retry_adapter = requests.adapters.HTTPAdapter(max_retries=3)
425 self._auth_request_session.mount("https://", retry_adapter)
426
427 # Do not pass `self` as the session here, as it can lead to
428 # infinite recursion.
429 auth_request = Request(self._auth_request_session)
430 else:
431 self._auth_request_session = None
432
433 # Request instance used by internal methods (for example,
434 # credentials.refresh).
435 self._auth_request = auth_request
436
437 # https://google.aip.dev/auth/4111
438 # Attempt to use self-signed JWTs when a service account is used.
439 if isinstance(self.credentials, service_account.Credentials):
440 self.credentials._create_self_signed_jwt(
441 "https://{}/".format(self._default_host) if self._default_host else None
442 )
443
444 def configure_mtls_channel(self, client_cert_callback=None):
445 """Configure the client certificate and key for SSL connection.
446
447 This method configures mTLS if client certificates are explicitly enabled
448 (via GOOGLE_API_USE_CLIENT_CERTIFICATE=true) or auto-enabled (when the env
449 variable is unset and workload certificates are discovered). In these cases,
450 if the client certificate and key are successfully obtained, a
451 :class:`_MutualTlsAdapter` instance will be mounted to the "https://" prefix.
452
453 Args:
454 client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
455 The optional callback returns the client certificate and private
456 key bytes both in PEM format.
457 If the callback is None, application default SSL credentials
458 will be used.
459
460 .. warning::
461 Calling this method mutates the underlying `requests.Session` adapter
462 dictionary. It is not thread-safe to call this explicitly while other
463 threads are making requests.
464
465 Raises:
466 google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
467 creation failed for any reason. The existing session state (such
468 as adapter mounts) remains unmodified if this error is raised.
469 """
470 use_client_cert = google.auth.transport._mtls_helper.check_use_client_cert()
471 if not use_client_cert:
472 return
473
474 try:
475 (
476 is_mtls,
477 cert,
478 key,
479 ) = google.auth.transport._mtls_helper.get_client_cert_and_key(
480 client_cert_callback
481 )
482
483 old_adapter = self.adapters.get("https://")
484
485 kwargs = {}
486 if old_adapter is not None:
487 kwargs["max_retries"] = getattr(old_adapter, "max_retries", 0)
488 kwargs["pool_connections"] = getattr(
489 old_adapter, "_pool_connections", requests.adapters.DEFAULT_POOLSIZE
490 )
491 kwargs["pool_maxsize"] = getattr(
492 old_adapter, "_pool_maxsize", requests.adapters.DEFAULT_POOLSIZE
493 )
494 kwargs["pool_block"] = getattr(
495 old_adapter, "_pool_block", requests.adapters.DEFAULT_POOLBLOCK
496 )
497
498 old_auth_adapter = None
499 auth_kwargs = {}
500 if self._auth_request_session is not None:
501 old_auth_adapter = self._auth_request_session.adapters.get("https://")
502
503 if old_auth_adapter is not None:
504 auth_kwargs["max_retries"] = getattr(
505 old_auth_adapter, "max_retries", 0
506 )
507 auth_kwargs["pool_connections"] = getattr(
508 old_auth_adapter,
509 "_pool_connections",
510 requests.adapters.DEFAULT_POOLSIZE,
511 )
512 auth_kwargs["pool_maxsize"] = getattr(
513 old_auth_adapter,
514 "_pool_maxsize",
515 requests.adapters.DEFAULT_POOLSIZE,
516 )
517 auth_kwargs["pool_block"] = getattr(
518 old_auth_adapter,
519 "_pool_block",
520 requests.adapters.DEFAULT_POOLBLOCK,
521 )
522
523 if is_mtls:
524 new_adapter = _MutualTlsAdapter(cert, key, **kwargs)
525 if self._auth_request_session is not None:
526 new_auth_adapter = _MutualTlsAdapter(cert, key, **auth_kwargs)
527 else:
528 new_auth_adapter = None
529 else:
530 new_adapter = requests.adapters.HTTPAdapter(**kwargs)
531 if self._auth_request_session is not None:
532 new_auth_adapter = requests.adapters.HTTPAdapter(**auth_kwargs)
533 else:
534 new_auth_adapter = None
535 except (
536 exceptions.ClientCertError,
537 ImportError,
538 OSError,
539 ValueError,
540 ) as caught_exc:
541 new_exc = exceptions.MutualTLSChannelError(caught_exc)
542 raise new_exc from caught_exc
543
544 self.mount("https://", new_adapter)
545
546 if old_adapter is not None and old_adapter is not new_adapter:
547 old_adapter.close()
548
549 if self._auth_request_session is not None and new_auth_adapter is not None:
550 self._auth_request_session.mount("https://", new_auth_adapter)
551
552 if (
553 old_auth_adapter is not None
554 and old_auth_adapter is not new_auth_adapter
555 ):
556 old_auth_adapter.close()
557
558 self._is_mtls = is_mtls
559 if is_mtls:
560 self._cached_cert = cert
561 else:
562 if hasattr(self, "_cached_cert"):
563 del self._cached_cert
564
565 def request(
566 self,
567 method,
568 url,
569 data=None,
570 headers=None,
571 max_allowed_time=None,
572 timeout=_DEFAULT_TIMEOUT,
573 **kwargs
574 ):
575 """Implementation of Requests' request.
576
577 Args:
578 timeout (Optional[Union[float, Tuple[float, float]]]):
579 The amount of time in seconds to wait for the server response
580 with each individual request. Can also be passed as a tuple
581 ``(connect_timeout, read_timeout)``. See :meth:`requests.Session.request`
582 documentation for details.
583 max_allowed_time (Optional[float]):
584 If the method runs longer than this, a ``Timeout`` exception is
585 automatically raised. Unlike the ``timeout`` parameter, this
586 value applies to the total method execution time, even if
587 multiple requests are made under the hood.
588
589 Mind that it is not guaranteed that the timeout error is raised
590 at ``max_allowed_time``. It might take longer, for example, if
591 an underlying request takes a lot of time, but the request
592 itself does not timeout, e.g. if a large file is being
593 transmitted. The timeout error will be raised after such
594 request completes.
595 Raises:
596 google.auth.exceptions.MutualTLSChannelError: If mutual TLS
597 channel creation fails for any reason.
598 ValueError: If the client certificate is invalid.
599 """
600 # pylint: disable=arguments-differ
601 # Requests has a ton of arguments to request, but only two
602 # (method, url) are required. We pass through all of the other
603 # arguments to super, so no need to exhaustively list them here.
604
605 # Use a kwarg for this instead of an attribute to maintain
606 # thread-safety.
607 _credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0)
608
609 # Make a copy of the headers. They will be modified by the credentials
610 # and we want to pass the original headers if we recurse.
611 request_headers = headers.copy() if headers is not None else {}
612
613 # Do not apply the timeout unconditionally in order to not override the
614 # _auth_request's default timeout.
615 auth_request = (
616 self._auth_request
617 if timeout is None
618 else functools.partial(self._auth_request, timeout=timeout)
619 )
620
621 remaining_time = max_allowed_time
622
623 with TimeoutGuard(remaining_time) as guard:
624 self.credentials.before_request(auth_request, method, url, request_headers)
625 remaining_time = guard.remaining_timeout
626
627 with TimeoutGuard(remaining_time) as guard:
628 _helpers.request_log(_LOGGER, method, url, data, headers)
629 response = super(AuthorizedSession, self).request(
630 method,
631 url,
632 data=data,
633 headers=request_headers,
634 timeout=timeout,
635 **kwargs
636 )
637 remaining_time = guard.remaining_timeout
638
639 # If the response indicated that the credentials needed to be
640 # refreshed, then refresh the credentials and re-attempt the
641 # request.
642 # A stored token may expire between the time it is retrieved and
643 # the time the request is made, so we may need to try twice.
644 if (
645 response.status_code in self._refresh_status_codes
646 and _credential_refresh_attempt < self._max_refresh_attempts
647 ):
648 # Handle unauthorized permission error(401 status code)
649 if response.status_code == http_client.UNAUTHORIZED:
650 if self.is_mtls:
651 (
652 call_cert_bytes,
653 call_key_bytes,
654 cached_fingerprint,
655 current_cert_fingerprint,
656 ) = _mtls_helper.check_parameters_for_unauthorized_response(
657 self._cached_cert
658 )
659 if cached_fingerprint != current_cert_fingerprint:
660 try:
661 _LOGGER.info(
662 "Client certificate has changed, reconfiguring mTLS "
663 "channel."
664 )
665 self.configure_mtls_channel(
666 lambda: (call_cert_bytes, call_key_bytes)
667 )
668 except Exception as e:
669 _LOGGER.error("Failed to reconfigure mTLS channel: %s", e)
670 raise exceptions.MutualTLSChannelError(
671 "Failed to reconfigure mTLS channel"
672 ) from e
673 else:
674 _LOGGER.info(
675 "Skipping reconfiguration of mTLS channel because the client"
676 " certificate has not changed."
677 )
678 _LOGGER.info(
679 "Refreshing credentials due to a %s response. Attempt %s/%s.",
680 response.status_code,
681 _credential_refresh_attempt + 1,
682 self._max_refresh_attempts,
683 )
684
685 # Do not apply the timeout unconditionally in order to not override the
686 # _auth_request's default timeout.
687 auth_request = (
688 self._auth_request
689 if timeout is None
690 else functools.partial(self._auth_request, timeout=timeout)
691 )
692
693 with TimeoutGuard(remaining_time) as guard:
694 self.credentials.refresh(auth_request)
695 remaining_time = guard.remaining_timeout
696
697 # Recurse. Pass in the original headers, not our modified set, but
698 # do pass the adjusted max allowed time (i.e. the remaining total time).
699 return self.request(
700 method,
701 url,
702 data=data,
703 headers=headers,
704 max_allowed_time=remaining_time,
705 timeout=timeout,
706 _credential_refresh_attempt=_credential_refresh_attempt + 1,
707 **kwargs
708 )
709
710 return response
711
712 @property
713 def is_mtls(self):
714 """Indicates if the created SSL channel is mutual TLS."""
715 return self._is_mtls
716
717 def close(self):
718 if self._auth_request_session is not None:
719 self._auth_request_session.close()
720 super(AuthorizedSession, self).close()