Repository: googleapis/google-auth-library-python

Issue:
Wrong timezones in compute_engine.IDTokenCredentials expiry
The `expiry` of compute_engine.IDTokenCredentials is in the local timezone, but it's then compared to utc. This means that an expired token may be used. Expiry of all other credential types are correctly in UTC.

#### Environment details

  - OS: Linux
  - Python version: 3.11
  - pip version: 23.1.2
  - `google-auth` version: 2.19.1
  - Tested on GKE 1.24.12-gke.500 with Workload Identity, but AFAICT it would also fail anywhere on GCP

#### Steps to reproduce

Run on a GCE VM (or a GKE pod).

Configure Python to use some timezone far from UTC, eg `export TZ=America/New_York`.

```py
import google.auth.compute_engine.credentials
import google.auth.transport.requests

r = google.auth.transport.requests.Request()
creds = google.auth.compute_engine.credentials.IDTokenCredentials(r, target_audience="foo", use_metadata_identity_endpoint=True)
creds.refresh(r)

print(f"expiry: {creds.expiry}")
print(f"expired: {creds.expired}")
```

Here, `expired` incorrectly reports false, because it compares the local `expiry` with `utcnow`.

Another failure mode is in timezones with a positive offset (eg `Europe/Prague`), where the token will be treated as not-expired even after it actually expired.

All other credential types use utc for everything, so they don't have the problem. Even the compute engine OAuth2 credentials in the same file (ie just `Credentials`, not `IDTokenCredentials`).

Should be a very simple fix, to use UTC datetime everywhere.



Discussion:
Quick scan: Issue seems similar to https://github.com/googleapis/google-auth-library-python/issues/1264. Will take a deeper look later today.
Btw the fix is really simple, just changing `fromtimestamp` to `utcfromtimestamp` in https://github.com/googleapis/google-auth-library-python/blob/bcb85bf1f5b98d98c1b1af47487acb52053c4b17/google/auth/compute_engine/credentials.py#L392. We have deployed this as a hotfix to our app and works fine.

Resolving patch:
diff --git a/google/auth/compute_engine/credentials.py b/google/auth/compute_engine/credentials.py
--- a/google/auth/compute_engine/credentials.py
+++ b/google/auth/compute_engine/credentials.py
@@ -389,7 +389,7 @@ def _call_metadata_identity_endpoint(self, request):
             six.raise_from(new_exc, caught_exc)
 
         _, payload, _, _ = jwt._unverified_decode(id_token)
-        return id_token, datetime.datetime.fromtimestamp(payload["exp"])
+        return id_token, datetime.datetime.utcfromtimestamp(payload["exp"])
 
     def refresh(self, request):
         """Refreshes the ID token.


Test patch:
diff --git a/tests/compute_engine/test_credentials.py b/tests/compute_engine/test_credentials.py
--- a/tests/compute_engine/test_credentials.py
+++ b/tests/compute_engine/test_credentials.py
@@ -770,7 +770,7 @@ def test_get_id_token_from_metadata(
         }
 
         assert cred.token == SAMPLE_ID_TOKEN
-        assert cred.expiry == datetime.datetime.fromtimestamp(SAMPLE_ID_TOKEN_EXP)
+        assert cred.expiry == datetime.datetime.utcfromtimestamp(SAMPLE_ID_TOKEN_EXP)
         assert cred._use_metadata_identity_endpoint
         assert cred._signer is None
         assert cred._token_uri is None