Skip to content

Commit be65490

Browse files
otetardvimalloc
authored andcommitted
Implement the ability to define a leeway when validating JWT tokens. (#218)
PyJWT library implement a way to define a “leeway” for time validations. > PyJWT also supports the leeway part of the expiration time > definition, which means you can validate a expiration time which is > in the past but not very far. For example, if you have a JWT payload > with a expiration time set to 30 seconds after creation but you know > that sometimes you will process it after 30 seconds, you can set a > leeway of 10 seconds in order to have some margin: > > https://github.com/jpadilla/pyjwt/blob/master/docs/usage.rst This is implemented as an optional configuration setting, `JWT_DECODE_LEEWAY`.
1 parent c5f32c3 commit be65490

File tree

6 files changed

+52
-6
lines changed

6 files changed

+52
-6
lines changed

docs/options.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ General Options:
4747
``JWT_DECODE_AUDIENCE`` The audience you expect in a JWT when decoding it.
4848
If this option differs from the 'aud' claim in a JWT, the ``'invalid_token_callback'`` is invoked.
4949
Defaults to ``'None'``.
50+
``JWT_DECODE_LEEWAY`` Define the leeway part of the expiration time definition, which
51+
means you can validate an expiration time which is in the past but
52+
not very far. This leeway is used for `nbf` (“not before”) and `exp`
53+
(“expiration time”).
54+
Defaults to ``0``
5055
================================= =========================================
5156

5257

flask_jwt_extended/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,5 +292,9 @@ def json_encoder(self):
292292
def audience(self):
293293
return current_app.config['JWT_DECODE_AUDIENCE']
294294

295+
@property
296+
def leeway(self):
297+
return current_app.config['JWT_DECODE_LEEWAY']
298+
295299

296300
config = _Config()

flask_jwt_extended/jwt_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ def _set_default_configuration_options(app):
197197
app.config.setdefault('JWT_IDENTITY_CLAIM', 'identity')
198198
app.config.setdefault('JWT_USER_CLAIMS', 'user_claims')
199199
app.config.setdefault('JWT_DECODE_AUDIENCE', None)
200+
app.config.setdefault('JWT_DECODE_LEEWAY', 0)
200201

201202
app.config.setdefault('JWT_CLAIMS_IN_REFRESH_TOKEN', False)
202203

flask_jwt_extended/tokens.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ def encode_refresh_token(identity, secret, algorithm, expires_delta, user_claims
113113

114114

115115
def decode_jwt(encoded_token, secret, algorithm, identity_claim_key,
116-
user_claims_key, csrf_value=None, audience=None):
116+
user_claims_key, csrf_value=None, audience=None,
117+
leeway=0):
117118
"""
118119
Decodes an encoded JWT
119120
@@ -124,10 +125,13 @@ def decode_jwt(encoded_token, secret, algorithm, identity_claim_key,
124125
:param user_claims_key: expected key that contains the user claims
125126
:param csrf_value: Expected double submit csrf value
126127
:param audience: expected audience in the JWT
128+
:param leeway: optional leeway to add some margin around expiration times
127129
:return: Dictionary containing contents of the JWT
128130
"""
131+
129132
# This call verifies the ext, iat, nbf, and aud claims
130-
data = jwt.decode(encoded_token, secret, algorithms=[algorithm], audience=audience)
133+
data = jwt.decode(encoded_token, secret, algorithms=[algorithm], audience=audience,
134+
leeway=leeway)
131135

132136
# Make sure that any custom claims we expect in the token are present
133137
if 'jti' not in data:

flask_jwt_extended/utils.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,9 @@ def decode_token(encoded_token, csrf_value=None):
8484
secret = jwt_manager._decode_key_callback(unverified_claims, unverified_headers)
8585
except TypeError:
8686
msg = (
87-
"The single-argument (unverified_claims) form of decode_key_callback is deprecated. "
88-
"Update your code to use the two-argument form (unverified_claims, unverified_headers)."
87+
"The single-argument (unverified_claims) form of decode_key_callback ",
88+
"is deprecated. Update your code to use the two-argument form ",
89+
"(unverified_claims, unverified_headers)."
8990
)
9091
warn(msg, DeprecationWarning)
9192
secret = jwt_manager._decode_key_callback(unverified_claims)
@@ -96,7 +97,8 @@ def decode_token(encoded_token, csrf_value=None):
9697
identity_claim_key=config.identity_claim_key,
9798
user_claims_key=config.user_claims_key,
9899
csrf_value=csrf_value,
99-
audience=config.audience
100+
audience=config.audience,
101+
leeway=config.leeway
100102
)
101103

102104

tests/test_decode_tokens.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
import warnings
55

66
from flask import Flask
7-
from jwt import ExpiredSignatureError, InvalidSignatureError, InvalidAudienceError
7+
8+
from jwt import (
9+
ExpiredSignatureError, InvalidSignatureError, InvalidAudienceError,
10+
ImmatureSignatureError
11+
)
812

913
from flask_jwt_extended import (
1014
JWTManager, create_access_token, decode_token, create_refresh_token,
@@ -37,6 +41,20 @@ def default_access_token(app):
3741
}
3842

3943

44+
@pytest.fixture(scope='function')
45+
def patch_datetime_now(monkeypatch):
46+
47+
DATE_IN_FUTURE = datetime.utcnow() + timedelta(seconds=30)
48+
49+
class mydatetime(datetime):
50+
@classmethod
51+
def utcnow(cls):
52+
return DATE_IN_FUTURE
53+
54+
monkeypatch.setattr(__name__ + ".datetime", mydatetime)
55+
monkeypatch.setattr("datetime.datetime", mydatetime)
56+
57+
4058
@pytest.mark.parametrize("user_loader_return", [{}, None])
4159
def test_no_user_claims(app, user_loader_return):
4260
jwtM = get_jwt_manager(app)
@@ -107,6 +125,18 @@ def test_never_expire_token(app):
107125
assert 'exp' not in decoded
108126

109127

128+
def test_nbf_token_in_future(app, patch_datetime_now):
129+
with pytest.raises(ImmatureSignatureError):
130+
with app.test_request_context():
131+
access_token = create_access_token('username')
132+
decode_token(access_token)
133+
134+
with app.test_request_context():
135+
app.config['JWT_DECODE_LEEWAY'] = 30
136+
access_token = create_access_token('username')
137+
decode_token(access_token)
138+
139+
110140
def test_alternate_identity_claim(app, default_access_token):
111141
app.config['JWT_IDENTITY_CLAIM'] = 'sub'
112142

0 commit comments

Comments
 (0)