diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 9ee60f7e4..c07f148f0 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: image: gcr.io/repo-automation-bots/owlbot-python:latest - digest: sha256:aea14a583128771ae8aefa364e1652f3c56070168ef31beb203534222d842b8b + digest: sha256:0ffe3bdd6c7159692df5f7744da74e5ef19966288a6bf76023e8e04e0c424d7d diff --git a/CHANGELOG.md b/CHANGELOG.md index 68b388db9..056aeac6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ [1]: https://pypi.org/project/google-auth/#history +## [2.1.0](https://www.github.com/googleapis/google-auth-library-python/compare/v2.0.2...v2.1.0) (2021-09-10) + + +### Features + +* Improve handling of clock skew ([#858](https://www.github.com/googleapis/google-auth-library-python/issues/858)) ([45c4491](https://www.github.com/googleapis/google-auth-library-python/commit/45c4491fb971c9edf590b27b9e271b7a23a1bba6)) + + +### Bug Fixes + +* add SAML challenge to reauth ([#819](https://www.github.com/googleapis/google-auth-library-python/issues/819)) ([13aed5f](https://www.github.com/googleapis/google-auth-library-python/commit/13aed5ffe3ba435004ab48202462452f04d7cb29)) +* disable warning if quota project id provided to auth.default() ([#856](https://www.github.com/googleapis/google-auth-library-python/issues/856)) ([11ebaeb](https://www.github.com/googleapis/google-auth-library-python/commit/11ebaeb9d7c0862916154cfb810238574507629a)) +* rename CLOCK_SKEW and separate client/server user case ([#863](https://www.github.com/googleapis/google-auth-library-python/issues/863)) ([738611b](https://www.github.com/googleapis/google-auth-library-python/commit/738611bd2914f0fd5fa8b49b65f56ef321829c85)) + ### [2.0.2](https://www.github.com/googleapis/google-auth-library-python/compare/v2.0.1...v2.0.2) (2021-08-25) diff --git a/google/auth/_default.py b/google/auth/_default.py index 7da77a28f..d4ccbc6ec 100644 --- a/google/auth/_default.py +++ b/google/auth/_default.py @@ -172,7 +172,7 @@ def load_credentials_from_file( ) -def _get_gcloud_sdk_credentials(): +def _get_gcloud_sdk_credentials(quota_project_id=None): """Gets the credentials and project ID from the Cloud SDK.""" from google.auth import _cloud_sdk @@ -185,7 +185,9 @@ def _get_gcloud_sdk_credentials(): _LOGGER.debug("Cloud SDK credentials not found on disk; not using them") return None, None - credentials, project_id = load_credentials_from_file(credentials_filename) + credentials, project_id = load_credentials_from_file( + credentials_filename, quota_project_id=quota_project_id + ) if not project_id: project_id = _cloud_sdk.get_project_id() @@ -193,7 +195,7 @@ def _get_gcloud_sdk_credentials(): return credentials, project_id -def _get_explicit_environ_credentials(): +def _get_explicit_environ_credentials(quota_project_id=None): """Gets credentials from the GOOGLE_APPLICATION_CREDENTIALS environment variable.""" from google.auth import _cloud_sdk @@ -213,11 +215,11 @@ def _get_explicit_environ_credentials(): "Explicit credentials path %s is the same as Cloud SDK credentials path, fall back to Cloud SDK credentials flow...", explicit_file, ) - return _get_gcloud_sdk_credentials() + return _get_gcloud_sdk_credentials(quota_project_id=quota_project_id) if explicit_file is not None: credentials, project_id = load_credentials_from_file( - os.environ[environment_vars.CREDENTIALS] + os.environ[environment_vars.CREDENTIALS], quota_project_id=quota_project_id ) return credentials, project_id @@ -447,8 +449,8 @@ def default(scopes=None, request=None, quota_project_id=None, default_scopes=Non # with_scopes_if_required() below will ensure scopes/default scopes are # safely set on the returned credentials since requires_scopes will # guard against setting scopes on user credentials. - _get_explicit_environ_credentials, - _get_gcloud_sdk_credentials, + lambda: _get_explicit_environ_credentials(quota_project_id=quota_project_id), + lambda: _get_gcloud_sdk_credentials(quota_project_id=quota_project_id), _get_gae_credentials, lambda: _get_gce_credentials(request), ) diff --git a/google/auth/_default_async.py b/google/auth/_default_async.py index 82e6c432d..3fa125b46 100644 --- a/google/auth/_default_async.py +++ b/google/auth/_default_async.py @@ -73,11 +73,13 @@ def load_credentials_from_file(filename, scopes=None, quota_project_id=None): try: credentials = credentials.Credentials.from_authorized_user_info( info, scopes=scopes - ).with_quota_project(quota_project_id) + ) except ValueError as caught_exc: msg = "Failed to load authorized user credentials from {}".format(filename) new_exc = exceptions.DefaultCredentialsError(msg, caught_exc) raise new_exc from caught_exc + if quota_project_id: + credentials = credentials.with_quota_project(quota_project_id) if not credentials.quota_project_id: _default._warn_about_problematic_credentials(credentials) return credentials, None @@ -104,7 +106,7 @@ def load_credentials_from_file(filename, scopes=None, quota_project_id=None): ) -def _get_gcloud_sdk_credentials(): +def _get_gcloud_sdk_credentials(quota_project_id=None): """Gets the credentials and project ID from the Cloud SDK.""" from google.auth import _cloud_sdk @@ -114,7 +116,9 @@ def _get_gcloud_sdk_credentials(): if not os.path.isfile(credentials_filename): return None, None - credentials, project_id = load_credentials_from_file(credentials_filename) + credentials, project_id = load_credentials_from_file( + credentials_filename, quota_project_id=quota_project_id + ) if not project_id: project_id = _cloud_sdk.get_project_id() @@ -122,7 +126,7 @@ def _get_gcloud_sdk_credentials(): return credentials, project_id -def _get_explicit_environ_credentials(): +def _get_explicit_environ_credentials(quota_project_id=None): """Gets credentials from the GOOGLE_APPLICATION_CREDENTIALS environment variable.""" from google.auth import _cloud_sdk @@ -134,11 +138,11 @@ def _get_explicit_environ_credentials(): # Cloud sdk flow calls gcloud to fetch project id, so if the explicit # file path is cloud sdk credentials path, then we should fall back # to cloud sdk flow, otherwise project id cannot be obtained. - return _get_gcloud_sdk_credentials() + return _get_gcloud_sdk_credentials(quota_project_id=quota_project_id) if explicit_file is not None: credentials, project_id = load_credentials_from_file( - os.environ[environment_vars.CREDENTIALS] + os.environ[environment_vars.CREDENTIALS], quota_project_id=quota_project_id ) return credentials, project_id @@ -250,8 +254,8 @@ def default_async(scopes=None, request=None, quota_project_id=None): ) checkers = ( - _get_explicit_environ_credentials, - _get_gcloud_sdk_credentials, + lambda: _get_explicit_environ_credentials(quota_project_id=quota_project_id), + lambda: _get_gcloud_sdk_credentials(quota_project_id=quota_project_id), _get_gae_credentials, lambda: _get_gce_credentials(request), ) diff --git a/google/auth/_helpers.py b/google/auth/_helpers.py index 09f32f84e..55adf5bc3 100644 --- a/google/auth/_helpers.py +++ b/google/auth/_helpers.py @@ -20,8 +20,11 @@ import urllib -CLOCK_SKEW_SECS = 10 # 10 seconds -CLOCK_SKEW = datetime.timedelta(seconds=CLOCK_SKEW_SECS) +# Token server doesn't provide a new a token when doing refresh unless the +# token is expiring within 30 seconds, so refresh threshold should not be +# more than 30 seconds. Otherwise auth lib will send tons of refresh requests +# until 30 seconds before the expiration, and cause a spike of CPU usage. +REFRESH_THRESHOLD = datetime.timedelta(seconds=20) def copy_docstring(source_class): diff --git a/google/auth/credentials.py b/google/auth/credentials.py index 6356f5434..8d9974ce1 100644 --- a/google/auth/credentials.py +++ b/google/auth/credentials.py @@ -62,7 +62,7 @@ def expired(self): # Remove 10 seconds from expiry to err on the side of reporting # expiration early so that we avoid the 401-refresh-retry loop. - skewed_expiry = self.expiry - _helpers.CLOCK_SKEW + skewed_expiry = self.expiry - _helpers.REFRESH_THRESHOLD return _helpers.utcnow() >= skewed_expiry @property diff --git a/google/auth/exceptions.py b/google/auth/exceptions.py index 57f181ea1..e9e737780 100644 --- a/google/auth/exceptions.py +++ b/google/auth/exceptions.py @@ -57,3 +57,7 @@ def __init__(self, message=None): super(ReauthFailError, self).__init__( "Reauthentication failed. {0}".format(message) ) + + +class ReauthSamlChallengeFailError(ReauthFailError): + """An exception for SAML reauth challenge failures.""" diff --git a/google/auth/jwt.py b/google/auth/jwt.py index d931bf7b9..bb9ffae83 100644 --- a/google/auth/jwt.py +++ b/google/auth/jwt.py @@ -167,12 +167,14 @@ def decode_header(token): return header -def _verify_iat_and_exp(payload): +def _verify_iat_and_exp(payload, clock_skew_in_seconds=0): """Verifies the ``iat`` (Issued At) and ``exp`` (Expires) claims in a token payload. Args: payload (Mapping[str, str]): The JWT payload. + clock_skew_in_seconds (int): The clock skew used for `iat` and `exp` + validation. Raises: ValueError: if any checks failed. @@ -188,20 +190,24 @@ def _verify_iat_and_exp(payload): iat = payload["iat"] # Err on the side of accepting a token that is slightly early to account # for clock skew. - earliest = iat - _helpers.CLOCK_SKEW_SECS + earliest = iat - clock_skew_in_seconds if now < earliest: - raise ValueError("Token used too early, {} < {}".format(now, iat)) + raise ValueError( + "Token used too early, {} < {}. Check that your computer's clock is set correctly.".format( + now, iat + ) + ) # Make sure the token wasn't issued in the past. exp = payload["exp"] # Err on the side of accepting a token that is slightly out of date # to account for clow skew. - latest = exp + _helpers.CLOCK_SKEW_SECS + latest = exp + clock_skew_in_seconds if latest < now: raise ValueError("Token expired, {} < {}".format(latest, now)) -def decode(token, certs=None, verify=True, audience=None): +def decode(token, certs=None, verify=True, audience=None, clock_skew_in_seconds=0): """Decode and verify a JWT. Args: @@ -217,6 +223,8 @@ def decode(token, certs=None, verify=True, audience=None): audience (str or list): The audience claim, 'aud', that this JWT should contain. Or a list of audience claims. If None then the JWT's 'aud' parameter is not verified. + clock_skew_in_seconds (int): The clock skew used for `iat` and `exp` + validation. Returns: Mapping[str, str]: The deserialized JSON payload in the JWT. @@ -267,7 +275,7 @@ def decode(token, certs=None, verify=True, audience=None): raise ValueError("Could not verify token signature.") # Verify the issued at and created times in the payload. - _verify_iat_and_exp(payload) + _verify_iat_and_exp(payload, clock_skew_in_seconds) # Check audience. if audience is not None: diff --git a/google/auth/version.py b/google/auth/version.py index 46b4030bd..a02dc8aec 100644 --- a/google/auth/version.py +++ b/google/auth/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.0.2" +__version__ = "2.1.0" diff --git a/google/oauth2/_credentials_async.py b/google/oauth2/_credentials_async.py index b4878c543..e7b9637c8 100644 --- a/google/oauth2/_credentials_async.py +++ b/google/oauth2/_credentials_async.py @@ -75,6 +75,7 @@ async def refresh(self, request): self._client_secret, scopes=self._scopes, rapt_token=self._rapt_token, + enable_reauth_refresh=self._enable_reauth_refresh, ) self.token = access_token diff --git a/google/oauth2/_reauth_async.py b/google/oauth2/_reauth_async.py index 510578bf7..f74f50b43 100644 --- a/google/oauth2/_reauth_async.py +++ b/google/oauth2/_reauth_async.py @@ -248,6 +248,7 @@ async def refresh_grant( client_secret, scopes=None, rapt_token=None, + enable_reauth_refresh=False, ): """Implements the reauthentication flow. @@ -265,6 +266,9 @@ async def refresh_grant( token has a wild card scope (e.g. 'https://www.googleapis.com/auth/any-api'). rapt_token (Optional(str)): The rapt token for reauth. + enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow + should be used. The default value is False. This option is for + gcloud only, other users should use the default value. Returns: Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The @@ -299,6 +303,11 @@ async def refresh_grant( == reauth._REAUTH_NEEDED_ERROR_RAPT_REQUIRED ) ): + if not enable_reauth_refresh: + raise exceptions.RefreshError( + "Reauthentication is needed. Please run `gcloud auth login --update-adc` to reauthenticate." + ) + rapt_token = await get_rapt_token( request, client_id, client_secret, refresh_token, token_uri, scopes=scopes ) diff --git a/google/oauth2/challenges.py b/google/oauth2/challenges.py index 7756a8057..0baff62e0 100644 --- a/google/oauth2/challenges.py +++ b/google/oauth2/challenges.py @@ -25,6 +25,9 @@ REAUTH_ORIGIN = "https://accounts.google.com" +SAML_CHALLENGE_MESSAGE = ( + "Please run `gcloud auth login` to complete reauthentication with SAML." +) def get_user_password(text): @@ -148,7 +151,30 @@ def obtain_challenge_input(self, metadata): return None +class SamlChallenge(ReauthChallenge): + """Challenge that asks the users to browse to their ID Providers. + + Currently SAML challenge is not supported. When obtaining the challenge + input, exception will be raised to instruct the users to run + `gcloud auth login` for reauthentication. + """ + + @property + def name(self): + return "SAML" + + @property + def is_locally_eligible(self): + return True + + def obtain_challenge_input(self, metadata): + # Magic Arch has not fully supported returning a proper dedirect URL + # for programmatic SAML users today. So we error our here and request + # users to use gcloud to complete a login. + raise exceptions.ReauthSamlChallengeFailError(SAML_CHALLENGE_MESSAGE) + + AVAILABLE_CHALLENGES = { challenge.name: challenge - for challenge in [SecurityKeyChallenge(), PasswordChallenge()] + for challenge in [SecurityKeyChallenge(), PasswordChallenge(), SamlChallenge()] } diff --git a/google/oauth2/credentials.py b/google/oauth2/credentials.py index 98fd71b04..6d34edf04 100644 --- a/google/oauth2/credentials.py +++ b/google/oauth2/credentials.py @@ -54,6 +54,9 @@ class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaPr credentials = credentials.with_quota_project('myproject-123) + Reauth is disabled by default. To enable reauth, set the + `enable_reauth_refresh` parameter to True in the constructor. Note that + reauth feature is intended for gcloud to use only. If reauth is enabled, `pyu2f` dependency has to be installed in order to use security key reauth feature. Dependency can be installed via `pip install pyu2f` or `pip install google-auth[reauth]`. @@ -73,6 +76,7 @@ def __init__( expiry=None, rapt_token=None, refresh_handler=None, + enable_reauth_refresh=False, ): """ Args: @@ -109,6 +113,8 @@ def __init__( refresh tokens are provided and tokens are obtained by calling some external process on demand. It is particularly useful for retrieving downscoped tokens from a token broker. + enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow + should be used. This flag is for gcloud to use only. """ super(Credentials, self).__init__() self.token = token @@ -123,6 +129,7 @@ def __init__( self._quota_project_id = quota_project_id self._rapt_token = rapt_token self.refresh_handler = refresh_handler + self._enable_reauth_refresh = enable_reauth_refresh def __getstate__(self): """A __getstate__ method must exist for the __setstate__ to be called @@ -151,6 +158,7 @@ def __setstate__(self, d): self._client_secret = d.get("_client_secret") self._quota_project_id = d.get("_quota_project_id") self._rapt_token = d.get("_rapt_token") + self._enable_reauth_refresh = d.get("_enable_reauth_refresh") # The refresh_handler setter should be used to repopulate this. self._refresh_handler = None @@ -241,6 +249,7 @@ def with_quota_project(self, quota_project_id): default_scopes=self.default_scopes, quota_project_id=quota_project_id, rapt_token=self.rapt_token, + enable_reauth_refresh=self._enable_reauth_refresh, ) @_helpers.copy_docstring(credentials.Credentials) @@ -261,7 +270,7 @@ def refresh(self, request): raise exceptions.RefreshError( "The refresh_handler returned expiry is not a datetime object." ) - if _helpers.utcnow() >= expiry - _helpers.CLOCK_SKEW: + if _helpers.utcnow() >= expiry - _helpers.REFRESH_THRESHOLD: raise exceptions.RefreshError( "The credentials returned by the refresh_handler are " "already expired." @@ -296,6 +305,7 @@ def refresh(self, request): self._client_secret, scopes=scopes, rapt_token=self._rapt_token, + enable_reauth_refresh=self._enable_reauth_refresh, ) self.token = access_token @@ -349,7 +359,7 @@ def from_authorized_user_info(cls, info, scopes=None): expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S" ) else: - expiry = _helpers.utcnow() - _helpers.CLOCK_SKEW + expiry = _helpers.utcnow() - _helpers.REFRESH_THRESHOLD # process scopes, which needs to be a seq if scopes is None and "scopes" in info: @@ -366,6 +376,7 @@ def from_authorized_user_info(cls, info, scopes=None): client_secret=info.get("client_secret"), quota_project_id=info.get("quota_project_id"), # may not exist expiry=expiry, + rapt_token=info.get("rapt_token"), # may not exist ) @classmethod diff --git a/google/oauth2/reauth.py b/google/oauth2/reauth.py index fc2629e82..1e496d12e 100644 --- a/google/oauth2/reauth.py +++ b/google/oauth2/reauth.py @@ -275,6 +275,7 @@ def refresh_grant( client_secret, scopes=None, rapt_token=None, + enable_reauth_refresh=False, ): """Implements the reauthentication flow. @@ -292,6 +293,9 @@ def refresh_grant( token has a wild card scope (e.g. 'https://www.googleapis.com/auth/any-api'). rapt_token (Optional(str)): The rapt token for reauth. + enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow + should be used. The default value is False. This option is for + gcloud only, other users should use the default value. Returns: Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The @@ -324,6 +328,11 @@ def refresh_grant( or response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_RAPT_REQUIRED ) ): + if not enable_reauth_refresh: + raise exceptions.RefreshError( + "Reauthentication is needed. Please run `gcloud auth login --update-adc` to reauthenticate." + ) + rapt_token = get_rapt_token( request, client_id, client_secret, refresh_token, token_uri, scopes=scopes ) diff --git a/owlbot.py b/owlbot.py index 61cf1281a..58aa53a70 100644 --- a/owlbot.py +++ b/owlbot.py @@ -25,9 +25,4 @@ 'value: "docs"', ) -# Remove the replacement below once https://github.com/googleapis/synthtool/pull/1188 is merged - -# Update googleapis/repo-automation-bots repo to main in .kokoro/*.sh files -assert 1 == s.replace(".kokoro/*.sh", "repo-automation-bots/tree/master", "repo-automation-bots/tree/main") - s.shell.run(["nox", "-s", "blacken"], hide_output=False) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 8f0d88ac6..a44427604 100644 Binary files a/system_tests/secrets.tar.enc and b/system_tests/secrets.tar.enc differ diff --git a/tests/compute_engine/test_credentials.py b/tests/compute_engine/test_credentials.py index ebe9aa5ba..81cc6db31 100644 --- a/tests/compute_engine/test_credentials.py +++ b/tests/compute_engine/test_credentials.py @@ -64,7 +64,7 @@ def test_default_state(self): @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.min + _helpers.CLOCK_SKEW, + return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) def test_refresh_success(self, get, utcnow): @@ -98,7 +98,7 @@ def test_refresh_success(self, get, utcnow): @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.min + _helpers.CLOCK_SKEW, + return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) def test_refresh_success_with_scopes(self, get, utcnow): diff --git a/tests/data/authorized_user_with_rapt_token.json b/tests/data/authorized_user_with_rapt_token.json new file mode 100644 index 000000000..64b161d42 --- /dev/null +++ b/tests/data/authorized_user_with_rapt_token.json @@ -0,0 +1,8 @@ +{ + "client_id": "123", + "client_secret": "secret", + "refresh_token": "alabalaportocala", + "type": "authorized_user", + "rapt_token": "rapt" + } + \ No newline at end of file diff --git a/tests/oauth2/test_challenges.py b/tests/oauth2/test_challenges.py index 019b908da..412895ada 100644 --- a/tests/oauth2/test_challenges.py +++ b/tests/oauth2/test_challenges.py @@ -130,3 +130,11 @@ def test_password_challenge(getpass_mock): assert challenges.PasswordChallenge().obtain_challenge_input({}) == { "credential": " " } + + +def test_saml_challenge(): + challenge = challenges.SamlChallenge() + assert challenge.is_locally_eligible + assert challenge.name == "SAML" + with pytest.raises(exceptions.ReauthSamlChallengeFailError): + challenge.obtain_challenge_input(None) diff --git a/tests/oauth2/test_credentials.py b/tests/oauth2/test_credentials.py index 4a7f66e7f..243f97de8 100644 --- a/tests/oauth2/test_credentials.py +++ b/tests/oauth2/test_credentials.py @@ -51,6 +51,7 @@ def make_credentials(cls): client_id=cls.CLIENT_ID, client_secret=cls.CLIENT_SECRET, rapt_token=cls.RAPT_TOKEN, + enable_reauth_refresh=True, ) def test_default_state(self): @@ -114,7 +115,7 @@ def test_invalid_refresh_handler(self): @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.min + _helpers.CLOCK_SKEW, + return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) def test_refresh_success(self, unused_utcnow, refresh_grant): token = "token" @@ -149,6 +150,7 @@ def test_refresh_success(self, unused_utcnow, refresh_grant): self.CLIENT_SECRET, None, self.RAPT_TOKEN, + True, ) # Check that the credentials have the token and expiry @@ -173,7 +175,7 @@ def test_refresh_no_refresh_token(self): @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.min + _helpers.CLOCK_SKEW, + return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) def test_refresh_with_refresh_token_and_refresh_handler( self, unused_utcnow, refresh_grant @@ -219,6 +221,7 @@ def test_refresh_with_refresh_token_and_refresh_handler( self.CLIENT_SECRET, None, self.RAPT_TOKEN, + False, ) # Check that the credentials have the token and expiry @@ -358,7 +361,7 @@ def test_refresh_with_refresh_handler_invalid_expiry(self): @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh_with_refresh_handler_expired_token(self, unused_utcnow): - expected_expiry = datetime.datetime.min + _helpers.CLOCK_SKEW + expected_expiry = datetime.datetime.min + _helpers.REFRESH_THRESHOLD # Simulate refresh handler returns an expired token. refresh_handler = mock.Mock(return_value=("TOKEN", expected_expiry)) scopes = ["email", "profile"] @@ -388,7 +391,7 @@ def test_refresh_with_refresh_handler_expired_token(self, unused_utcnow): @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.min + _helpers.CLOCK_SKEW, + return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) def test_credentials_with_scopes_requested_refresh_success( self, unused_utcnow, refresh_grant @@ -422,6 +425,7 @@ def test_credentials_with_scopes_requested_refresh_success( scopes=scopes, default_scopes=default_scopes, rapt_token=self.RAPT_TOKEN, + enable_reauth_refresh=True, ) # Refresh credentials @@ -436,6 +440,7 @@ def test_credentials_with_scopes_requested_refresh_success( self.CLIENT_SECRET, scopes, self.RAPT_TOKEN, + True, ) # Check that the credentials have the token and expiry @@ -452,7 +457,7 @@ def test_credentials_with_scopes_requested_refresh_success( @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.min + _helpers.CLOCK_SKEW, + return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) def test_credentials_with_only_default_scopes_requested( self, unused_utcnow, refresh_grant @@ -484,6 +489,7 @@ def test_credentials_with_only_default_scopes_requested( client_secret=self.CLIENT_SECRET, default_scopes=default_scopes, rapt_token=self.RAPT_TOKEN, + enable_reauth_refresh=True, ) # Refresh credentials @@ -498,6 +504,7 @@ def test_credentials_with_only_default_scopes_requested( self.CLIENT_SECRET, default_scopes, self.RAPT_TOKEN, + True, ) # Check that the credentials have the token and expiry @@ -514,7 +521,7 @@ def test_credentials_with_only_default_scopes_requested( @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.min + _helpers.CLOCK_SKEW, + return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) def test_credentials_with_scopes_returned_refresh_success( self, unused_utcnow, refresh_grant @@ -549,6 +556,7 @@ def test_credentials_with_scopes_returned_refresh_success( client_secret=self.CLIENT_SECRET, scopes=scopes, rapt_token=self.RAPT_TOKEN, + enable_reauth_refresh=True, ) # Refresh credentials @@ -563,6 +571,7 @@ def test_credentials_with_scopes_returned_refresh_success( self.CLIENT_SECRET, scopes, self.RAPT_TOKEN, + True, ) # Check that the credentials have the token and expiry @@ -579,7 +588,7 @@ def test_credentials_with_scopes_returned_refresh_success( @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.min + _helpers.CLOCK_SKEW, + return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) def test_credentials_with_scopes_refresh_failure_raises_refresh_error( self, unused_utcnow, refresh_grant @@ -615,6 +624,7 @@ def test_credentials_with_scopes_refresh_failure_raises_refresh_error( client_secret=self.CLIENT_SECRET, scopes=scopes, rapt_token=self.RAPT_TOKEN, + enable_reauth_refresh=True, ) # Refresh credentials @@ -632,6 +642,7 @@ def test_credentials_with_scopes_refresh_failure_raises_refresh_error( self.CLIENT_SECRET, scopes, self.RAPT_TOKEN, + True, ) # Check that the credentials have the token and expiry @@ -731,6 +742,7 @@ def test_from_authorized_user_file(self): assert creds.refresh_token == info["refresh_token"] assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT assert creds.scopes is None + assert creds.rapt_token is None scopes = ["email", "profile"] creds = credentials.Credentials.from_authorized_user_file( @@ -742,6 +754,18 @@ def test_from_authorized_user_file(self): assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT assert creds.scopes == scopes + def test_from_authorized_user_file_with_rapt_token(self): + info = AUTH_USER_INFO.copy() + file_path = os.path.join(DATA_DIR, "authorized_user_with_rapt_token.json") + + creds = credentials.Credentials.from_authorized_user_file(file_path) + assert creds.client_secret == info["client_secret"] + assert creds.client_id == info["client_id"] + assert creds.refresh_token == info["refresh_token"] + assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT + assert creds.scopes is None + assert creds.rapt_token == "rapt" + def test_to_json(self): info = AUTH_USER_INFO.copy() expiry = datetime.datetime(2020, 8, 14, 15, 54, 1) diff --git a/tests/oauth2/test_reauth.py b/tests/oauth2/test_reauth.py index e9ffa8a79..58d649d83 100644 --- a/tests/oauth2/test_reauth.py +++ b/tests/oauth2/test_reauth.py @@ -270,6 +270,7 @@ def test_refresh_grant_failed(): "client_secret", scopes=["foo", "bar"], rapt_token="rapt_token", + enable_reauth_refresh=True, ) assert excinfo.match(r"Bad request") mock_token_request.assert_called_with( @@ -298,7 +299,12 @@ def test_refresh_grant_success(): "google.oauth2.reauth.get_rapt_token", return_value="new_rapt_token" ): assert reauth.refresh_grant( - MOCK_REQUEST, "token_uri", "refresh_token", "client_id", "client_secret" + MOCK_REQUEST, + "token_uri", + "refresh_token", + "client_id", + "client_secret", + enable_reauth_refresh=True, ) == ( "access_token", "refresh_token", @@ -306,3 +312,18 @@ def test_refresh_grant_success(): {"access_token": "access_token"}, "new_rapt_token", ) + + +def test_refresh_grant_reauth_refresh_disabled(): + with mock.patch( + "google.oauth2._client._token_endpoint_request_no_throw" + ) as mock_token_request: + mock_token_request.side_effect = [ + (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}), + (True, {"access_token": "access_token"}), + ] + with pytest.raises(exceptions.RefreshError) as excinfo: + reauth.refresh_grant( + MOCK_REQUEST, "token_uri", "refresh_token", "client_id", "client_secret" + ) + assert excinfo.match(r"Reauthentication is needed") diff --git a/tests/test__default.py b/tests/test__default.py index a515f3813..c70ceaa57 100644 --- a/tests/test__default.py +++ b/tests/test__default.py @@ -328,15 +328,18 @@ def test__get_explicit_environ_credentials_no_env(): assert _default._get_explicit_environ_credentials() == (None, None) +@pytest.mark.parametrize("quota_project_id", [None, "project-foo"]) @LOAD_FILE_PATCH -def test__get_explicit_environ_credentials(load, monkeypatch): +def test__get_explicit_environ_credentials(load, quota_project_id, monkeypatch): monkeypatch.setenv(environment_vars.CREDENTIALS, "filename") - credentials, project_id = _default._get_explicit_environ_credentials() + credentials, project_id = _default._get_explicit_environ_credentials( + quota_project_id=quota_project_id + ) assert credentials is MOCK_CREDENTIALS assert project_id is mock.sentinel.project_id - load.assert_called_with("filename") + load.assert_called_with("filename", quota_project_id=quota_project_id) @LOAD_FILE_PATCH @@ -350,36 +353,40 @@ def test__get_explicit_environ_credentials_no_project_id(load, monkeypatch): assert project_id is None +@pytest.mark.parametrize("quota_project_id", [None, "project-foo"]) @mock.patch( "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True ) @mock.patch("google.auth._default._get_gcloud_sdk_credentials", autospec=True) def test__get_explicit_environ_credentials_fallback_to_gcloud( - get_gcloud_creds, get_adc_path, monkeypatch + get_gcloud_creds, get_adc_path, quota_project_id, monkeypatch ): # Set explicit credentials path to cloud sdk credentials path. get_adc_path.return_value = "filename" monkeypatch.setenv(environment_vars.CREDENTIALS, "filename") - _default._get_explicit_environ_credentials() + _default._get_explicit_environ_credentials(quota_project_id=quota_project_id) # Check we fall back to cloud sdk flow since explicit credentials path is # cloud sdk credentials path - get_gcloud_creds.assert_called_once() + get_gcloud_creds.assert_called_with(quota_project_id=quota_project_id) +@pytest.mark.parametrize("quota_project_id", [None, "project-foo"]) @LOAD_FILE_PATCH @mock.patch( "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True ) -def test__get_gcloud_sdk_credentials(get_adc_path, load): +def test__get_gcloud_sdk_credentials(get_adc_path, load, quota_project_id): get_adc_path.return_value = SERVICE_ACCOUNT_FILE - credentials, project_id = _default._get_gcloud_sdk_credentials() + credentials, project_id = _default._get_gcloud_sdk_credentials( + quota_project_id=quota_project_id + ) assert credentials is MOCK_CREDENTIALS assert project_id is mock.sentinel.project_id - load.assert_called_with(SERVICE_ACCOUNT_FILE) + load.assert_called_with(SERVICE_ACCOUNT_FILE, quota_project_id=quota_project_id) @mock.patch( @@ -779,3 +786,22 @@ def test_default_environ_external_credentials_bad_format(monkeypatch, tmpdir): assert excinfo.match( "Failed to load external account credentials from {}".format(str(filename)) ) + + +@mock.patch( + "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True +) +def test_default_warning_without_quota_project_id_for_user_creds(get_adc_path): + get_adc_path.return_value = AUTHORIZED_USER_CLOUD_SDK_FILE + + with pytest.warns(UserWarning, match="Cloud SDK"): + credentials, project_id = _default.default(quota_project_id=None) + + +@mock.patch( + "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True +) +def test_default_no_warning_with_quota_project_id_for_user_creds(get_adc_path): + get_adc_path.return_value = AUTHORIZED_USER_CLOUD_SDK_FILE + + credentials, project_id = _default.default(quota_project_id="project-foo") diff --git a/tests/test_credentials.py b/tests/test_credentials.py index 0633b38c0..2de638840 100644 --- a/tests/test_credentials.py +++ b/tests/test_credentials.py @@ -46,7 +46,9 @@ def test_expired_and_valid(): # Set the expiration to one second more than now plus the clock skew # accomodation. These credentials should be valid. credentials.expiry = ( - datetime.datetime.utcnow() + _helpers.CLOCK_SKEW + datetime.timedelta(seconds=1) + datetime.datetime.utcnow() + + _helpers.REFRESH_THRESHOLD + + datetime.timedelta(seconds=1) ) assert credentials.valid diff --git a/tests/test_downscoped.py b/tests/test_downscoped.py index 795ec2942..9ca95f5aa 100644 --- a/tests/test_downscoped.py +++ b/tests/test_downscoped.py @@ -669,7 +669,9 @@ def test_before_request_expired(self, utcnow): # Set the expiration to one second more than now plus the clock skew # accommodation. These credentials should be valid. credentials.expiry = ( - datetime.datetime.min + _helpers.CLOCK_SKEW + datetime.timedelta(seconds=1) + datetime.datetime.min + + _helpers.REFRESH_THRESHOLD + + datetime.timedelta(seconds=1) ) assert credentials.valid diff --git a/tests/test_external_account.py b/tests/test_external_account.py index e8297dab6..df6174f17 100644 --- a/tests/test_external_account.py +++ b/tests/test_external_account.py @@ -976,7 +976,9 @@ def test_before_request_expired(self, utcnow): # Set the expiration to one second more than now plus the clock skew # accomodation. These credentials should be valid. credentials.expiry = ( - datetime.datetime.min + _helpers.CLOCK_SKEW + datetime.timedelta(seconds=1) + datetime.datetime.min + + _helpers.REFRESH_THRESHOLD + + datetime.timedelta(seconds=1) ) assert credentials.valid @@ -1027,7 +1029,9 @@ def test_before_request_impersonation_expired(self, utcnow): # Set the expiration to one second more than now plus the clock skew # accomodation. These credentials should be valid. credentials.expiry = ( - datetime.datetime.min + _helpers.CLOCK_SKEW + datetime.timedelta(seconds=1) + datetime.datetime.min + + _helpers.REFRESH_THRESHOLD + + datetime.timedelta(seconds=1) ) assert credentials.valid diff --git a/tests/test_iam.py b/tests/test_iam.py index 30ce2279f..e9eca583c 100644 --- a/tests/test_iam.py +++ b/tests/test_iam.py @@ -45,7 +45,7 @@ def __init__(self): super(CredentialsImpl, self).__init__() self.token = "token" # Force refresh - self.expiry = datetime.datetime.min + _helpers.CLOCK_SKEW + self.expiry = datetime.datetime.min + _helpers.REFRESH_THRESHOLD def refresh(self, request): pass diff --git a/tests/test_impersonated_credentials.py b/tests/test_impersonated_credentials.py index 126c4c344..3dbb6caa6 100644 --- a/tests/test_impersonated_credentials.py +++ b/tests/test_impersonated_credentials.py @@ -211,11 +211,11 @@ def test_refresh_source_credentials(self, time_skew): credentials = self.make_credentials(lifetime=None) # Source credentials is refreshed only if it is expired within - # _helpers.CLOCK_SKEW from now. We add a time_skew to the expiry, so + # _helpers.REFRESH_THRESHOLD from now. We add a time_skew to the expiry, so # source credentials is refreshed only if time_skew <= 0. credentials._source_credentials.expiry = ( _helpers.utcnow() - + _helpers.CLOCK_SKEW + + _helpers.REFRESH_THRESHOLD + datetime.timedelta(seconds=time_skew) ) credentials._source_credentials.token = "Token" @@ -238,7 +238,7 @@ def test_refresh_source_credentials(self, time_skew): assert not credentials.expired # Source credentials is refreshed only if it is expired within - # _helpers.CLOCK_SKEW + # _helpers.REFRESH_THRESHOLD if time_skew > 0: source_cred_refresh.assert_not_called() else: diff --git a/tests/test_jwt.py b/tests/test_jwt.py index 0dd7fa968..ba7277cdc 100644 --- a/tests/test_jwt.py +++ b/tests/test_jwt.py @@ -197,7 +197,7 @@ def test_decode_bad_token_too_early(token_factory): } ) with pytest.raises(ValueError) as excinfo: - jwt.decode(token, PUBLIC_CERT_BYTES) + jwt.decode(token, PUBLIC_CERT_BYTES, clock_skew_in_seconds=59) assert excinfo.match(r"Token used too early") @@ -210,10 +210,40 @@ def test_decode_bad_token_expired(token_factory): } ) with pytest.raises(ValueError) as excinfo: - jwt.decode(token, PUBLIC_CERT_BYTES) + jwt.decode(token, PUBLIC_CERT_BYTES, clock_skew_in_seconds=59) assert excinfo.match(r"Token expired") +def test_decode_success_with_no_clock_skew(token_factory): + token = token_factory( + claims={ + "exp": _helpers.datetime_to_secs( + _helpers.utcnow() + datetime.timedelta(seconds=1) + ), + "iat": _helpers.datetime_to_secs( + _helpers.utcnow() - datetime.timedelta(seconds=1) + ), + } + ) + + jwt.decode(token, PUBLIC_CERT_BYTES) + + +def test_decode_success_with_custom_clock_skew(token_factory): + token = token_factory( + claims={ + "exp": _helpers.datetime_to_secs( + _helpers.utcnow() + datetime.timedelta(seconds=2) + ), + "iat": _helpers.datetime_to_secs( + _helpers.utcnow() - datetime.timedelta(seconds=2) + ), + } + ) + + jwt.decode(token, PUBLIC_CERT_BYTES, clock_skew_in_seconds=1) + + def test_decode_bad_token_wrong_audience(token_factory): token = token_factory() audience = "audience2@example.com" diff --git a/tests/transport/test_grpc.py b/tests/transport/test_grpc.py index 926c1bc40..3437658a3 100644 --- a/tests/transport/test_grpc.py +++ b/tests/transport/test_grpc.py @@ -80,7 +80,7 @@ def test_call_no_refresh(self): def test_call_refresh(self): credentials = CredentialsStub() - credentials.expiry = datetime.datetime.min + _helpers.CLOCK_SKEW + credentials.expiry = datetime.datetime.min + _helpers.REFRESH_THRESHOLD request = mock.create_autospec(transport.Request) plugin = google.auth.transport.grpc.AuthMetadataPlugin(credentials, request) diff --git a/tests_async/oauth2/test_credentials_async.py b/tests_async/oauth2/test_credentials_async.py index 99cf16f80..06c91419c 100644 --- a/tests_async/oauth2/test_credentials_async.py +++ b/tests_async/oauth2/test_credentials_async.py @@ -43,6 +43,7 @@ def make_credentials(cls): token_uri=cls.TOKEN_URI, client_id=cls.CLIENT_ID, client_secret=cls.CLIENT_SECRET, + enable_reauth_refresh=True, ) def test_default_state(self): @@ -61,7 +62,7 @@ def test_default_state(self): @mock.patch("google.oauth2._reauth_async.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.min + _helpers.CLOCK_SKEW, + return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) @pytest.mark.asyncio async def test_refresh_success(self, unused_utcnow, refresh_grant): @@ -97,6 +98,7 @@ async def test_refresh_success(self, unused_utcnow, refresh_grant): self.CLIENT_SECRET, None, None, + True, ) # Check that the credentials have the token and expiry @@ -122,7 +124,7 @@ async def test_refresh_no_refresh_token(self): @mock.patch("google.oauth2._reauth_async.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.min + _helpers.CLOCK_SKEW, + return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) @pytest.mark.asyncio async def test_credentials_with_scopes_requested_refresh_success( @@ -169,6 +171,7 @@ async def test_credentials_with_scopes_requested_refresh_success( self.CLIENT_SECRET, scopes, "old_rapt_token", + False, ) # Check that the credentials have the token and expiry @@ -185,7 +188,7 @@ async def test_credentials_with_scopes_requested_refresh_success( @mock.patch("google.oauth2._reauth_async.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.min + _helpers.CLOCK_SKEW, + return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) @pytest.mark.asyncio async def test_credentials_with_scopes_returned_refresh_success( @@ -231,6 +234,7 @@ async def test_credentials_with_scopes_returned_refresh_success( self.CLIENT_SECRET, scopes, None, + False, ) # Check that the credentials have the token and expiry @@ -247,7 +251,7 @@ async def test_credentials_with_scopes_returned_refresh_success( @mock.patch("google.oauth2._reauth_async.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.min + _helpers.CLOCK_SKEW, + return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) @pytest.mark.asyncio async def test_credentials_with_scopes_refresh_failure_raises_refresh_error( @@ -301,6 +305,7 @@ async def test_credentials_with_scopes_refresh_failure_raises_refresh_error( self.CLIENT_SECRET, scopes, None, + False, ) # Check that the credentials have the token and expiry diff --git a/tests_async/oauth2/test_reauth_async.py b/tests_async/oauth2/test_reauth_async.py index f144d89f5..d982e13a1 100644 --- a/tests_async/oauth2/test_reauth_async.py +++ b/tests_async/oauth2/test_reauth_async.py @@ -318,7 +318,12 @@ async def test_refresh_grant_success(): "google.oauth2._reauth_async.get_rapt_token", return_value="new_rapt_token" ): assert await _reauth_async.refresh_grant( - MOCK_REQUEST, "token_uri", "refresh_token", "client_id", "client_secret" + MOCK_REQUEST, + "token_uri", + "refresh_token", + "client_id", + "client_secret", + enable_reauth_refresh=True, ) == ( "access_token", "refresh_token", @@ -326,3 +331,19 @@ async def test_refresh_grant_success(): {"access_token": "access_token"}, "new_rapt_token", ) + + +@pytest.mark.asyncio +async def test_refresh_grant_reauth_refresh_disabled(): + with mock.patch( + "google.oauth2._client_async._token_endpoint_request_no_throw" + ) as mock_token_request: + mock_token_request.side_effect = [ + (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}), + (True, {"access_token": "access_token"}), + ] + with pytest.raises(exceptions.RefreshError) as excinfo: + assert await _reauth_async.refresh_grant( + MOCK_REQUEST, "token_uri", "refresh_token", "client_id", "client_secret" + ) + assert excinfo.match(r"Reauthentication is needed") diff --git a/tests_async/test__default_async.py b/tests_async/test__default_async.py index b67230342..69a50d69a 100644 --- a/tests_async/test__default_async.py +++ b/tests_async/test__default_async.py @@ -165,15 +165,18 @@ def test__get_explicit_environ_credentials_no_env(): assert _default._get_explicit_environ_credentials() == (None, None) +@pytest.mark.parametrize("quota_project_id", [None, "project-foo"]) @LOAD_FILE_PATCH -def test__get_explicit_environ_credentials(load, monkeypatch): +def test__get_explicit_environ_credentials(load, quota_project_id, monkeypatch): monkeypatch.setenv(environment_vars.CREDENTIALS, "filename") - credentials, project_id = _default._get_explicit_environ_credentials() + credentials, project_id = _default._get_explicit_environ_credentials( + quota_project_id=quota_project_id + ) assert credentials is MOCK_CREDENTIALS assert project_id is mock.sentinel.project_id - load.assert_called_with("filename") + load.assert_called_with("filename", quota_project_id=quota_project_id) @LOAD_FILE_PATCH @@ -187,36 +190,42 @@ def test__get_explicit_environ_credentials_no_project_id(load, monkeypatch): assert project_id is None +@pytest.mark.parametrize("quota_project_id", [None, "project-foo"]) @mock.patch( "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True ) @mock.patch("google.auth._default_async._get_gcloud_sdk_credentials", autospec=True) def test__get_explicit_environ_credentials_fallback_to_gcloud( - get_gcloud_creds, get_adc_path, monkeypatch + get_gcloud_creds, get_adc_path, quota_project_id, monkeypatch ): # Set explicit credentials path to cloud sdk credentials path. get_adc_path.return_value = "filename" monkeypatch.setenv(environment_vars.CREDENTIALS, "filename") - _default._get_explicit_environ_credentials() + _default._get_explicit_environ_credentials(quota_project_id=quota_project_id) # Check we fall back to cloud sdk flow since explicit credentials path is # cloud sdk credentials path - get_gcloud_creds.assert_called_once() + get_gcloud_creds.assert_called_with(quota_project_id=quota_project_id) +@pytest.mark.parametrize("quota_project_id", [None, "project-foo"]) @LOAD_FILE_PATCH @mock.patch( "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True ) -def test__get_gcloud_sdk_credentials(get_adc_path, load): +def test__get_gcloud_sdk_credentials(get_adc_path, load, quota_project_id): get_adc_path.return_value = test_default.SERVICE_ACCOUNT_FILE - credentials, project_id = _default._get_gcloud_sdk_credentials() + credentials, project_id = _default._get_gcloud_sdk_credentials( + quota_project_id=quota_project_id + ) assert credentials is MOCK_CREDENTIALS assert project_id is mock.sentinel.project_id - load.assert_called_with(test_default.SERVICE_ACCOUNT_FILE) + load.assert_called_with( + test_default.SERVICE_ACCOUNT_FILE, quota_project_id=quota_project_id + ) @mock.patch( @@ -533,3 +542,22 @@ def test_default_no_app_engine_compute_engine_module(unused_get): sys.modules["google.auth.compute_engine"] = None sys.modules["google.auth.app_engine"] = None assert _default.default_async() == (MOCK_CREDENTIALS, mock.sentinel.project_id) + + +@mock.patch( + "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True +) +def test_default_warning_without_quota_project_id_for_user_creds(get_adc_path): + get_adc_path.return_value = test_default.AUTHORIZED_USER_CLOUD_SDK_FILE + + with pytest.warns(UserWarning, match="Cloud SDK"): + credentials, project_id = _default.default_async(quota_project_id=None) + + +@mock.patch( + "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True +) +def test_default_no_warning_with_quota_project_id_for_user_creds(get_adc_path): + get_adc_path.return_value = test_default.AUTHORIZED_USER_CLOUD_SDK_FILE + + credentials, project_id = _default.default_async(quota_project_id="project-foo") diff --git a/tests_async/test_credentials_async.py b/tests_async/test_credentials_async.py index 0a4890825..5315483da 100644 --- a/tests_async/test_credentials_async.py +++ b/tests_async/test_credentials_async.py @@ -46,7 +46,9 @@ def test_expired_and_valid(): # Set the expiration to one second more than now plus the clock skew # accomodation. These credentials should be valid. credentials.expiry = ( - datetime.datetime.utcnow() + _helpers.CLOCK_SKEW + datetime.timedelta(seconds=1) + datetime.datetime.utcnow() + + _helpers.REFRESH_THRESHOLD + + datetime.timedelta(seconds=1) ) assert credentials.valid