diff --git a/docs/options.rst b/docs/options.rst index 754769cb..3fe7d6cd 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -20,10 +20,14 @@ General Options: in a sequence or a set to check more then one location, such as: ``('headers', 'cookies')``. Defaults to ``['headers']`` ``JWT_ACCESS_TOKEN_EXPIRES`` How long an access token should live before it expires. This - takes a ``datetime.timedelta`` or an ``int`` (seconds), and defaults to 15 minutes. + takes any value that can be safely added to a ``datetime.datetime`` object, including + ``datetime.timedelta``, `dateutil.relativedelta `_, + or an ``int`` (seconds), and defaults to 15 minutes. Can be set to ``False`` to disable expiration. ``JWT_REFRESH_TOKEN_EXPIRES`` How long a refresh token should live before it expires. This - takes a ``datetime.timedelta`` or ``int`` (seconds), and defaults to 30 days. + takes any value that can be safely added to a ``datetime.datetime`` object, including + ``datetime.timedelta``, `dateutil.relativedelta `_, + or an ``int`` (seconds), and defaults to 30 days. Can be set to ``False`` to disable expiration. ``JWT_ALGORITHM`` Which algorithm to sign the JWT with. `See here `_ for the options. Defaults to ``'HS256'``. diff --git a/flask_jwt_extended/config.py b/flask_jwt_extended/config.py index 2b2d36b6..7de6c866 100644 --- a/flask_jwt_extended/config.py +++ b/flask_jwt_extended/config.py @@ -1,5 +1,6 @@ import datetime from warnings import warn +from six import raise_from # In Python 2.7 collections.abc is a part of the collections module. try: @@ -188,10 +189,14 @@ def access_expires(self): delta = current_app.config['JWT_ACCESS_TOKEN_EXPIRES'] if type(delta) is int: delta = datetime.timedelta(seconds=delta) - if not isinstance(delta, datetime.timedelta) and delta is not False: - err = 'JWT_ACCESS_TOKEN_EXPIRES must be a ' \ - 'datetime.timedelta, int or False' - raise RuntimeError(err) + if delta is not False: + try: + delta + datetime.datetime.now() + except TypeError as e: + err = ( + "must be able to add JWT_ACCESS_TOKEN_EXPIRES to datetime.datetime" + ) + raise_from(RuntimeError(err), e) return delta @property @@ -199,10 +204,14 @@ def refresh_expires(self): delta = current_app.config['JWT_REFRESH_TOKEN_EXPIRES'] if type(delta) is int: delta = datetime.timedelta(seconds=delta) - if not isinstance(delta, datetime.timedelta) and delta is not False: - err = 'JWT_REFRESH_TOKEN_EXPIRES must be a ' \ - 'datetime.timedelta, int or False' - raise RuntimeError(err) + if delta is not False: + try: + delta + datetime.datetime.now() + except TypeError as e: + err = ( + "must be able to add JWT_REFRESH_TOKEN_EXPIRES to datetime.datetime" + ) + raise_from(RuntimeError(err), e) return delta @property diff --git a/setup.py b/setup.py index f601157e..02308434 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ 'Werkzeug>=0.14', # Needed for SameSite cookie functionality 'Flask', 'PyJWT', + 'six', ], extras_require={ 'asymmetric_crypto': ["cryptography >= 2.3"] diff --git a/tests/test_config.py b/tests/test_config.py index f680b8c3..b189afd3 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,6 +2,7 @@ import pytest from datetime import timedelta +from dateutil.relativedelta import relativedelta from flask import Flask from flask.json import JSONEncoder @@ -72,7 +73,8 @@ def test_default_configs(app): assert config.error_msg_key == 'msg' -def test_override_configs(app): +@pytest.mark.parametrize("delta_func", [timedelta, relativedelta]) +def test_override_configs(app, delta_func): app.config['JWT_TOKEN_LOCATION'] = ['cookies', 'query_string', 'json'] app.config['JWT_HEADER_NAME'] = 'TestHeader' app.config['JWT_HEADER_TYPE'] = 'TestType' @@ -100,8 +102,8 @@ def test_override_configs(app): app.config['JWT_ACCESS_CSRF_HEADER_NAME'] = 'X-ACCESS-CSRF' app.config['JWT_REFRESH_CSRF_HEADER_NAME'] = 'X-REFRESH-CSRF' - app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(minutes=5) - app.config['JWT_REFRESH_TOKEN_EXPIRES'] = timedelta(days=5) + app.config['JWT_ACCESS_TOKEN_EXPIRES'] = delta_func(minutes=5) + app.config['JWT_REFRESH_TOKEN_EXPIRES'] = delta_func(days=5) app.config['JWT_ALGORITHM'] = 'HS512' app.config['JWT_BLACKLIST_ENABLED'] = True @@ -151,8 +153,8 @@ class CustomJSONEncoder(JSONEncoder): assert config.access_csrf_header_name == 'X-ACCESS-CSRF' assert config.refresh_csrf_header_name == 'X-REFRESH-CSRF' - assert config.access_expires == timedelta(minutes=5) - assert config.refresh_expires == timedelta(days=5) + assert config.access_expires == delta_func(minutes=5) + assert config.refresh_expires == delta_func(days=5) assert config.algorithm == 'HS512' assert config.blacklist_enabled is True diff --git a/tests/test_decode_tokens.py b/tests/test_decode_tokens.py index cbb513cb..fe5ef516 100644 --- a/tests/test_decode_tokens.py +++ b/tests/test_decode_tokens.py @@ -1,6 +1,7 @@ import jwt import pytest from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta import warnings from flask import Flask @@ -103,9 +104,10 @@ def test_bad_token_type(app, default_access_token): decode_token(bad_type_token) -def test_expired_token(app): +@pytest.mark.parametrize("delta_func", [timedelta, relativedelta]) +def test_expired_token(app, delta_func): with app.test_request_context(): - delta = timedelta(minutes=-5) + delta = delta_func(minutes=-5) access_token = create_access_token('username', expires_delta=delta) refresh_token = create_refresh_token('username', expires_delta=delta) with pytest.raises(ExpiredSignatureError): @@ -114,9 +116,10 @@ def test_expired_token(app): decode_token(refresh_token) -def test_allow_expired_token(app): +@pytest.mark.parametrize("delta_func", [timedelta, relativedelta]) +def test_allow_expired_token(app, delta_func): with app.test_request_context(): - delta = timedelta(minutes=-5) + delta = delta_func(minutes=-5) access_token = create_access_token('username', expires_delta=delta) refresh_token = create_refresh_token('username', expires_delta=delta) for token in (access_token, refresh_token): diff --git a/tests/test_view_decorators.py b/tests/test_view_decorators.py index 8ec65413..0b4d41fb 100644 --- a/tests/test_view_decorators.py +++ b/tests/test_view_decorators.py @@ -1,6 +1,7 @@ import pytest import warnings from datetime import timedelta +from dateutil.relativedelta import relativedelta from flask import Flask, jsonify from flask_jwt_extended import ( @@ -146,7 +147,8 @@ def test_refresh_jwt_required(app): assert response.get_json() == {'foo': 'bar'} -def test_jwt_optional(app): +@pytest.mark.parametrize("delta_func", [timedelta, relativedelta]) +def test_jwt_optional(app, delta_func): url = '/optional_protected' test_client = app.test_client() @@ -156,7 +158,7 @@ def test_jwt_optional(app): refresh_token = create_refresh_token('username') expired_token = create_access_token( identity='username', - expires_delta=timedelta(minutes=-1) + expires_delta=delta_func(minutes=-1) ) response = test_client.get(url, headers=make_headers(fresh_access_token)) @@ -235,12 +237,13 @@ def test_jwt_invalid_audience(app): assert response.get_json() == {'msg': 'Invalid audience'} -def test_expired_token(app): +@pytest.mark.parametrize("delta_func", [timedelta, relativedelta]) +def test_expired_token(app, delta_func): url = '/protected' jwtM = get_jwt_manager(app) test_client = app.test_client() with app.test_request_context(): - token = create_access_token('username', expires_delta=timedelta(minutes=-1)) + token = create_access_token('username', expires_delta=delta_func(minutes=-1)) # Test default response response = test_client.get(url, headers=make_headers(token)) diff --git a/tox.ini b/tox.ini index 63fd0caf..9650a9a8 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,7 @@ deps = pytest coverage cryptography + python-dateutil # TODO why does this not work? # extras = # asymmetric_crypto