Skip to content
This repository was archived by the owner on Jan 18, 2025. It is now read-only.

Commit 0cef592

Browse files
committed
Avoid using expired tokens from credential storage.
This covers the case where credential storage contains an expired (but different) token, as well as the case where the token read from storage is almost expired and results in a 401 when resending the original request.
1 parent 3a742ee commit 0cef592

2 files changed

Lines changed: 68 additions & 15 deletions

File tree

oauth2client/client.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -557,14 +557,20 @@ def new_request(uri, method='GET', body=None, headers=None,
557557
resp, content = request_orig(uri, method, body, clean_headers(headers),
558558
redirections, connection_type)
559559

560-
if resp.status in REFRESH_STATUS_CODES:
561-
logger.info('Refreshing due to a %s', resp.status)
560+
# A stored token may expire between the time it is retrieved and the time
561+
# the request is made, so we may need to try twice.
562+
max_refresh_attempts = 2
563+
for refresh_attempt in range(max_refresh_attempts):
564+
if resp.status not in REFRESH_STATUS_CODES:
565+
break
566+
logger.info('Refreshing due to a %s (attempt %s/%s)', resp.status,
567+
refresh_attempt + 1, max_refresh_attempts)
562568
self._refresh(request_orig)
563569
self.apply(headers)
564-
return request_orig(uri, method, body, clean_headers(headers),
565-
redirections, connection_type)
566-
else:
567-
return (resp, content)
570+
resp, content = request_orig(uri, method, body, clean_headers(headers),
571+
redirections, connection_type)
572+
573+
return (resp, content)
568574

569575
# Replace the request method with our own closure.
570576
http.request = new_request
@@ -757,8 +763,10 @@ def _refresh(self, http_request):
757763
self.store.acquire_lock()
758764
try:
759765
new_cred = self.store.locked_get()
766+
760767
if (new_cred and not new_cred.invalid and
761-
new_cred.access_token != self.access_token):
768+
new_cred.access_token != self.access_token and
769+
not new_cred.access_token_expired):
762770
logger.info('Updated access_token read from Storage')
763771
self._updateFromCredential(new_cred)
764772
else:

tests/test_file.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,19 @@
2424

2525
import copy
2626
import datetime
27-
import httplib2
2827
import json
2928
import os
3029
import pickle
3130
import stat
3231
import tempfile
3332
import unittest
3433

35-
from oauth2client import GOOGLE_TOKEN_URI
34+
from .http_mock import HttpMockSequence
3635
from oauth2client import file
3736
from oauth2client import locked_file
3837
from oauth2client import multistore_file
3938
from oauth2client import util
4039
from oauth2client.client import AccessTokenCredentials
41-
from oauth2client.client import AssertionCredentials
4240
from oauth2client.client import OAuth2Credentials
4341
try:
4442
# Python2
@@ -64,11 +62,12 @@ def setUp(self):
6462
except OSError:
6563
pass
6664

67-
def create_test_credentials(self, client_id='some_client_id'):
65+
def create_test_credentials(self, client_id='some_client_id',
66+
expiration=None):
6867
access_token = 'foo'
6968
client_secret = 'cOuDdkfjxxnv+'
7069
refresh_token = '1/0/a.df219fjls0'
71-
token_expiry = datetime.datetime.utcnow()
70+
token_expiry = expiration or datetime.datetime.utcnow()
7271
token_uri = 'https://www.google.com/accounts/o8/oauth2/token'
7372
user_agent = 'refresh_checker/1.0'
7473

@@ -119,8 +118,55 @@ def test_pickle_and_json_interop(self):
119118
self.assertEquals(data['_class'], 'OAuth2Credentials')
120119
self.assertEquals(data['_module'], OAuth2Credentials.__module__)
121120

122-
def test_token_refresh(self):
123-
credentials = self.create_test_credentials()
121+
def test_token_refresh_store_expired(self):
122+
expiration = datetime.datetime.utcnow() - datetime.timedelta(minutes=15)
123+
credentials = self.create_test_credentials(expiration=expiration)
124+
125+
s = file.Storage(FILENAME)
126+
s.put(credentials)
127+
credentials = s.get()
128+
new_cred = copy.copy(credentials)
129+
new_cred.access_token = 'bar'
130+
s.put(new_cred)
131+
132+
access_token = '1/3w'
133+
token_response = {'access_token': access_token, 'expires_in': 3600}
134+
http = HttpMockSequence([
135+
({'status': '200'}, json.dumps(token_response).encode('utf-8')),
136+
])
137+
138+
credentials._refresh(http.request)
139+
self.assertEquals(credentials.access_token, access_token)
140+
141+
def test_token_refresh_store_expires_soon(self):
142+
# Tests the case where an access token that is valid when it is read from
143+
# the store expires before the original request succeeds.
144+
expiration = datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
145+
credentials = self.create_test_credentials(expiration=expiration)
146+
147+
s = file.Storage(FILENAME)
148+
s.put(credentials)
149+
credentials = s.get()
150+
new_cred = copy.copy(credentials)
151+
new_cred.access_token = 'bar'
152+
s.put(new_cred)
153+
154+
access_token = '1/3w'
155+
token_response = {'access_token': access_token, 'expires_in': 3600}
156+
http = HttpMockSequence([
157+
({'status': '401'}, 'Initial token expired'),
158+
({'status': '401'}, 'Store token expired'),
159+
({'status': '200'}, json.dumps(token_response).encode('utf-8')),
160+
({'status': '200'}, 'Valid response to original request')
161+
])
162+
163+
credentials.authorize(http)
164+
http.request('https://example.com')
165+
self.assertEquals(credentials.access_token, access_token)
166+
167+
def test_token_refresh_good_store(self):
168+
expiration = datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
169+
credentials = self.create_test_credentials(expiration=expiration)
124170

125171
s = file.Storage(FILENAME)
126172
s.put(credentials)
@@ -287,7 +333,6 @@ def test_multistore_file_backwards_compatibility(self):
287333

288334
self.assertEqual(credentials.access_token, stored_credentials.access_token)
289335

290-
291336
def test_multistore_file_get_all_keys(self):
292337
# start with no keys
293338
keys = multistore_file.get_all_credential_keys(FILENAME)

0 commit comments

Comments
 (0)