Skip to content

Commit d0e0aba

Browse files
authored
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.
1 parent b921a0a commit d0e0aba

File tree

2 files changed

+57
-10
lines changed

2 files changed

+57
-10
lines changed

google/oauth2/credentials.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
.. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.1
3232
"""
3333

34+
from datetime import datetime
3435
import io
3536
import json
3637

@@ -66,6 +67,7 @@ def __init__(
6667
client_secret=None,
6768
scopes=None,
6869
quota_project_id=None,
70+
expiry=None,
6971
):
7072
"""
7173
Args:
@@ -95,6 +97,7 @@ def __init__(
9597
"""
9698
super(Credentials, self).__init__()
9799
self.token = token
100+
self.expiry = expiry
98101
self._refresh_token = refresh_token
99102
self._id_token = id_token
100103
self._scopes = scopes
@@ -128,6 +131,11 @@ def refresh_token(self):
128131
"""Optional[str]: The OAuth 2.0 refresh token."""
129132
return self._refresh_token
130133

134+
@property
135+
def scopes(self):
136+
"""Optional[str]: The OAuth 2.0 permission scopes."""
137+
return self._scopes
138+
131139
@property
132140
def token_uri(self):
133141
"""Optional[str]: The OAuth 2.0 authorization server's token endpoint
@@ -241,16 +249,30 @@ def from_authorized_user_info(cls, info, scopes=None):
241249
"fields {}.".format(", ".join(missing))
242250
)
243251

252+
# access token expiry (datetime obj); auto-expire if not saved
253+
expiry = info.get("expiry")
254+
if expiry:
255+
expiry = datetime.strptime(
256+
expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S"
257+
)
258+
else:
259+
expiry = _helpers.utcnow() - _helpers.CLOCK_SKEW
260+
261+
# process scopes, which needs to be a seq
262+
if scopes is None and "scopes" in info:
263+
scopes = info.get("scopes")
264+
if isinstance(scopes, str):
265+
scopes = scopes.split(" ")
266+
244267
return cls(
245-
None, # No access token, must be refreshed.
246-
refresh_token=info["refresh_token"],
247-
token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT,
268+
token=info.get("token"),
269+
refresh_token=info.get("refresh_token"),
270+
token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, # always overrides
248271
scopes=scopes,
249-
client_id=info["client_id"],
250-
client_secret=info["client_secret"],
251-
quota_project_id=info.get(
252-
"quota_project_id"
253-
), # quota project may not exist
272+
client_id=info.get("client_id"),
273+
client_secret=info.get("client_secret"),
274+
quota_project_id=info.get("quota_project_id"), # may not exist
275+
expiry=expiry,
254276
)
255277

256278
@classmethod
@@ -294,8 +316,10 @@ def to_json(self, strip=None):
294316
"client_secret": self.client_secret,
295317
"scopes": self.scopes,
296318
}
319+
if self.expiry: # flatten expiry timestamp
320+
prep["expiry"] = self.expiry.isoformat() + "Z"
297321

298-
# Remove empty entries
322+
# Remove empty entries (those which are None)
299323
prep = {k: v for k, v in prep.items() if v is not None}
300324

301325
# Remove entries that explicitely need to be removed
@@ -316,7 +340,6 @@ class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject):
316340
specified, the current active account will be used.
317341
quota_project_id (Optional[str]): The project ID used for quota
318342
and billing.
319-
320343
"""
321344

322345
def __init__(self, account=None, quota_project_id=None):

tests/oauth2/test_credentials.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,20 @@ def test_from_authorized_user_info(self):
359359
assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
360360
assert creds.scopes == scopes
361361

362+
info["scopes"] = "email" # single non-array scope from file
363+
creds = credentials.Credentials.from_authorized_user_info(info)
364+
assert creds.scopes == [info["scopes"]]
365+
366+
info["scopes"] = ["email", "profile"] # array scope from file
367+
creds = credentials.Credentials.from_authorized_user_info(info)
368+
assert creds.scopes == info["scopes"]
369+
370+
expiry = datetime.datetime(2020, 8, 14, 15, 54, 1)
371+
info["expiry"] = expiry.isoformat() + "Z"
372+
creds = credentials.Credentials.from_authorized_user_info(info)
373+
assert creds.expiry == expiry
374+
assert creds.expired
375+
362376
def test_from_authorized_user_file(self):
363377
info = AUTH_USER_INFO.copy()
364378

@@ -381,7 +395,10 @@ def test_from_authorized_user_file(self):
381395

382396
def test_to_json(self):
383397
info = AUTH_USER_INFO.copy()
398+
expiry = datetime.datetime(2020, 8, 14, 15, 54, 1)
399+
info["expiry"] = expiry.isoformat() + "Z"
384400
creds = credentials.Credentials.from_authorized_user_info(info)
401+
assert creds.expiry == expiry
385402

386403
# Test with no `strip` arg
387404
json_output = creds.to_json()
@@ -392,6 +409,7 @@ def test_to_json(self):
392409
assert json_asdict.get("client_id") == creds.client_id
393410
assert json_asdict.get("scopes") == creds.scopes
394411
assert json_asdict.get("client_secret") == creds.client_secret
412+
assert json_asdict.get("expiry") == info["expiry"]
395413

396414
# Test with a `strip` arg
397415
json_output = creds.to_json(strip=["client_secret"])
@@ -403,6 +421,12 @@ def test_to_json(self):
403421
assert json_asdict.get("scopes") == creds.scopes
404422
assert json_asdict.get("client_secret") is None
405423

424+
# Test with no expiry
425+
creds.expiry = None
426+
json_output = creds.to_json()
427+
json_asdict = json.loads(json_output)
428+
assert json_asdict.get("expiry") is None
429+
406430
def test_pickle_and_unpickle(self):
407431
creds = self.make_credentials()
408432
unpickled = pickle.loads(pickle.dumps(creds))

0 commit comments

Comments
 (0)