1616"""Interfaces for credentials."""
1717
1818import abc
19+ from enum import Enum
1920import os
2021
2122from google .auth import _helpers , environment_vars
2223from google .auth import exceptions
2324from google .auth import metrics
25+ from google .auth ._refresh_worker import RefreshThreadManager
2426
2527
2628class Credentials (metaclass = abc .ABCMeta ):
@@ -59,17 +61,22 @@ def __init__(self):
5961 """Optional[str]: The universe domain value, default is googleapis.com
6062 """
6163
64+ self ._use_non_blocking_refresh = False
65+ self ._refresh_worker = RefreshThreadManager ()
66+
6267 @property
6368 def expired (self ):
6469 """Checks if the credentials are expired.
6570
6671 Note that credentials can be invalid but not expired because
6772 Credentials with :attr:`expiry` set to None is considered to never
6873 expire.
74+
75+ .. deprecated:: v2.24.0
76+ Prefer checking :attr:`token_state` instead.
6977 """
7078 if not self .expiry :
7179 return False
72-
7380 # Remove some threshold from expiry to err on the side of reporting
7481 # expiration early so that we avoid the 401-refresh-retry loop.
7582 skewed_expiry = self .expiry - _helpers .REFRESH_THRESHOLD
@@ -81,9 +88,34 @@ def valid(self):
8188
8289 This is True if the credentials have a :attr:`token` and the token
8390 is not :attr:`expired`.
91+
92+ .. deprecated:: v2.24.0
93+ Prefer checking :attr:`token_state` instead.
8494 """
8595 return self .token is not None and not self .expired
8696
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+
87119 @property
88120 def quota_project_id (self ):
89121 """Project to use for quota and billing purposes."""
@@ -154,6 +186,25 @@ def apply(self, headers, token=None):
154186 if self .quota_project_id :
155187 headers ["x-goog-user-project" ] = self .quota_project_id
156188
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+
157208 def before_request (self , request , method , url , headers ):
158209 """Performs credential-specific before request logic.
159210
@@ -171,11 +222,17 @@ def before_request(self, request, method, url, headers):
171222 # pylint: disable=unused-argument
172223 # (Subclasses may use these arguments to ascertain information about
173224 # 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+
176230 metrics .add_metric_header (headers , self ._metric_header_for_usage ())
177231 self .apply (headers )
178232
233+ def with_non_blocking_refresh (self ):
234+ self ._use_non_blocking_refresh = True
235+
179236
180237class CredentialsWithQuotaProject (Credentials ):
181238 """Abstract base for credentials supporting ``with_quota_project`` factory"""
@@ -439,3 +496,16 @@ def signer(self):
439496 # pylint: disable=missing-raises-doc
440497 # (pylint doesn't recognize that this is abstract)
441498 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
0 commit comments