google-auth 2.25.2__tar.gz → 2.26.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. {google-auth-2.25.2/google_auth.egg-info → google-auth-2.26.0}/PKG-INFO +1 -1
  2. google-auth-2.26.0/google/auth/_refresh_worker.py +98 -0
  3. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/credentials.py +92 -5
  4. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/external_account.py +1 -9
  5. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/external_account_authorized_user.py +12 -0
  6. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/impersonated_credentials.py +4 -1
  7. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/version.py +1 -1
  8. {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/credentials.py +8 -9
  9. {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/service_account.py +1 -9
  10. {google-auth-2.25.2 → google-auth-2.26.0/google_auth.egg-info}/PKG-INFO +1 -1
  11. {google-auth-2.25.2 → google-auth-2.26.0}/google_auth.egg-info/SOURCES.txt +2 -0
  12. {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_credentials.py +10 -2
  13. google-auth-2.26.0/tests/test__refresh_worker.py +147 -0
  14. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_credentials.py +118 -0
  15. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_downscoped.py +18 -0
  16. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_external_account.py +10 -0
  17. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_external_account_authorized_user.py +31 -0
  18. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_impersonated_credentials.py +1 -1
  19. {google-auth-2.25.2 → google-auth-2.26.0}/LICENSE +0 -0
  20. {google-auth-2.25.2 → google-auth-2.26.0}/MANIFEST.in +0 -0
  21. {google-auth-2.25.2 → google-auth-2.26.0}/README.rst +0 -0
  22. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/__init__.py +0 -0
  23. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_cloud_sdk.py +0 -0
  24. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_credentials_async.py +0 -0
  25. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_default.py +0 -0
  26. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_default_async.py +0 -0
  27. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_exponential_backoff.py +0 -0
  28. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_helpers.py +0 -0
  29. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_jwt_async.py +0 -0
  30. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_oauth2client.py +0 -0
  31. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/_service_account_info.py +0 -0
  32. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/api_key.py +0 -0
  33. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/app_engine.py +0 -0
  34. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/aws.py +0 -0
  35. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/compute_engine/__init__.py +0 -0
  36. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/compute_engine/_metadata.py +0 -0
  37. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/compute_engine/credentials.py +0 -0
  38. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/crypt/__init__.py +0 -0
  39. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/crypt/_cryptography_rsa.py +0 -0
  40. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/crypt/_helpers.py +0 -0
  41. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/crypt/_python_rsa.py +0 -0
  42. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/crypt/base.py +0 -0
  43. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/crypt/es256.py +0 -0
  44. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/crypt/rsa.py +0 -0
  45. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/downscoped.py +0 -0
  46. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/environment_vars.py +0 -0
  47. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/exceptions.py +0 -0
  48. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/iam.py +0 -0
  49. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/identity_pool.py +0 -0
  50. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/jwt.py +0 -0
  51. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/metrics.py +0 -0
  52. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/pluggable.py +0 -0
  53. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/__init__.py +0 -0
  54. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/_aiohttp_requests.py +0 -0
  55. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/_custom_tls_signer.py +0 -0
  56. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/_http_client.py +0 -0
  57. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/_mtls_helper.py +0 -0
  58. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/grpc.py +0 -0
  59. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/mtls.py +0 -0
  60. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/requests.py +0 -0
  61. {google-auth-2.25.2 → google-auth-2.26.0}/google/auth/transport/urllib3.py +0 -0
  62. {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/__init__.py +0 -0
  63. {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/_client.py +0 -0
  64. {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/_client_async.py +0 -0
  65. {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/_credentials_async.py +0 -0
  66. {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/_id_token_async.py +0 -0
  67. {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/_reauth_async.py +0 -0
  68. {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/_service_account_async.py +0 -0
  69. {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/challenges.py +0 -0
  70. {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/gdch_credentials.py +0 -0
  71. {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/id_token.py +0 -0
  72. {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/reauth.py +0 -0
  73. {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/sts.py +0 -0
  74. {google-auth-2.25.2 → google-auth-2.26.0}/google/oauth2/utils.py +0 -0
  75. {google-auth-2.25.2 → google-auth-2.26.0}/google_auth.egg-info/dependency_links.txt +0 -0
  76. {google-auth-2.25.2 → google-auth-2.26.0}/google_auth.egg-info/requires.txt +0 -0
  77. {google-auth-2.25.2 → google-auth-2.26.0}/google_auth.egg-info/top_level.txt +0 -0
  78. {google-auth-2.25.2 → google-auth-2.26.0}/setup.cfg +0 -0
  79. {google-auth-2.25.2 → google-auth-2.26.0}/setup.py +0 -0
  80. {google-auth-2.25.2 → google-auth-2.26.0}/tests/__init__.py +0 -0
  81. {google-auth-2.25.2 → google-auth-2.26.0}/tests/compute_engine/__init__.py +0 -0
  82. {google-auth-2.25.2 → google-auth-2.26.0}/tests/compute_engine/data/smbios_product_name +0 -0
  83. {google-auth-2.25.2 → google-auth-2.26.0}/tests/compute_engine/data/smbios_product_name_non_google +0 -0
  84. {google-auth-2.25.2 → google-auth-2.26.0}/tests/compute_engine/test__metadata.py +0 -0
  85. {google-auth-2.25.2 → google-auth-2.26.0}/tests/compute_engine/test_credentials.py +0 -0
  86. {google-auth-2.25.2 → google-auth-2.26.0}/tests/conftest.py +0 -0
  87. {google-auth-2.25.2 → google-auth-2.26.0}/tests/crypt/__init__.py +0 -0
  88. {google-auth-2.25.2 → google-auth-2.26.0}/tests/crypt/test__cryptography_rsa.py +0 -0
  89. {google-auth-2.25.2 → google-auth-2.26.0}/tests/crypt/test__python_rsa.py +0 -0
  90. {google-auth-2.25.2 → google-auth-2.26.0}/tests/crypt/test_crypt.py +0 -0
  91. {google-auth-2.25.2 → google-auth-2.26.0}/tests/crypt/test_es256.py +0 -0
  92. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/authorized_user.json +0 -0
  93. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/authorized_user_cloud_sdk.json +0 -0
  94. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/authorized_user_cloud_sdk_with_quota_project_id.json +0 -0
  95. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/authorized_user_with_rapt_token.json +0 -0
  96. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/client_secrets.json +0 -0
  97. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/context_aware_metadata.json +0 -0
  98. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/enterprise_cert_invalid.json +0 -0
  99. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/enterprise_cert_valid.json +0 -0
  100. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/enterprise_cert_valid_provider.json +0 -0
  101. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/es256_privatekey.pem +0 -0
  102. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/es256_public_cert.pem +0 -0
  103. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/es256_publickey.pem +0 -0
  104. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/es256_service_account.json +0 -0
  105. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/external_account_authorized_user.json +0 -0
  106. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/external_subject_token.json +0 -0
  107. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/external_subject_token.txt +0 -0
  108. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/gdch_service_account.json +0 -0
  109. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/impersonated_service_account_authorized_user_source.json +0 -0
  110. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/impersonated_service_account_service_account_source.json +0 -0
  111. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/impersonated_service_account_with_quota_project.json +0 -0
  112. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/old_oauth_credentials_py3.pickle +0 -0
  113. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/other_cert.pem +0 -0
  114. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/pem_from_pkcs12.pem +0 -0
  115. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/privatekey.p12 +0 -0
  116. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/privatekey.pem +0 -0
  117. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/privatekey.pub +0 -0
  118. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/public_cert.pem +0 -0
  119. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/service_account.json +0 -0
  120. {google-auth-2.25.2 → google-auth-2.26.0}/tests/data/service_account_non_gdu.json +0 -0
  121. {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/__init__.py +0 -0
  122. {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test__client.py +0 -0
  123. {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_challenges.py +0 -0
  124. {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_gdch_credentials.py +0 -0
  125. {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_id_token.py +0 -0
  126. {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_reauth.py +0 -0
  127. {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_service_account.py +0 -0
  128. {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_sts.py +0 -0
  129. {google-auth-2.25.2 → google-auth-2.26.0}/tests/oauth2/test_utils.py +0 -0
  130. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test__cloud_sdk.py +0 -0
  131. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test__default.py +0 -0
  132. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test__exponential_backoff.py +0 -0
  133. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test__helpers.py +0 -0
  134. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test__oauth2client.py +0 -0
  135. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test__service_account_info.py +0 -0
  136. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_api_key.py +0 -0
  137. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_app_engine.py +0 -0
  138. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_aws.py +0 -0
  139. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_exceptions.py +0 -0
  140. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_iam.py +0 -0
  141. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_identity_pool.py +0 -0
  142. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_jwt.py +0 -0
  143. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_metrics.py +0 -0
  144. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_packaging.py +0 -0
  145. {google-auth-2.25.2 → google-auth-2.26.0}/tests/test_pluggable.py +0 -0
  146. {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/__init__.py +0 -0
  147. {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/compliance.py +0 -0
  148. {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/test__custom_tls_signer.py +0 -0
  149. {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/test__http_client.py +0 -0
  150. {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/test__mtls_helper.py +0 -0
  151. {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/test_grpc.py +0 -0
  152. {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/test_mtls.py +0 -0
  153. {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/test_requests.py +0 -0
  154. {google-auth-2.25.2 → google-auth-2.26.0}/tests/transport/test_urllib3.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: google-auth
3
- Version: 2.25.2
3
+ Version: 2.26.0
4
4
  Summary: Google Authentication Library
5
5
  Home-page: https://github.com/googleapis/google-auth-library-python
6
6
  Author: Google Cloud Platform
@@ -0,0 +1,98 @@
1
+ # Copyright 2023 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
+ import copy
16
+ import logging
17
+ import threading
18
+
19
+ import google.auth.exceptions as e
20
+
21
+ _LOGGER = logging.getLogger(__name__)
22
+
23
+
24
+ class RefreshThreadManager:
25
+ """
26
+ Organizes exactly one background job that refresh a token.
27
+ """
28
+
29
+ def __init__(self):
30
+ """Initializes the manager."""
31
+
32
+ self._worker = None
33
+ self._lock = threading.Lock() # protects access to worker threads.
34
+
35
+ def start_refresh(self, cred, request):
36
+ """Starts a refresh thread for the given credentials.
37
+ The credentials are refreshed using the request parameter.
38
+ request and cred MUST not be None
39
+
40
+ Returns True if a background refresh was kicked off. False otherwise.
41
+
42
+ Args:
43
+ cred: A credentials object.
44
+ request: A request object.
45
+ Returns:
46
+ bool
47
+ """
48
+ if cred is None or request is None:
49
+ raise e.InvalidValue(
50
+ "Unable to start refresh. cred and request must be valid and instantiated objects."
51
+ )
52
+
53
+ with self._lock:
54
+ if self._worker is not None and self._worker._error_info is not None:
55
+ return False
56
+
57
+ if self._worker is None or not self._worker.is_alive(): # pragma: NO COVER
58
+ self._worker = RefreshThread(cred=cred, request=copy.deepcopy(request))
59
+ self._worker.start()
60
+ return True
61
+
62
+ def clear_error(self):
63
+ """
64
+ Removes any errors that were stored from previous background refreshes.
65
+ """
66
+ with self._lock:
67
+ if self._worker:
68
+ self._worker._error_info = None
69
+
70
+
71
+ class RefreshThread(threading.Thread):
72
+ """
73
+ Thread that refreshes credentials.
74
+ """
75
+
76
+ def __init__(self, cred, request, **kwargs):
77
+ """Initializes the thread.
78
+
79
+ Args:
80
+ cred: A Credential object to refresh.
81
+ request: A Request object used to perform a credential refresh.
82
+ **kwargs: Additional keyword arguments.
83
+ """
84
+
85
+ super().__init__(**kwargs)
86
+ self._cred = cred
87
+ self._request = request
88
+ self._error_info = None
89
+
90
+ def run(self):
91
+ """
92
+ Perform the credential refresh.
93
+ """
94
+ try:
95
+ self._cred.refresh(self._request)
96
+ except Exception as err: # pragma: NO COVER
97
+ _LOGGER.error(f"Background refresh failed due to: {err}")
98
+ self._error_info = err
@@ -16,11 +16,13 @@
16
16
  """Interfaces for credentials."""
17
17
 
18
18
  import abc
19
+ from enum import Enum
19
20
  import os
20
21
 
21
22
  from google.auth import _helpers, environment_vars
22
23
  from google.auth import exceptions
23
24
  from google.auth import metrics
25
+ from google.auth._refresh_worker import RefreshThreadManager
24
26
 
25
27
 
26
28
  class Credentials(metaclass=abc.ABCMeta):
@@ -59,6 +61,9 @@ class Credentials(metaclass=abc.ABCMeta):
59
61
  """Optional[str]: The universe domain value, default is googleapis.com
60
62
  """
61
63
 
64
+ self._use_non_blocking_refresh = False
65
+ self._refresh_worker = RefreshThreadManager()
66
+
62
67
  @property
63
68
  def expired(self):
64
69
  """Checks if the credentials are expired.
@@ -66,10 +71,12 @@ class Credentials(metaclass=abc.ABCMeta):
66
71
  Note that credentials can be invalid but not expired because
67
72
  Credentials with :attr:`expiry` set to None is considered to never
68
73
  expire.
74
+
75
+ .. deprecated:: v2.24.0
76
+ Prefer checking :attr:`token_state` instead.
69
77
  """
70
78
  if not self.expiry:
71
79
  return False
72
-
73
80
  # Remove some threshold from expiry to err on the side of reporting
74
81
  # expiration early so that we avoid the 401-refresh-retry loop.
75
82
  skewed_expiry = self.expiry - _helpers.REFRESH_THRESHOLD
@@ -81,9 +88,34 @@ class Credentials(metaclass=abc.ABCMeta):
81
88
 
82
89
  This is True if the credentials have a :attr:`token` and the token
83
90
  is not :attr:`expired`.
91
+
92
+ .. deprecated:: v2.24.0
93
+ Prefer checking :attr:`token_state` instead.
84
94
  """
85
95
  return self.token is not None and not self.expired
86
96
 
97
+ @property
98
+ def token_state(self):
99
+ """
100
+ See `:obj:`TokenState`
101
+ """
102
+ if self.token is None:
103
+ return TokenState.INVALID
104
+
105
+ # Credentials that can't expire are always treated as fresh.
106
+ if self.expiry is None:
107
+ return TokenState.FRESH
108
+
109
+ expired = _helpers.utcnow() >= self.expiry
110
+ if expired:
111
+ return TokenState.INVALID
112
+
113
+ is_stale = _helpers.utcnow() >= (self.expiry - _helpers.REFRESH_THRESHOLD)
114
+ if is_stale:
115
+ return TokenState.STALE
116
+
117
+ return TokenState.FRESH
118
+
87
119
  @property
88
120
  def quota_project_id(self):
89
121
  """Project to use for quota and billing purposes."""
@@ -154,6 +186,25 @@ class Credentials(metaclass=abc.ABCMeta):
154
186
  if self.quota_project_id:
155
187
  headers["x-goog-user-project"] = self.quota_project_id
156
188
 
189
+ def _blocking_refresh(self, request):
190
+ if not self.valid:
191
+ self.refresh(request)
192
+
193
+ def _non_blocking_refresh(self, request):
194
+ use_blocking_refresh_fallback = False
195
+
196
+ if self.token_state == TokenState.STALE:
197
+ use_blocking_refresh_fallback = not self._refresh_worker.start_refresh(
198
+ self, request
199
+ )
200
+
201
+ if self.token_state == TokenState.INVALID or use_blocking_refresh_fallback:
202
+ self.refresh(request)
203
+ # If the blocking refresh succeeds then we can clear the error info
204
+ # on the background refresh worker, and perform refreshes in a
205
+ # background thread.
206
+ self._refresh_worker.clear_error()
207
+
157
208
  def before_request(self, request, method, url, headers):
158
209
  """Performs credential-specific before request logic.
159
210
 
@@ -171,11 +222,17 @@ class Credentials(metaclass=abc.ABCMeta):
171
222
  # pylint: disable=unused-argument
172
223
  # (Subclasses may use these arguments to ascertain information about
173
224
  # the http request.)
174
- if not self.valid:
175
- self.refresh(request)
225
+ if self._use_non_blocking_refresh:
226
+ self._non_blocking_refresh(request)
227
+ else:
228
+ self._blocking_refresh(request)
229
+
176
230
  metrics.add_metric_header(headers, self._metric_header_for_usage())
177
231
  self.apply(headers)
178
232
 
233
+ def with_non_blocking_refresh(self):
234
+ self._use_non_blocking_refresh = True
235
+
179
236
 
180
237
  class CredentialsWithQuotaProject(Credentials):
181
238
  """Abstract base for credentials supporting ``with_quota_project`` factory"""
@@ -188,7 +245,7 @@ class CredentialsWithQuotaProject(Credentials):
188
245
  billing purposes
189
246
 
190
247
  Returns:
191
- google.oauth2.credentials.Credentials: A new credentials instance.
248
+ google.auth.credentials.Credentials: A new credentials instance.
192
249
  """
193
250
  raise NotImplementedError("This credential does not support quota project.")
194
251
 
@@ -209,11 +266,28 @@ class CredentialsWithTokenUri(Credentials):
209
266
  token_uri (str): The uri to use for fetching/exchanging tokens
210
267
 
211
268
  Returns:
212
- google.oauth2.credentials.Credentials: A new credentials instance.
269
+ google.auth.credentials.Credentials: A new credentials instance.
213
270
  """
214
271
  raise NotImplementedError("This credential does not use token uri.")
215
272
 
216
273
 
274
+ class CredentialsWithUniverseDomain(Credentials):
275
+ """Abstract base for credentials supporting ``with_universe_domain`` factory"""
276
+
277
+ def with_universe_domain(self, universe_domain):
278
+ """Returns a copy of these credentials with a modified universe domain.
279
+
280
+ Args:
281
+ universe_domain (str): The universe domain to use
282
+
283
+ Returns:
284
+ google.auth.credentials.Credentials: A new credentials instance.
285
+ """
286
+ raise NotImplementedError(
287
+ "This credential does not support with_universe_domain."
288
+ )
289
+
290
+
217
291
  class AnonymousCredentials(Credentials):
218
292
  """Credentials that do not provide any authentication information.
219
293
 
@@ -422,3 +496,16 @@ class Signing(metaclass=abc.ABCMeta):
422
496
  # pylint: disable=missing-raises-doc
423
497
  # (pylint doesn't recognize that this is abstract)
424
498
  raise NotImplementedError("Signer must be implemented.")
499
+
500
+
501
+ class TokenState(Enum):
502
+ """
503
+ Tracks the state of a token.
504
+ FRESH: The token is valid. It is not expired or close to expired, or the token has no expiry.
505
+ STALE: The token is close to expired, and should be refreshed. The token can be used normally.
506
+ INVALID: The token is expired or invalid. The token cannot be used for a normal operation.
507
+ """
508
+
509
+ FRESH = 1
510
+ STALE = 2
511
+ INVALID = 3
@@ -415,16 +415,8 @@ class Credentials(
415
415
  new_cred._metrics_options = self._metrics_options
416
416
  return new_cred
417
417
 
418
+ @_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
418
419
  def with_universe_domain(self, universe_domain):
419
- """Create a copy of these credentials with the given universe domain.
420
-
421
- Args:
422
- universe_domain (str): The universe domain value.
423
-
424
- Returns:
425
- google.auth.external_account.Credentials: A new credentials
426
- instance.
427
- """
428
420
  kwargs = self._constructor_args()
429
421
  kwargs.update(universe_domain=universe_domain)
430
422
  new_cred = self.__class__(**kwargs)
@@ -43,6 +43,7 @@ from google.auth import exceptions
43
43
  from google.oauth2 import sts
44
44
  from google.oauth2 import utils
45
45
 
46
+ _DEFAULT_UNIVERSE_DOMAIN = "googleapis.com"
46
47
  _EXTERNAL_ACCOUNT_AUTHORIZED_USER_JSON_TYPE = "external_account_authorized_user"
47
48
 
48
49
 
@@ -75,6 +76,7 @@ class Credentials(
75
76
  revoke_url=None,
76
77
  scopes=None,
77
78
  quota_project_id=None,
79
+ universe_domain=_DEFAULT_UNIVERSE_DOMAIN,
78
80
  ):
79
81
  """Instantiates a external account authorized user credentials object.
80
82
 
@@ -98,6 +100,8 @@ class Credentials(
98
100
  quota_project_id (str): The optional project ID used for quota and billing.
99
101
  This project may be different from the project used to
100
102
  create the credentials.
103
+ universe_domain (Optional[str]): The universe domain. The default value
104
+ is googleapis.com.
101
105
 
102
106
  Returns:
103
107
  google.auth.external_account_authorized_user.Credentials: The
@@ -116,6 +120,7 @@ class Credentials(
116
120
  self._revoke_url = revoke_url
117
121
  self._quota_project_id = quota_project_id
118
122
  self._scopes = scopes
123
+ self._universe_domain = universe_domain or _DEFAULT_UNIVERSE_DOMAIN
119
124
 
120
125
  if not self.valid and not self.can_refresh:
121
126
  raise exceptions.InvalidOperation(
@@ -162,6 +167,7 @@ class Credentials(
162
167
  "revoke_url": self._revoke_url,
163
168
  "scopes": self._scopes,
164
169
  "quota_project_id": self._quota_project_id,
170
+ "universe_domain": self._universe_domain,
165
171
  }
166
172
 
167
173
  @property
@@ -297,6 +303,12 @@ class Credentials(
297
303
  kwargs.update(token_url=token_uri)
298
304
  return self.__class__(**kwargs)
299
305
 
306
+ @_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
307
+ def with_universe_domain(self, universe_domain):
308
+ kwargs = self.constructor_args()
309
+ kwargs.update(universe_domain=universe_domain)
310
+ return self.__class__(**kwargs)
311
+
300
312
  @classmethod
301
313
  def from_info(cls, info, **kwargs):
302
314
  """Creates a Credentials instance from parsed external account info.
@@ -259,7 +259,10 @@ class Credentials(
259
259
  """
260
260
 
261
261
  # Refresh our source credentials if it is not valid.
262
- if not self._source_credentials.valid:
262
+ if (
263
+ self._source_credentials.token_state == credentials.TokenState.STALE
264
+ or self._source_credentials.token_state == credentials.TokenState.INVALID
265
+ ):
263
266
  self._source_credentials.refresh(request)
264
267
 
265
268
  body = {
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "2.25.2"
15
+ __version__ = "2.26.0"
@@ -160,7 +160,11 @@ class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaPr
160
160
  # unpickling certain callables (lambda, functools.partial instances)
161
161
  # because they need to be importable.
162
162
  # Instead, the refresh_handler setter should be used to repopulate this.
163
- del state_dict["_refresh_handler"]
163
+ if "_refresh_handler" in state_dict:
164
+ del state_dict["_refresh_handler"]
165
+
166
+ if "_refresh_worker" in state_dict:
167
+ del state_dict["_refresh_worker"]
164
168
  return state_dict
165
169
 
166
170
  def __setstate__(self, d):
@@ -183,6 +187,8 @@ class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaPr
183
187
  self._universe_domain = d.get("_universe_domain") or _DEFAULT_UNIVERSE_DOMAIN
184
188
  # The refresh_handler setter should be used to repopulate this.
185
189
  self._refresh_handler = None
190
+ self._refresh_worker = None
191
+ self._use_non_blocking_refresh = d.get("_use_non_blocking_refresh", False)
186
192
 
187
193
  @property
188
194
  def refresh_token(self):
@@ -302,15 +308,8 @@ class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaPr
302
308
  universe_domain=self._universe_domain,
303
309
  )
304
310
 
311
+ @_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
305
312
  def with_universe_domain(self, universe_domain):
306
- """Create a copy of the credential with the given universe domain.
307
-
308
- Args:
309
- universe_domain (str): The universe domain value.
310
-
311
- Returns:
312
- google.oauth2.credentials.Credentials: A new credentials instance.
313
- """
314
313
 
315
314
  return self.__class__(
316
315
  self.token,
@@ -325,16 +325,8 @@ class Credentials(
325
325
  cred._always_use_jwt_access = always_use_jwt_access
326
326
  return cred
327
327
 
328
+ @_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
328
329
  def with_universe_domain(self, universe_domain):
329
- """Create a copy of these credentials with the given universe domain.
330
-
331
- Args:
332
- universe_domain (str): The universe domain value.
333
-
334
- Returns:
335
- google.auth.service_account.Credentials: A new credentials
336
- instance.
337
- """
338
330
  cred = self._make_copy()
339
331
  cred._universe_domain = universe_domain
340
332
  if universe_domain != _DEFAULT_UNIVERSE_DOMAIN:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: google-auth
3
- Version: 2.25.2
3
+ Version: 2.26.0
4
4
  Summary: Google Authentication Library
5
5
  Home-page: https://github.com/googleapis/google-auth-library-python
6
6
  Author: Google Cloud Platform
@@ -12,6 +12,7 @@ google/auth/_exponential_backoff.py
12
12
  google/auth/_helpers.py
13
13
  google/auth/_jwt_async.py
14
14
  google/auth/_oauth2client.py
15
+ google/auth/_refresh_worker.py
15
16
  google/auth/_service_account_info.py
16
17
  google/auth/api_key.py
17
18
  google/auth/app_engine.py
@@ -75,6 +76,7 @@ tests/test__default.py
75
76
  tests/test__exponential_backoff.py
76
77
  tests/test__helpers.py
77
78
  tests/test__oauth2client.py
79
+ tests/test__refresh_worker.py
78
80
  tests/test__service_account_info.py
79
81
  tests/test_api_key.py
80
82
  tests/test_app_engine.py
@@ -24,6 +24,7 @@ import pytest # type: ignore
24
24
  from google.auth import _helpers
25
25
  from google.auth import exceptions
26
26
  from google.auth import transport
27
+ from google.auth.credentials import TokenState
27
28
  from google.oauth2 import credentials
28
29
 
29
30
 
@@ -61,6 +62,7 @@ class TestCredentials(object):
61
62
  assert not credentials.expired
62
63
  # Scopes aren't required for these credentials
63
64
  assert not credentials.requires_scopes
65
+ assert credentials.token_state == TokenState.INVALID
64
66
  # Test properties
65
67
  assert credentials.refresh_token == self.REFRESH_TOKEN
66
68
  assert credentials.token_uri == self.TOKEN_URI
@@ -911,7 +913,11 @@ class TestCredentials(object):
911
913
  assert list(creds.__dict__).sort() == list(unpickled.__dict__).sort()
912
914
 
913
915
  for attr in list(creds.__dict__):
914
- assert getattr(creds, attr) == getattr(unpickled, attr)
916
+ # Worker should always be None
917
+ if attr == "_refresh_worker":
918
+ assert getattr(unpickled, attr) is None
919
+ else:
920
+ assert getattr(creds, attr) == getattr(unpickled, attr)
915
921
 
916
922
  def test_pickle_and_unpickle_universe_domain(self):
917
923
  # old version of auth lib doesn't have _universe_domain, so the pickled
@@ -945,7 +951,7 @@ class TestCredentials(object):
945
951
  for attr in list(creds.__dict__):
946
952
  # For the _refresh_handler property, the unpickled creds should be
947
953
  # set to None.
948
- if attr == "_refresh_handler":
954
+ if attr == "_refresh_handler" or attr == "_refresh_worker":
949
955
  assert getattr(unpickled, attr) is None
950
956
  else:
951
957
  assert getattr(creds, attr) == getattr(unpickled, attr)
@@ -957,6 +963,8 @@ class TestCredentials(object):
957
963
  # this mimics a pickle created with a previous class definition with
958
964
  # fewer attributes
959
965
  del creds.__dict__["_quota_project_id"]
966
+ del creds.__dict__["_refresh_handler"]
967
+ del creds.__dict__["_refresh_worker"]
960
968
 
961
969
  unpickled = pickle.loads(pickle.dumps(creds))
962
970
 
@@ -0,0 +1,147 @@
1
+ # Copyright 2023 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
+ import random
16
+ import threading
17
+ import time
18
+
19
+ import mock
20
+ import pytest # type: ignore
21
+
22
+ from google.auth import _refresh_worker, credentials, exceptions
23
+
24
+ MAIN_THREAD_SLEEP_MS = 100 / 1000
25
+
26
+
27
+ class MockCredentialsImpl(credentials.Credentials):
28
+ def __init__(self, sleep_seconds=None):
29
+ self.refresh_count = 0
30
+ self.token = None
31
+ self.sleep_seconds = sleep_seconds if sleep_seconds else None
32
+
33
+ def refresh(self, request):
34
+ if self.sleep_seconds:
35
+ time.sleep(self.sleep_seconds)
36
+ self.token = request
37
+ self.refresh_count += 1
38
+
39
+
40
+ @pytest.fixture
41
+ def test_thread_count():
42
+ return 25
43
+
44
+
45
+ def _cred_spinlock(cred):
46
+ while cred.token is None: # pragma: NO COVER
47
+ time.sleep(MAIN_THREAD_SLEEP_MS)
48
+
49
+
50
+ def test_invalid_start_refresh():
51
+ w = _refresh_worker.RefreshThreadManager()
52
+ with pytest.raises(exceptions.InvalidValue):
53
+ w.start_refresh(None, None)
54
+
55
+
56
+ def test_start_refresh():
57
+ w = _refresh_worker.RefreshThreadManager()
58
+ cred = MockCredentialsImpl()
59
+ request = mock.MagicMock()
60
+ assert w.start_refresh(cred, request)
61
+
62
+ assert w._worker is not None
63
+
64
+ _cred_spinlock(cred)
65
+
66
+ assert cred.token == request
67
+ assert cred.refresh_count == 1
68
+
69
+
70
+ def test_nonblocking_start_refresh():
71
+ w = _refresh_worker.RefreshThreadManager()
72
+ cred = MockCredentialsImpl(sleep_seconds=1)
73
+ request = mock.MagicMock()
74
+ assert w.start_refresh(cred, request)
75
+
76
+ assert w._worker is not None
77
+ assert not cred.token
78
+ assert cred.refresh_count == 0
79
+
80
+
81
+ def test_multiple_refreshes_multiple_workers(test_thread_count):
82
+ w = _refresh_worker.RefreshThreadManager()
83
+ cred = MockCredentialsImpl()
84
+ request = mock.MagicMock()
85
+
86
+ def _thread_refresh():
87
+ time.sleep(random.randrange(0, 5))
88
+ assert w.start_refresh(cred, request)
89
+
90
+ threads = [
91
+ threading.Thread(target=_thread_refresh) for _ in range(test_thread_count)
92
+ ]
93
+ for t in threads:
94
+ t.start()
95
+
96
+ _cred_spinlock(cred)
97
+
98
+ assert cred.token == request
99
+ # There is a chance only one thread has enough time to perform a refresh.
100
+ # Generally multiple threads will have time to perform a refresh
101
+ assert cred.refresh_count > 0
102
+
103
+
104
+ def test_refresh_error():
105
+ w = _refresh_worker.RefreshThreadManager()
106
+ cred = mock.MagicMock()
107
+ request = mock.MagicMock()
108
+
109
+ cred.refresh.side_effect = exceptions.RefreshError("Failed to refresh")
110
+
111
+ assert w.start_refresh(cred, request)
112
+
113
+ while w._worker._error_info is None: # pragma: NO COVER
114
+ time.sleep(MAIN_THREAD_SLEEP_MS)
115
+
116
+ assert w._worker is not None
117
+ assert isinstance(w._worker._error_info, exceptions.RefreshError)
118
+
119
+
120
+ def test_refresh_error_call_refresh_again():
121
+ w = _refresh_worker.RefreshThreadManager()
122
+ cred = mock.MagicMock()
123
+ request = mock.MagicMock()
124
+
125
+ cred.refresh.side_effect = exceptions.RefreshError("Failed to refresh")
126
+
127
+ assert w.start_refresh(cred, request)
128
+
129
+ while w._worker._error_info is None: # pragma: NO COVER
130
+ time.sleep(MAIN_THREAD_SLEEP_MS)
131
+
132
+ assert not w.start_refresh(cred, request)
133
+
134
+
135
+ def test_refresh_dead_worker():
136
+ cred = MockCredentialsImpl()
137
+ request = mock.MagicMock()
138
+
139
+ w = _refresh_worker.RefreshThreadManager()
140
+ w._worker = None
141
+
142
+ w.start_refresh(cred, request)
143
+
144
+ _cred_spinlock(cred)
145
+
146
+ assert cred.token == request
147
+ assert cred.refresh_count == 1