From bfb1bc0a5aa4c88d190758d575ea34727b901606 Mon Sep 17 00:00:00 2001 From: Benjamin Bertrand Date: Tue, 12 Dec 2017 21:29:55 +0100 Subject: [PATCH 1/2] Allow to disable expiration time claim Setting expires_delta to timedelta(0) removes the "exp" claim and creates a token that never expires. Fix issue #105 --- docs/blacklist_and_token_revoking.rst | 2 ++ docs/changing_default_behavior.rst | 14 ++++++++++++++ docs/options.rst | 3 ++- flask_jwt_extended/tokens.py | 13 ++++++++++--- flask_jwt_extended/utils.py | 10 ++++++---- tests/test_decode_tokens.py | 10 ++++++++++ 6 files changed, 44 insertions(+), 8 deletions(-) diff --git a/docs/blacklist_and_token_revoking.rst b/docs/blacklist_and_token_revoking.rst index eab94120..a0b38c82 100644 --- a/docs/blacklist_and_token_revoking.rst +++ b/docs/blacklist_and_token_revoking.rst @@ -1,3 +1,5 @@ +.. _Blacklist and Token Revoking: + Blacklist and Token Revoking ============================ diff --git a/docs/changing_default_behavior.rst b/docs/changing_default_behavior.rst index eb7fcc3d..79d831f7 100644 --- a/docs/changing_default_behavior.rst +++ b/docs/changing_default_behavior.rst @@ -68,3 +68,17 @@ You could accomplish this like such: expires = datetime.timedelta(days=365) token = create_access_token(username, expires_delta=expires) return jsonify({'token': token}), 201 + +You can even disable expiration by setting `expires_delta` to `datetime.timedelta(0)`: + +.. code-block:: python + + @app.route('/create-api-token', methods=['POST']) + @jwt_required + def create_api_token(): + username = get_jwt_identity() + expires = datetime.timedelta() + token = create_access_token(username, expires_delta=expires) + return jsonify({'token': token}), 201 + +Note that in this case, you should enable token revoking (see :ref:`Blacklist and Token Revoking`). diff --git a/docs/options.rst b/docs/options.rst index f7e072b3..ec9f6b7a 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -22,7 +22,8 @@ General Options: ``JWT_ACCESS_TOKEN_EXPIRES`` How long an access token should live before it expires. This takes a ``datetime.timedelta``, and defaults to 15 minutes ``JWT_REFRESH_TOKEN_EXPIRES`` How long a refresh token should live before it expires. This - takes a ``datetime.timedelta``, and defaults to 30 days + takes a ``datetime.timedelta``, and defaults to 30 days. + Can be set to ``datetime.timedelta(0)`` to disable expiration. ``JWT_ALGORITHM`` Which algorithm to sign the JWT with. `See here `_ for the options. Defaults to ``'HS256'``. ``JWT_SECRET_KEY`` The secret key needed for symmetric based signing algorithms, diff --git a/flask_jwt_extended/tokens.py b/flask_jwt_extended/tokens.py index e34a45d4..58c85f36 100644 --- a/flask_jwt_extended/tokens.py +++ b/flask_jwt_extended/tokens.py @@ -15,11 +15,16 @@ def _encode_jwt(additional_token_data, expires_delta, secret, algorithm): uid = str(uuid.uuid4()) now = datetime.datetime.utcnow() token_data = { - 'exp': now + expires_delta, 'iat': now, 'nbf': now, 'jti': uid, } + # If expires_delta is timedelta(0), the JWT should never expire + # and the 'exp' claim is not set. + if expires_delta: + # A timedelta object is considered to be true if and only if + # it isn't equal to timedelta(0) + token_data['exp'] = now + expires_delta token_data.update(additional_token_data) encoded_token = jwt.encode(token_data, secret, algorithm).decode('utf-8') return encoded_token @@ -35,7 +40,8 @@ def encode_access_token(identity, secret, algorithm, expires_delta, fresh, :param secret: Secret key to encode the JWT with :param algorithm: Which algorithm to encode this JWT with :param expires_delta: How far in the future this token should expire - (datetime.timedelta) + (set to timedelta(0) to disable expiration) + :type expires_delta: datetime.timedelta :param fresh: If this should be a 'fresh' token or not :param user_claims: Custom claims to include in this token. This data must be json serializable @@ -69,7 +75,8 @@ def encode_refresh_token(identity, secret, algorithm, expires_delta, csrf, :param secret: Secret key to encode the JWT with :param algorithm: Which algorithm to use for the toek :param expires_delta: How far in the future this token should expire - (datetime.timedelta) + (set to timedelta(0) to disable expiration) + :type expires_delta: datetime.timedelta :param csrf: Whether to include a csrf double submit claim in this token (boolean) :param identity_claim_key: Which key should be used to store the identity diff --git a/flask_jwt_extended/utils.py b/flask_jwt_extended/utils.py index 9402f36b..9e56a1d5 100644 --- a/flask_jwt_extended/utils.py +++ b/flask_jwt_extended/utils.py @@ -100,8 +100,9 @@ def create_access_token(identity, fresh=False, expires_delta=None): :func:`~flask_jwt_extended.fresh_jwt_required` endpoints. Defaults to `False`. :param expires_delta: A `datetime.timedelta` for how long this token should - last before it expires. If this is None, it will - use the 'JWT_ACCESS_TOKEN_EXPIRES` config value + last before it expires. Set to timedelta(0) to disable + expiration. If this is None, it will use the + 'JWT_ACCESS_TOKEN_EXPIRES` config value (see :ref:`Configuration Options`) :return: An encoded access token """ @@ -120,8 +121,9 @@ def create_refresh_token(identity, expires_delta=None): to define a callback function that will be used to pull a json serializable identity out of the object. :param expires_delta: A `datetime.timedelta` for how long this token should - last before it expires. If this is None, it will - use the 'JWT_REFRESH_TOKEN_EXPIRES` config value + last before it expires. Set to timedelta(0) to disable + expiration. If this is None, it will use the + 'JWT_REFRESH_TOKEN_EXPIRES` config value (see :ref:`Configuration Options`) :return: An encoded access token """ diff --git a/tests/test_decode_tokens.py b/tests/test_decode_tokens.py index d6af3473..15ffd931 100644 --- a/tests/test_decode_tokens.py +++ b/tests/test_decode_tokens.py @@ -84,6 +84,16 @@ def test_expired_token(app): decode_token(refresh_token) +def test_never_expire_token(app): + with app.test_request_context(): + delta = timedelta(0) + 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): + decoded = decode_token(token) + assert 'exp' not in decoded + + def test_alternate_identity_claim(app, default_access_token): app.config['JWT_IDENTITY_CLAIM'] = 'sub' From 413d17d35a5e96354a3b8babc97fb6ebaa3ceb26 Mon Sep 17 00:00:00 2001 From: Benjamin Bertrand Date: Wed, 13 Dec 2017 10:21:51 +0100 Subject: [PATCH 2/2] Use False to disable expiration This is more intuitive than timedelta(0) --- docs/changing_default_behavior.rst | 5 ++--- docs/options.rst | 5 +++-- flask_jwt_extended/config.py | 8 ++++---- flask_jwt_extended/tokens.py | 12 +++++------- flask_jwt_extended/utils.py | 4 ++-- tests/test_config.py | 16 ++++++++++++++++ tests/test_decode_tokens.py | 5 ++--- 7 files changed, 34 insertions(+), 21 deletions(-) diff --git a/docs/changing_default_behavior.rst b/docs/changing_default_behavior.rst index 79d831f7..7bb44c0e 100644 --- a/docs/changing_default_behavior.rst +++ b/docs/changing_default_behavior.rst @@ -69,7 +69,7 @@ You could accomplish this like such: token = create_access_token(username, expires_delta=expires) return jsonify({'token': token}), 201 -You can even disable expiration by setting `expires_delta` to `datetime.timedelta(0)`: +You can even disable expiration by setting `expires_delta` to `False`: .. code-block:: python @@ -77,8 +77,7 @@ You can even disable expiration by setting `expires_delta` to `datetime.timedelt @jwt_required def create_api_token(): username = get_jwt_identity() - expires = datetime.timedelta() - token = create_access_token(username, expires_delta=expires) + token = create_access_token(username, expires_delta=False) return jsonify({'token': token}), 201 Note that in this case, you should enable token revoking (see :ref:`Blacklist and Token Revoking`). diff --git a/docs/options.rst b/docs/options.rst index ec9f6b7a..b1b879d0 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -20,10 +20,11 @@ General Options: in a list 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``, and defaults to 15 minutes + takes a ``datetime.timedelta``, 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``, and defaults to 30 days. - Can be set to ``datetime.timedelta(0)`` to disable expiration. + 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'``. ``JWT_SECRET_KEY`` The secret key needed for symmetric based signing algorithms, diff --git a/flask_jwt_extended/config.py b/flask_jwt_extended/config.py index c66e3b12..0c7e21c6 100644 --- a/flask_jwt_extended/config.py +++ b/flask_jwt_extended/config.py @@ -151,15 +151,15 @@ def refresh_csrf_header_name(self): @property def access_expires(self): delta = current_app.config['JWT_ACCESS_TOKEN_EXPIRES'] - if not isinstance(delta, datetime.timedelta): - raise RuntimeError('JWT_ACCESS_TOKEN_EXPIRES must be a datetime.timedelta') + if not isinstance(delta, datetime.timedelta) and delta is not False: + raise RuntimeError('JWT_ACCESS_TOKEN_EXPIRES must be a datetime.timedelta or False') return delta @property def refresh_expires(self): delta = current_app.config['JWT_REFRESH_TOKEN_EXPIRES'] - if not isinstance(delta, datetime.timedelta): - raise RuntimeError('JWT_REFRESH_TOKEN_EXPIRES must be a datetime.timedelta') + if not isinstance(delta, datetime.timedelta) and delta is not False: + raise RuntimeError('JWT_REFRESH_TOKEN_EXPIRES must be a datetime.timedelta or False') return delta @property diff --git a/flask_jwt_extended/tokens.py b/flask_jwt_extended/tokens.py index 58c85f36..6d253f76 100644 --- a/flask_jwt_extended/tokens.py +++ b/flask_jwt_extended/tokens.py @@ -19,11 +19,9 @@ def _encode_jwt(additional_token_data, expires_delta, secret, algorithm): 'nbf': now, 'jti': uid, } - # If expires_delta is timedelta(0), the JWT should never expire + # If expires_delta is False, the JWT should never expire # and the 'exp' claim is not set. if expires_delta: - # A timedelta object is considered to be true if and only if - # it isn't equal to timedelta(0) token_data['exp'] = now + expires_delta token_data.update(additional_token_data) encoded_token = jwt.encode(token_data, secret, algorithm).decode('utf-8') @@ -40,8 +38,8 @@ def encode_access_token(identity, secret, algorithm, expires_delta, fresh, :param secret: Secret key to encode the JWT with :param algorithm: Which algorithm to encode this JWT with :param expires_delta: How far in the future this token should expire - (set to timedelta(0) to disable expiration) - :type expires_delta: datetime.timedelta + (set to False to disable expiration) + :type expires_delta: datetime.timedelta or False :param fresh: If this should be a 'fresh' token or not :param user_claims: Custom claims to include in this token. This data must be json serializable @@ -75,8 +73,8 @@ def encode_refresh_token(identity, secret, algorithm, expires_delta, csrf, :param secret: Secret key to encode the JWT with :param algorithm: Which algorithm to use for the toek :param expires_delta: How far in the future this token should expire - (set to timedelta(0) to disable expiration) - :type expires_delta: datetime.timedelta + (set to False to disable expiration) + :type expires_delta: datetime.timedelta or False :param csrf: Whether to include a csrf double submit claim in this token (boolean) :param identity_claim_key: Which key should be used to store the identity diff --git a/flask_jwt_extended/utils.py b/flask_jwt_extended/utils.py index 9e56a1d5..e5f92ae9 100644 --- a/flask_jwt_extended/utils.py +++ b/flask_jwt_extended/utils.py @@ -100,7 +100,7 @@ def create_access_token(identity, fresh=False, expires_delta=None): :func:`~flask_jwt_extended.fresh_jwt_required` endpoints. Defaults to `False`. :param expires_delta: A `datetime.timedelta` for how long this token should - last before it expires. Set to timedelta(0) to disable + last before it expires. Set to False to disable expiration. If this is None, it will use the 'JWT_ACCESS_TOKEN_EXPIRES` config value (see :ref:`Configuration Options`) @@ -121,7 +121,7 @@ def create_refresh_token(identity, expires_delta=None): to define a callback function that will be used to pull a json serializable identity out of the object. :param expires_delta: A `datetime.timedelta` for how long this token should - last before it expires. Set to timedelta(0) to disable + last before it expires. Set to False to disable expiration. If this is None, it will use the 'JWT_REFRESH_TOKEN_EXPIRES` config value (see :ref:`Configuration Options`) diff --git a/tests/test_config.py b/tests/test_config.py index 14623bb1..dd37c651 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -129,6 +129,14 @@ def test_override_configs(app): assert config.user_claims_key == 'bar' +def test_tokens_never_expire(app): + app.config['JWT_ACCESS_TOKEN_EXPIRES'] = False + app.config['JWT_REFRESH_TOKEN_EXPIRES'] = False + with app.test_request_context(): + assert config.access_expires is False + assert config.refresh_expires is False + + # noinspection PyStatementEffect def test_symmetric_secret_key(app): with app.test_request_context(): @@ -208,6 +216,14 @@ def test_invalid_config_options(app): with pytest.raises(RuntimeError): config.refresh_expires + app.config['JWT_ACCESS_TOKEN_EXPIRES'] = True + with pytest.raises(RuntimeError): + config.access_expires + + app.config['JWT_REFRESH_TOKEN_EXPIRES'] = True + with pytest.raises(RuntimeError): + config.refresh_expires + app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = 'banana' with pytest.raises(RuntimeError): config.blacklist_checks diff --git a/tests/test_decode_tokens.py b/tests/test_decode_tokens.py index 15ffd931..d75927ad 100644 --- a/tests/test_decode_tokens.py +++ b/tests/test_decode_tokens.py @@ -86,9 +86,8 @@ def test_expired_token(app): def test_never_expire_token(app): with app.test_request_context(): - delta = timedelta(0) - access_token = create_access_token('username', expires_delta=delta) - refresh_token = create_refresh_token('username', expires_delta=delta) + access_token = create_access_token('username', expires_delta=False) + refresh_token = create_refresh_token('username', expires_delta=False) for token in (access_token, refresh_token): decoded = decode_token(token) assert 'exp' not in decoded