Skip to content

Commit 076789a

Browse files
committed
relax config type check on JWT_*_TOKEN_EXPIRES
Instead of requiring JWT_ACCESS_TOKEN_EXPIRES and JWT_REFRESH_TOKEN_EXPIRES to be of type `datetime.timedelta` or `False`, checks in config.py support any value that can be successfully added to a `datetime.datetime` object. Closes #214.
1 parent 3f37e1e commit 076789a

File tree

7 files changed

+46
-23
lines changed

7 files changed

+46
-23
lines changed

docs/options.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ General Options:
2020
in a sequence or a set to check more then one location, such as:
2121
``('headers', 'cookies')``. Defaults to ``['headers']``
2222
``JWT_ACCESS_TOKEN_EXPIRES`` How long an access token should live before it expires. This
23-
takes a ``datetime.timedelta`` or an ``int`` (seconds), and defaults to 15 minutes.
23+
takes any value that can be safely added to a ``datetime.datetime`` object, including
24+
``datetime.timedelta``, `dateutil.relativedelta <https://dateutil.readthedocs.io/en/stable/relativedelta.html>`_,
25+
or an ``int`` (seconds), and defaults to 15 minutes.
2426
Can be set to ``False`` to disable expiration.
2527
``JWT_REFRESH_TOKEN_EXPIRES`` How long a refresh token should live before it expires. This
26-
takes a ``datetime.timedelta`` or ``int`` (seconds), and defaults to 30 days.
28+
takes any value that can be safely added to a ``datetime.datetime`` object, including
29+
``datetime.timedelta``, `dateutil.relativedelta <https://dateutil.readthedocs.io/en/stable/relativedelta.html>`_,
30+
or an ``int`` (seconds), and defaults to 30 days.
2731
Can be set to ``False`` to disable expiration.
2832
``JWT_ALGORITHM`` Which algorithm to sign the JWT with. `See here <https://pyjwt.readthedocs.io/en/latest/algorithms.html>`_
2933
for the options. Defaults to ``'HS256'``.

flask_jwt_extended/config.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
22
from warnings import warn
3+
from six import raise_from
34

45
# In Python 2.7 collections.abc is a part of the collections module.
56
try:
@@ -188,21 +189,29 @@ def access_expires(self):
188189
delta = current_app.config['JWT_ACCESS_TOKEN_EXPIRES']
189190
if type(delta) is int:
190191
delta = datetime.timedelta(seconds=delta)
191-
if not isinstance(delta, datetime.timedelta) and delta is not False:
192-
err = 'JWT_ACCESS_TOKEN_EXPIRES must be a ' \
193-
'datetime.timedelta, int or False'
194-
raise RuntimeError(err)
192+
if delta is not False:
193+
try:
194+
delta + datetime.datetime.now()
195+
except TypeError as e:
196+
err = (
197+
"must be able to add JWT_ACCESS_TOKEN_EXPIRES to datetime.datetime"
198+
)
199+
raise_from(RuntimeError(err), e)
195200
return delta
196201

197202
@property
198203
def refresh_expires(self):
199204
delta = current_app.config['JWT_REFRESH_TOKEN_EXPIRES']
200205
if type(delta) is int:
201206
delta = datetime.timedelta(seconds=delta)
202-
if not isinstance(delta, datetime.timedelta) and delta is not False:
203-
err = 'JWT_REFRESH_TOKEN_EXPIRES must be a ' \
204-
'datetime.timedelta, int or False'
205-
raise RuntimeError(err)
207+
if delta is not False:
208+
try:
209+
delta + datetime.datetime.now()
210+
except TypeError as e:
211+
err = (
212+
"must be able to add JWT_REFRESH_TOKEN_EXPIRES to datetime.datetime"
213+
)
214+
raise_from(RuntimeError(err), e)
206215
return delta
207216

