Skip to content

Commit 8c2762f

Browse files
committed
Refactor exp/iat checking in crypt.verify_signed_jwt_with_certs.
Moved check into protected function _verify_time_range.
1 parent f7f2792 commit 8c2762f

File tree

2 files changed

+144
-24
lines changed

2 files changed

+144
-24
lines changed

oauth2client/crypt.py

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ def _check_audience(payload_dict, audience):
128128
payload_dict: dict, A dictionary containing a JWT payload.
129129
audience: string or NoneType, an audience to check for in
130130
the JWT payload.
131+
132+
Raises:
133+
AppIdentityError: If there is no ``'aud'`` field in the payload
134+
dictionary but there is an ``audience`` to check.
135+
AppIdentityError: If the ``'aud'`` field in the payload dictionary
136+
does not match the ``audience``.
131137
"""
132138
if audience is None:
133139
return
@@ -141,6 +147,57 @@ def _check_audience(payload_dict, audience):
141147
(audience_in_payload, audience, payload_dict))
142148

143149

150+
def _verify_time_range(payload_dict):
151+
"""Verifies the issued at and expiration from a JWT payload.
152+
153+
Makes sure the current time (in UTC) falls between the issued at and
154+
expiration for the JWT (with some skew allowed for via
155+
``CLOCK_SKEW_SECS``).
156+
157+
Args:
158+
payload_dict: dict, A dictionary containing a JWT payload.
159+
160+
Raises:
161+
AppIdentityError: If there is no ``'iat'`` field in the payload
162+
dictionary.
163+
AppIdentityError: If there is no ``'exp'`` field in the payload
164+
dictionary.
165+
AppIdentityError: If the JWT expiration is too far in the future (i.e.
166+
if the expiration would imply a token lifetime
167+
longer than what is allowed.)
168+
AppIdentityError: If the token appears to have been issued in the
169+
future (up to clock skew).
170+
AppIdentityError: If the token appears to have expired in the past
171+
(up to clock skew).
172+
"""
173+
# Get the current time to use throughout.
174+
now = int(time.time())
175+
176+
# Make sure issued at and expiration are in the payload.
177+
issued_at = payload_dict.get('iat')
178+
if issued_at is None:
179+
raise AppIdentityError('No iat field in token: %s' % (payload_dict,))
180+
expiration = payload_dict.get('exp')
181+
if expiration is None:
182+
raise AppIdentityError('No exp field in token: %s' % (payload_dict,))
183+
184+
# Make sure the expiration gives an acceptable token lifetime.
185+
if expiration >= now + MAX_TOKEN_LIFETIME_SECS:
186+
raise AppIdentityError('exp field too far in future: %s' %
187+
(payload_dict,))
188+
189+
# Make sure (up to clock skew) that the token wasn't issued in the future.
190+
earliest = issued_at - CLOCK_SKEW_SECS
191+
if now < earliest:
192+
raise AppIdentityError('Token used too early, %d < %d: %s' %
193+
(now, earliest, payload_dict))
194+
# Make sure (up to clock skew) that the token isn't already expired.
195+
latest = expiration + CLOCK_SKEW_SECS
196+
if now > latest:
197+
raise AppIdentityError('Token used too late, %d > %d: %s' %
198+
(now, latest, payload_dict))
199+
200+
144201
def verify_signed_jwt_with_certs(jwt, certs, audience=None):
145202
"""Verify a JWT against public certs.
146203
@@ -156,7 +213,7 @@ def verify_signed_jwt_with_certs(jwt, certs, audience=None):
156213
dict, The deserialized JSON payload in the JWT.
157214
158215
Raises:
159-
AppIdentityError if any checks are failed.
216+
AppIdentityError: if any checks are failed.
160217
"""
161218
jwt = _to_bytes(jwt)
162219

@@ -175,31 +232,11 @@ def verify_signed_jwt_with_certs(jwt, certs, audience=None):
175232
except:
176233
raise AppIdentityError('Can\'t parse token: %s' % (payload_bytes,))
177234

178-
# Check signature.
235+
# Verify that the signature matches the message.
179236
_verify_signature(message_to_sign, signature, certs)
180237

181-
# Check creation timestamp.
182-
issued_at = payload_dict.get('iat')
183-
if issued_at is None:
184-
raise AppIdentityError('No iat field in token: %s' % (payload_bytes,))
185-
earliest = issued_at - CLOCK_SKEW_SECS
186-
187-
# Check expiration timestamp.
188-
now = int(time.time())
189-
expiration = payload_dict.get('exp')
190-
if expiration is None:
191-
raise AppIdentityError('No exp field in token: %s' % (payload_bytes,))
192-
if expiration >= now + MAX_TOKEN_LIFETIME_SECS:
193-
raise AppIdentityError('exp field too far in future: %s' %
194-
(payload_bytes,))
195-
latest = expiration + CLOCK_SKEW_SECS
196-
197-
if now < earliest:
198-
raise AppIdentityError('Token used too early, %d < %d: %s' %
199-
(now, earliest, payload_bytes))
200-
if now > latest:
201-
raise AppIdentityError('Token used too late, %d > %d: %s' %
202-
(now, latest, payload_bytes))
238+
# Verify the issued at and created times in the payload.
239+
_verify_time_range(payload_dict)
203240

204241
# Check audience.
205242
_check_audience(payload_dict, audience)

tests/test_crypt.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,89 @@ def test_wrong_aud(self):
175175
self.assertRaises(crypt.AppIdentityError, crypt._check_audience,
176176
payload_dict, audience2)
177177

178+
class Test__verify_time_range(unittest.TestCase):
179+
180+
def _exception_helper(self, payload_dict):
181+
exception_caught = None
182+
try:
183+
crypt._verify_time_range(payload_dict)
184+
except crypt.AppIdentityError as exc:
185+
exception_caught = exc
186+
187+
return exception_caught
188+
189+
def test_without_issued_at(self):
190+
payload_dict = {}
191+
exception_caught = self._exception_helper(payload_dict)
192+
self.assertNotEqual(exception_caught, None)
193+
self.assertTrue(str(exception_caught).startswith(
194+
'No iat field in token'))
195+
196+
def test_without_expiration(self):
197+
payload_dict = {'iat': 'iat'}
198+
exception_caught = self._exception_helper(payload_dict)
199+
self.assertNotEqual(exception_caught, None)
200+
self.assertTrue(str(exception_caught).startswith(
201+
'No exp field in token'))
202+
203+
def test_with_bad_token_lifetime(self):
204+
current_time = 123456
205+
payload_dict = {
206+
'iat': 'iat',
207+
'exp': current_time + crypt.MAX_TOKEN_LIFETIME_SECS + 1,
208+
}
209+
with mock.patch('oauth2client.crypt.time') as time:
210+
time.time = mock.MagicMock(name='time',
211+
return_value=current_time)
212+
213+
exception_caught = self._exception_helper(payload_dict)
214+
self.assertNotEqual(exception_caught, None)
215+
self.assertTrue(str(exception_caught).startswith(
216+
'exp field too far in future'))
217+
218+
def test_with_issued_at_in_future(self):
219+
current_time = 123456
220+
payload_dict = {
221+
'iat': current_time + crypt.CLOCK_SKEW_SECS + 1,
222+
'exp': current_time + crypt.MAX_TOKEN_LIFETIME_SECS - 1,
223+
}
224+
with mock.patch('oauth2client.crypt.time') as time:
225+
time.time = mock.MagicMock(name='time',
226+
return_value=current_time)
227+
228+
exception_caught = self._exception_helper(payload_dict)
229+
self.assertNotEqual(exception_caught, None)
230+
self.assertTrue(str(exception_caught).startswith(
231+
'Token used too early'))
232+
233+
def test_with_expiration_in_the_past(self):
234+
current_time = 123456
235+
payload_dict = {
236+
'iat': current_time,
237+
'exp': current_time - crypt.CLOCK_SKEW_SECS - 1,
238+
}
239+
with mock.patch('oauth2client.crypt.time') as time:
240+
time.time = mock.MagicMock(name='time',
241+
return_value=current_time)
242+
243+
exception_caught = self._exception_helper(payload_dict)
244+
self.assertNotEqual(exception_caught, None)
245+
self.assertTrue(str(exception_caught).startswith(
246+
'Token used too late'))
247+
248+
def test_success(self):
249+
current_time = 123456
250+
payload_dict = {
251+
'iat': current_time,
252+
'exp': current_time + crypt.MAX_TOKEN_LIFETIME_SECS - 1,
253+
}
254+
with mock.patch('oauth2client.crypt.time') as time:
255+
time.time = mock.MagicMock(name='time',
256+
return_value=current_time)
257+
258+
exception_caught = self._exception_helper(payload_dict)
259+
self.assertEqual(exception_caught, None)
260+
178261

179262
class _MockOrderedDict(object):
180263

0 commit comments

Comments
 (0)