fix: fix expiry for `to_json()` (#589)
* This patch for </issues/501> includes the following fixes:
- The access token is always set to `None`, so the fix involves using (the access) `token` from the saved JSON credentials file.
- For refresh needs, `expiry` also needs to be saved via `to_json()`.
- DUMP: As `expiry` is a `datetime.datetime` object, serialize to `datetime.isoformat()` in the same [`oauth2client` format](https://github.com/googleapis/oauth2client/blob/master/oauth2client/client.py#L55) for consistency.
- LOAD: Add code to restore `expiry` back to `datetime.datetime` object when imported.
- LOAD: If `expiry` was unsaved, automatically set it as expired so refresh takes place.
- Minor `scopes` updates
- DUMP: Add property for `scopes` so `to_json()` can grab it
- LOAD: `scopes` may be saved as a string instead of a JSON array (Python list), so ensure it is Sequence[str] when imported.
diff --git a/google/oauth2/credentials.py b/google/oauth2/credentials.py
index 6e58f63..36b8f0c 100644
--- a/google/oauth2/credentials.py
+++ b/google/oauth2/credentials.py
@@ -31,6 +31,7 @@
.. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.1
"""
+from datetime import datetime
import io
import json
@@ -66,6 +67,7 @@
client_secret=None,
scopes=None,
quota_project_id=None,
+ expiry=None,
):
"""
Args:
@@ -95,6 +97,7 @@
"""
super(Credentials, self).__init__()
self.token = token
+ self.expiry = expiry
self._refresh_token = refresh_token
self._id_token = id_token
self._scopes = scopes
@@ -129,6 +132,11 @@
return self._refresh_token
@property
+ def scopes(self):
+ """Optional[str]: The OAuth 2.0 permission scopes."""
+ return self._scopes
+
+ @property
def token_uri(self):
"""Optional[str]: The OAuth 2.0 authorization server's token endpoint
URI."""
@@ -241,16 +249,30 @@
"fields {}.".format(", ".join(missing))
)
+ # access token expiry (datetime obj); auto-expire if not saved
+ expiry = info.get("expiry")
+ if expiry:
+ expiry = datetime.strptime(
+ expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S"
+ )
+ else:
+ expiry = _helpers.utcnow() - _helpers.CLOCK_SKEW
+
+ # process scopes, which needs to be a seq
+ if scopes is None and "scopes" in info:
+ scopes = info.get("scopes")
+ if isinstance(scopes, str):
+ scopes = scopes.split(" ")
+
return cls(
- None, # No access token, must be refreshed.
- refresh_token=info["refresh_token"],
- token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT,
+ token=info.get("token"),
+ refresh_token=info.get("refresh_token"),
+ token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, # always overrides
scopes=scopes,
- client_id=info["client_id"],
- client_secret=info["client_secret"],
- quota_project_id=info.get(
- "quota_project_id"
- ), # quota project may not exist
+ client_id=info.get("client_id"),
+ client_secret=info.get("client_secret"),
+ quota_project_id=info.get("quota_project_id"), # may not exist
+ expiry=expiry,
)
@classmethod
@@ -294,8 +316,10 @@
"client_secret": self.client_secret,
"scopes": self.scopes,
}
+ if self.expiry: # flatten expiry timestamp
+ prep["expiry"] = self.expiry.isoformat() + "Z"
- # Remove empty entries
+ # Remove empty entries (those which are None)
prep = {k: v for k, v in prep.items() if v is not None}
# Remove entries that explicitely need to be removed
@@ -316,7 +340,6 @@
specified, the current active account will be used.
quota_project_id (Optional[str]): The project ID used for quota
and billing.
-
"""
def __init__(self, account=None, quota_project_id=None):