208217
@property

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
'Werkzeug>=0.14', # Needed for SameSite cookie functionality
3333
'Flask',
3434
'PyJWT',
35+
'six',
3536
],
3637
extras_require={
3738
'asymmetric_crypto': ["cryptography >= 2.3"]

tests/test_config.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44
from datetime import timedelta
5+
from dateutil.relativedelta import relativedelta
56
from flask import Flask
67
from flask.json import JSONEncoder
78

@@ -72,7 +73,8 @@ def test_default_configs(app):
7273
assert config.error_msg_key == 'msg'
7374

7475

75-
def test_override_configs(app):
76+
@pytest.mark.parametrize("delta_func", [timedelta, relativedelta])
77+
def test_override_configs(app, delta_func):
7678
app.config['JWT_TOKEN_LOCATION'] = ['cookies', 'query_string', 'json']
7779
app.config['JWT_HEADER_NAME'] = 'TestHeader'
7880
app.config['JWT_HEADER_TYPE'] = 'TestType'
@@ -100,8 +102,8 @@ def test_override_configs(app):
100102
app.config['JWT_ACCESS_CSRF_HEADER_NAME'] = 'X-ACCESS-CSRF'
101103
app.config['JWT_REFRESH_CSRF_HEADER_NAME'] = 'X-REFRESH-CSRF'
102104

103-
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(minutes=5)
104-
app.config['JWT_REFRESH_TOKEN_EXPIRES'] = timedelta(days=5)
105+
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = delta_func(minutes=5)
106+
app.config['JWT_REFRESH_TOKEN_EXPIRES'] = delta_func(days=5)
105107
app.config['JWT_ALGORITHM'] = 'HS512'
106108

107109
app.config['JWT_BLACKLIST_ENABLED'] = True
@@ -151,8 +153,8 @@ class CustomJSONEncoder(JSONEncoder):
151153
assert config.access_csrf_header_name == 'X-ACCESS-CSRF'
152154
assert config.refresh_csrf_header_name == 'X-REFRESH-CSRF'
153155

154-
assert config.access_expires == timedelta(minutes=5)
155-
assert config.refresh_expires == timedelta(days=5)
156+
assert config.access_expires == delta_func(minutes=5)
157+
assert config.refresh_expires == delta_func(days=5)
156158
assert config.algorithm == 'HS512'
157159

158160
assert config.blacklist_enabled is True

tests/test_decode_tokens.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import jwt
22
import pytest
33
from datetime import datetime, timedelta
4+
from dateutil.relativedelta import relativedelta
45
import warnings
56

67
from flask import Flask
@@ -103,9 +104,10 @@ def test_bad_token_type(app, default_access_token):
103104
decode_token(bad_type_token)
104105

105106

106-
def test_expired_token(app):
107+
@pytest.mark.parametrize("delta_func", [timedelta, relativedelta])
108+
def test_expired_token(app, delta_func):
107109
with app.test_request_context():
108-
delta = timedelta(minutes=-5)
110+
delta = delta_func(minutes=-5)
109111
access_token = create_access_token('username', expires_delta=delta)
110112
refresh_token = create_refresh_token('username', expires_delta=delta)
111113
with pytest.raises(ExpiredSignatureError):
@@ -114,9 +116,10 @@ def test_expired_token(app):
114116
decode_token(refresh_token)
115117

116118

117-
def test_allow_expired_token(app):
119+
@pytest.mark.parametrize("delta_func", [timedelta, relativedelta])
120+
def test_allow_expired_token(app, delta_func):
118121
with app.test_request_context():
119-
delta = timedelta(minutes=-5)
122+
delta = delta_func(minutes=-5)
120123
access_token = create_access_token('username', expires_delta=delta)
121124
refresh_token = create_refresh_token('username', expires_delta=delta)
122125
for token in (access_token, refresh_token):

tests/test_view_decorators.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22
import warnings
33
from datetime import timedelta
4+
from dateutil.relativedelta import relativedelta
45
from flask import Flask, jsonify
56

67
from flask_jwt_extended import (
@@ -146,7 +147,8 @@ def test_refresh_jwt_required(app):
146147
assert response.get_json() == {'foo': 'bar'}
147148

148149

149-
def test_jwt_optional(app):
150+
@pytest.mark.parametrize("delta_func", [timedelta, relativedelta])
151+
def test_jwt_optional(app, delta_func):
150152
url = '/optional_protected'
151153

152154
test_client = app.test_client()
@@ -156,7 +158,7 @@ def test_jwt_optional(app):
156158
refresh_token = create_refresh_token('username')
157159
expired_token = create_access_token(
158160
identity='username',
159-
expires_delta=timedelta(minutes=-1)
161+
expires_delta=delta_func(minutes=-1)
160162
)
161163

162164
response = test_client.get(url, headers=make_headers(fresh_access_token))
@@ -235,12 +237,13 @@ def test_jwt_invalid_audience(app):
235237
assert response.get_json() == {'msg': 'Invalid audience'}
236238

237239

238-
def test_expired_token(app):
240+
@pytest.mark.parametrize("delta_func", [timedelta, relativedelta])
241+
def test_expired_token(app, delta_func):
239242
url = '/protected'
240243
jwtM = get_jwt_manager(app)
241244
test_client = app.test_client()
242245
with app.test_request_context():
243-
token = create_access_token('username', expires_delta=timedelta(minutes=-1))
246+
token = create_access_token('username', expires_delta=delta_func(minutes=-1))
244247

245248
# Test default response
246249
response = test_client.get(url, headers=make_headers(token))

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ deps =
1414
pytest
1515
coverage
1616
cryptography
17+
python-dateutil
1718
# TODO why does this not work?
1819
# extras =
1920
# asymmetric_crypto

0 commit comments

Comments
 (0)