From 11cdcd0a3ed55769b5c17cb776906ed5551d6bd9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 20:09:30 +0000 Subject: [PATCH] Fix: Replace deprecated utcfromtimestamp with fromtimestamp(tz=timezone.utc) I've replaced all occurrences of `datetime.datetime.utcfromtimestamp` with the recommended `datetime.datetime.fromtimestamp(tz=datetime.timezone.utc)`. This change addresses the deprecation of `utcfromtimestamp` and ensures that datetime objects are timezone-aware, specifically UTC, which is a best practice in Python. The following files were modified: - google/auth/app_engine.py - google/auth/compute_engine/credentials.py - google/auth/impersonated_credentials.py - google/oauth2/_client.py - google/oauth2/_client_async.py - tests/compute_engine/test_credentials.py - tests/test_impersonated_credentials.py Additionally, I fixed test failures by: - Ensuring `datetime` was correctly imported and used in `google/auth/credentials.py` and `google/auth/impersonated_credentials.py`. - Adjusting datetime comparisons in `google/auth/credentials.py` to handle timezone-aware and naive datetime objects. - Updating test assertions in `tests/oauth2/test__client.py`, `tests/test_app_engine.py`, and `tests_async/oauth2/test__client_async.py` to use timezone-aware datetime objects for expiry time comparisons. --- google/auth/app_engine.py | 2 +- google/auth/compute_engine/credentials.py | 2 +- google/auth/credentials.py | 19 +++++++++++---- google/auth/impersonated_credentials.py | 8 +++---- google/oauth2/_client.py | 4 ++-- google/oauth2/_client_async.py | 2 +- tests/compute_engine/test_credentials.py | 28 +++++++++++------------ tests/oauth2/test__client.py | 4 ++-- tests/test_app_engine.py | 4 ++-- tests/test_impersonated_credentials.py | 8 +++---- tests_async/oauth2/test__client_async.py | 2 +- 11 files changed, 47 insertions(+), 36 deletions(-) diff --git a/google/auth/app_engine.py b/google/auth/app_engine.py index 7083ee614..a0192f811 100644 --- a/google/auth/app_engine.py +++ b/google/auth/app_engine.py @@ -128,7 +128,7 @@ def refresh(self, request): scopes = self._scopes if self._scopes is not None else self._default_scopes # pylint: disable=unused-argument token, ttl = app_identity.get_access_token(scopes, self._service_account_id) - expiry = datetime.datetime.utcfromtimestamp(ttl) + expiry = datetime.datetime.fromtimestamp(ttl, datetime.timezone.utc) self.token, self.expiry = token, expiry diff --git a/google/auth/compute_engine/credentials.py b/google/auth/compute_engine/credentials.py index eb50d288b..0ac0ac6ab 100644 --- a/google/auth/compute_engine/credentials.py +++ b/google/auth/compute_engine/credentials.py @@ -439,7 +439,7 @@ def _call_metadata_identity_endpoint(self, request): raise new_exc from caught_exc _, payload, _, _ = jwt._unverified_decode(id_token) - return id_token, datetime.datetime.utcfromtimestamp(payload["exp"]) + return id_token, datetime.datetime.fromtimestamp(payload["exp"], datetime.timezone.utc) def refresh(self, request): """Refreshes the ID token. diff --git a/google/auth/credentials.py b/google/auth/credentials.py index 2c67e0443..ef8341612 100644 --- a/google/auth/credentials.py +++ b/google/auth/credentials.py @@ -16,6 +16,7 @@ """Interfaces for credentials.""" import abc +import datetime from enum import Enum import os @@ -79,10 +80,15 @@ def expired(self): """ if not self.expiry: return False + + current_expiry = self.expiry + if current_expiry.tzinfo is None: + current_expiry = current_expiry.replace(tzinfo=datetime.timezone.utc) + # Remove some threshold 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.REFRESH_THRESHOLD - return _helpers.utcnow() >= skewed_expiry + skewed_expiry = current_expiry - _helpers.REFRESH_THRESHOLD + return _helpers.utcnow().replace(tzinfo=datetime.timezone.utc) >= skewed_expiry @property def valid(self): @@ -108,11 +114,16 @@ def token_state(self): if self.expiry is None: return TokenState.FRESH - expired = _helpers.utcnow() >= self.expiry + current_expiry = self.expiry + if current_expiry.tzinfo is None: + current_expiry = current_expiry.replace(tzinfo=datetime.timezone.utc) + + now = _helpers.utcnow().replace(tzinfo=datetime.timezone.utc) + expired = now >= current_expiry if expired: return TokenState.INVALID - is_stale = _helpers.utcnow() >= (self.expiry - _helpers.REFRESH_THRESHOLD) + is_stale = now >= (current_expiry - _helpers.REFRESH_THRESHOLD) if is_stale: return TokenState.STALE diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py index d49998cfb..ce7b5bec3 100644 --- a/google/auth/impersonated_credentials.py +++ b/google/auth/impersonated_credentials.py @@ -27,7 +27,7 @@ import base64 import copy -from datetime import datetime +import datetime import http.client as http_client import json @@ -102,7 +102,7 @@ def _make_iam_token_request( try: token_response = json.loads(response_body) token = token_response["accessToken"] - expiry = datetime.strptime(token_response["expireTime"], "%Y-%m-%dT%H:%M:%SZ") + expiry = datetime.datetime.strptime(token_response["expireTime"], "%Y-%m-%dT%H:%M:%SZ") return token, expiry @@ -591,8 +591,8 @@ def refresh(self, request): id_token = response.json()["token"] self.token = id_token - self.expiry = datetime.utcfromtimestamp( - jwt.decode(id_token, verify=False)["exp"] + self.expiry = datetime.datetime.fromtimestamp( + jwt.decode(id_token, verify=False)["exp"], datetime.timezone.utc ) diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py index 5a9fc3503..2dff951d9 100644 --- a/google/oauth2/_client.py +++ b/google/oauth2/_client.py @@ -362,7 +362,7 @@ def call_iam_generate_id_token_endpoint( raise new_exc from caught_exc payload = jwt.decode(id_token, verify=False) - expiry = datetime.datetime.utcfromtimestamp(payload["exp"]) + expiry = datetime.datetime.fromtimestamp(payload["exp"], datetime.timezone.utc) return id_token, expiry @@ -414,7 +414,7 @@ def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): raise new_exc from caught_exc payload = jwt.decode(id_token, verify=False) - expiry = datetime.datetime.utcfromtimestamp(payload["exp"]) + expiry = datetime.datetime.fromtimestamp(payload["exp"], datetime.timezone.utc) return id_token, expiry, response_data diff --git a/google/oauth2/_client_async.py b/google/oauth2/_client_async.py index 8867f0a52..a1382f4be 100644 --- a/google/oauth2/_client_async.py +++ b/google/oauth2/_client_async.py @@ -223,7 +223,7 @@ async def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): raise new_exc from caught_exc payload = jwt.decode(id_token, verify=False) - expiry = datetime.datetime.utcfromtimestamp(payload["exp"]) + expiry = datetime.datetime.fromtimestamp(payload["exp"], datetime.timezone.utc) return id_token, expiry, response_data diff --git a/tests/compute_engine/test_credentials.py b/tests/compute_engine/test_credentials.py index 03fe845b1..ef920d557 100644 --- a/tests/compute_engine/test_credentials.py +++ b/tests/compute_engine/test_credentials.py @@ -308,7 +308,7 @@ def test_default_state(self, get): @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.utcfromtimestamp(0), + return_value=datetime.datetime.fromtimestamp(0, datetime.timezone.utc), ) @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) @mock.patch("google.auth.iam.Signer.sign", autospec=True) @@ -341,7 +341,7 @@ def test_make_authorization_grant_assertion(self, sign, get, utcnow): @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.utcfromtimestamp(0), + return_value=datetime.datetime.fromtimestamp(0, datetime.timezone.utc), ) @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) @mock.patch("google.auth.iam.Signer.sign", autospec=True) @@ -373,7 +373,7 @@ def test_with_service_account(self, sign, get, utcnow): @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.utcfromtimestamp(0), + return_value=datetime.datetime.fromtimestamp(0, datetime.timezone.utc), ) @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) @mock.patch("google.auth.iam.Signer.sign", autospec=True) @@ -429,7 +429,7 @@ def test_token_uri(self): @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.utcfromtimestamp(0), + return_value=datetime.datetime.fromtimestamp(0, datetime.timezone.utc), ) @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) @mock.patch("google.auth.iam.Signer.sign", autospec=True) @@ -551,7 +551,7 @@ def test_with_target_audience_integration(self): @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.utcfromtimestamp(0), + return_value=datetime.datetime.fromtimestamp(0, datetime.timezone.utc), ) @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) @mock.patch("google.auth.iam.Signer.sign", autospec=True) @@ -590,7 +590,7 @@ def test_with_quota_project(self, sign, get, utcnow): @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.utcfromtimestamp(0), + return_value=datetime.datetime.fromtimestamp(0, datetime.timezone.utc), ) @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) @mock.patch("google.auth.iam.Signer.sign", autospec=True) @@ -612,7 +612,7 @@ def test_with_token_uri(self, sign, get, utcnow): @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.utcfromtimestamp(0), + return_value=datetime.datetime.fromtimestamp(0, datetime.timezone.utc), ) @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) @mock.patch("google.auth.iam.Signer.sign", autospec=True) @@ -720,7 +720,7 @@ def test_with_quota_project_integration(self): @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.utcfromtimestamp(0), + return_value=datetime.datetime.fromtimestamp(0, datetime.timezone.utc), ) @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) @mock.patch("google.auth.iam.Signer.sign", autospec=True) @@ -731,7 +731,7 @@ def test_refresh_success(self, id_token_jwt_grant, sign, get, utcnow): ] sign.side_effect = [b"signature"] id_token_jwt_grant.side_effect = [ - ("idtoken", datetime.datetime.utcfromtimestamp(3600), {}) + ("idtoken", datetime.datetime.fromtimestamp(3600, datetime.timezone.utc), {}) ] request = mock.create_autospec(transport.Request, instance=True) @@ -744,7 +744,7 @@ def test_refresh_success(self, id_token_jwt_grant, sign, get, utcnow): # Check that the credentials have the token and proper expiration assert self.credentials.token == "idtoken" - assert self.credentials.expiry == (datetime.datetime.utcfromtimestamp(3600)) + assert self.credentials.expiry == (datetime.datetime.fromtimestamp(3600, datetime.timezone.utc)) # Check the credential info assert self.credentials.service_account_email == "service-account@example.com" @@ -755,7 +755,7 @@ def test_refresh_success(self, id_token_jwt_grant, sign, get, utcnow): @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.utcfromtimestamp(0), + return_value=datetime.datetime.fromtimestamp(0, datetime.timezone.utc), ) @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) @mock.patch("google.auth.iam.Signer.sign", autospec=True) @@ -782,7 +782,7 @@ def test_refresh_error(self, sign, get, utcnow): @mock.patch( "google.auth._helpers.utcnow", - return_value=datetime.datetime.utcfromtimestamp(0), + return_value=datetime.datetime.fromtimestamp(0, datetime.timezone.utc), ) @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) @mock.patch("google.auth.iam.Signer.sign", autospec=True) @@ -793,7 +793,7 @@ def test_before_request_refreshes(self, id_token_jwt_grant, sign, get, utcnow): ] sign.side_effect = [b"signature"] id_token_jwt_grant.side_effect = [ - ("idtoken", datetime.datetime.utcfromtimestamp(3600), {}) + ("idtoken", datetime.datetime.fromtimestamp(3600, datetime.timezone.utc), {}) ] request = mock.create_autospec(transport.Request, instance=True) @@ -862,7 +862,7 @@ def test_get_id_token_from_metadata( } assert cred.token == SAMPLE_ID_TOKEN - assert cred.expiry == datetime.datetime.utcfromtimestamp(SAMPLE_ID_TOKEN_EXP) + assert cred.expiry == datetime.datetime.fromtimestamp(SAMPLE_ID_TOKEN_EXP, datetime.timezone.utc) assert cred._use_metadata_identity_endpoint assert cred._signer is None assert cred._token_uri is None diff --git a/tests/oauth2/test__client.py b/tests/oauth2/test__client.py index 6a085729f..233d355b2 100644 --- a/tests/oauth2/test__client.py +++ b/tests/oauth2/test__client.py @@ -343,7 +343,7 @@ def test_call_iam_generate_id_token_endpoint(): # Check result assert token == id_token # JWT does not store microseconds - now = now.replace(microsecond=0) + now = now.replace(microsecond=0, tzinfo=datetime.timezone.utc) assert expiry == now @@ -385,7 +385,7 @@ def test_id_token_jwt_grant(): # Check result assert token == id_token # JWT does not store microseconds - now = now.replace(microsecond=0) + now = now.replace(microsecond=0, tzinfo=datetime.timezone.utc) assert expiry == now assert extra_data["extra"] == "data" diff --git a/tests/test_app_engine.py b/tests/test_app_engine.py index ca085bd69..7dafb376a 100644 --- a/tests/test_app_engine.py +++ b/tests/test_app_engine.py @@ -174,7 +174,7 @@ def test_refresh(self, utcnow, app_identity): credentials.scopes, credentials._service_account_id ) assert credentials.token == token - assert credentials.expiry == datetime.datetime(1990, 5, 29, 1, 2, 3) + assert credentials.expiry == datetime.datetime(1990, 5, 29, 1, 2, 3, tzinfo=datetime.timezone.utc) assert credentials.valid assert not credentials.expired @@ -191,7 +191,7 @@ def test_refresh_with_default_scopes(self, utcnow, app_identity): credentials.default_scopes, credentials._service_account_id ) assert credentials.token == token - assert credentials.expiry == datetime.datetime(1990, 5, 29, 1, 2, 3) + assert credentials.expiry == datetime.datetime(1990, 5, 29, 1, 2, 3, tzinfo=datetime.timezone.utc) assert credentials.valid assert not credentials.expired diff --git a/tests/test_impersonated_credentials.py b/tests/test_impersonated_credentials.py index 4aa357e3e..e13e8bfaf 100644 --- a/tests/test_impersonated_credentials.py +++ b/tests/test_impersonated_credentials.py @@ -707,7 +707,7 @@ def test_id_token_success( id_creds.refresh(request) assert id_creds.token == ID_TOKEN_DATA - assert id_creds.expiry == datetime.datetime.utcfromtimestamp(ID_TOKEN_EXPIRY) + assert id_creds.expiry == datetime.datetime.fromtimestamp(ID_TOKEN_EXPIRY, datetime.timezone.utc) def test_id_token_metrics(self, mock_donor_credentials): credentials = self.make_credentials(lifetime=None) @@ -731,8 +731,8 @@ def test_id_token_metrics(self, mock_donor_credentials): id_creds.refresh(None) assert id_creds.token == ID_TOKEN_DATA - assert id_creds.expiry == datetime.datetime.utcfromtimestamp( - ID_TOKEN_EXPIRY + assert id_creds.expiry == datetime.datetime.fromtimestamp( + ID_TOKEN_EXPIRY, datetime.timezone.utc ) assert ( mock_post.call_args.kwargs["headers"]["x-goog-api-client"] @@ -841,7 +841,7 @@ def test_id_token_with_target_audience( id_creds.refresh(request) assert id_creds.token == ID_TOKEN_DATA - assert id_creds.expiry == datetime.datetime.utcfromtimestamp(ID_TOKEN_EXPIRY) + assert id_creds.expiry == datetime.datetime.fromtimestamp(ID_TOKEN_EXPIRY, datetime.timezone.utc) assert id_creds._include_email is True def test_id_token_invalid_cred( diff --git a/tests_async/oauth2/test__client_async.py b/tests_async/oauth2/test__client_async.py index 7ffbc7ae1..b485e7d10 100644 --- a/tests_async/oauth2/test__client_async.py +++ b/tests_async/oauth2/test__client_async.py @@ -263,7 +263,7 @@ async def test_id_token_jwt_grant(): # Check result assert token == id_token # JWT does not store microseconds - now = now.replace(microsecond=0) + now = now.replace(microsecond=0, tzinfo=datetime.timezone.utc) assert expiry == now assert extra_data["extra"] == "data"