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..7bb44c0e 100644 --- a/docs/changing_default_behavior.rst +++ b/docs/changing_default_behavior.rst @@ -68,3 +68,16 @@ 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 `False`: + +.. code-block:: python + + @app.route('/create-api-token', methods=['POST']) + @jwt_required + def create_api_token(): + username = get_jwt_identity() + 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 f7e072b3..b1b879d0 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -20,9 +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 + takes a ``datetime.timedelta``, 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'``. ``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 e34a45d4..6d253f76 100644 --- a/flask_jwt_extended/tokens.py +++ b/flask_jwt_extended/tokens.py @@ -15,11 +15,14 @@ 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 False, the JWT should never expire + # and the 'exp' claim is not set. + if expires_delta: + 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 +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 - (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 @@ -69,7 +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 - (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 9402f36b..e5f92ae9 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 False 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 False 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_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 d6af3473..d75927ad 100644 --- a/tests/test_decode_tokens.py +++ b/tests/test_decode_tokens.py @@ -84,6 +84,15 @@ def test_expired_token(app): decode_token(refresh_token) +def test_never_expire_token(app): + with app.test_request_context(): + 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 + + def test_alternate_identity_claim(app, default_access_token): app.config['JWT_IDENTITY_CLAIM'] = 'sub'