Skip to content

Commit 3db70f1

Browse files
committed
Merge pull request jpadilla#41 from skion/nbf
Not-before claim validation
2 parents 31da066 + 8ea7895 commit 3db70f1

File tree

4 files changed

+60
-1
lines changed

4 files changed

+60
-1
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ JSON Web Token defines some reserved claim names and defines how they should be
7777
used. PyJWT supports these reserved claim names:
7878

7979
- "exp" (Expiration Time) Claim
80+
- "nbf" (Not Before Time) Claim
8081

8182
### Expiration Time Claim
8283

@@ -125,6 +126,7 @@ you can set a leeway of 10 seconds in order to have some margin:
125126

126127
```python
127128
import datetime
129+
import time
128130
import jwt
129131

130132
jwt_payload = jwt.encode({
@@ -138,6 +140,8 @@ time.sleep(32)
138140
jwt.decode(jwt_payload, 'secret', leeway=10)
139141
```
140142

143+
PyJWT also supports not-before validation via the [`nbf` claim](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-27#section-4.1.5) in a similar fashion.
144+
141145
## License
142146

143147
MIT

jwt/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ def verify_signature(payload, signing_input, header, signature, key='',
251251
except KeyError:
252252
raise DecodeError('Algorithm not supported')
253253

254+
if 'nbf' in payload and verify_expiration:
255+
utc_timestamp = timegm(datetime.utcnow().utctimetuple())
256+
if payload['nbf'] > (utc_timestamp + leeway):
257+
raise ExpiredSignature("Signature not yet valid")
258+
254259
if 'exp' in payload and verify_expiration:
255260
utc_timestamp = timegm(datetime.utcnow().utctimetuple())
256261

tests/test_jwt.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,22 @@ def test_decode_with_expiration(self):
297297
lambda: jwt.verify_signature(
298298
decoded_payload, signing, header, signature, secret))
299299

300+
def test_decode_with_notbefore(self):
301+
self.payload['nbf'] = utc_timestamp() + 10
302+
secret = 'secret'
303+
jwt_message = jwt.encode(self.payload, secret)
304+
305+
self.assertRaises(
306+
jwt.ExpiredSignature,
307+
lambda: jwt.decode(jwt_message, secret))
308+
309+
decoded_payload, signing, header, signature = jwt.load(jwt_message)
310+
311+
self.assertRaises(
312+
jwt.ExpiredSignature,
313+
lambda: jwt.verify_signature(
314+
decoded_payload, signing, header, signature, secret))
315+
300316
def test_decode_skip_expiration_verification(self):
301317
self.payload['exp'] = time.time() - 1
302318
secret = 'secret'
@@ -308,6 +324,17 @@ def test_decode_skip_expiration_verification(self):
308324
jwt.verify_signature(decoded_payload, signing, header,
309325
signature, secret, verify_expiration=False)
310326

327+
def test_decode_skip_notbefore_verification(self):
328+
self.payload['nbf'] = time.time() + 10
329+
secret = 'secret'
330+
jwt_message = jwt.encode(self.payload, secret)
331+
332+
jwt.decode(jwt_message, secret, verify_expiration=False)
333+
334+
decoded_payload, signing, header, signature = jwt.load(jwt_message)
335+
jwt.verify_signature(decoded_payload, signing, header,
336+
signature, secret, verify_expiration=False)
337+
311338
def test_decode_with_expiration_with_leeway(self):
312339
self.payload['exp'] = utc_timestamp() - 2
313340
secret = 'secret'
@@ -331,6 +358,29 @@ def test_decode_with_expiration_with_leeway(self):
331358
lambda: jwt.verify_signature(decoded_payload, signing,
332359
header, signature, secret, leeway=1))
333360

361+
def test_decode_with_notbefore_with_leeway(self):
362+
self.payload['nbf'] = utc_timestamp() + 10
363+
secret = 'secret'
364+
jwt_message = jwt.encode(self.payload, secret)
365+
366+
decoded_payload, signing, header, signature = jwt.load(jwt_message)
367+
368+
# With 13 seconds leeway, should be ok
369+
jwt.decode(jwt_message, secret, leeway=13)
370+
371+
jwt.verify_signature(decoded_payload, signing, header,
372+
signature, secret, leeway=13)
373+
374+
# With 1 seconds, should fail
375+
self.assertRaises(
376+
jwt.ExpiredSignature,
377+
lambda: jwt.decode(jwt_message, secret, leeway=1))
378+
379+
self.assertRaises(
380+
jwt.ExpiredSignature,
381+
lambda: jwt.verify_signature(decoded_payload, signing,
382+
header, signature, secret, leeway=1))
383+
334384
def test_encode_decode_with_rsa_sha256(self):
335385
try:
336386
from Crypto.PublicKey import RSA

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = py26, py27, py32, py33
2+
envlist = py26, py27, py32, py33, py34
33

44
[testenv]
55
deps = PyCrypto

0 commit comments

Comments
 (0)