diff --git a/flask_jwt_extended/__init__.py b/flask_jwt_extended/__init__.py index a451d5eb..235c80e9 100644 --- a/flask_jwt_extended/__init__.py +++ b/flask_jwt_extended/__init__.py @@ -5,8 +5,9 @@ from .utils import ( create_refresh_token, create_access_token, get_jwt_identity, get_jwt_claims, set_access_cookies, set_refresh_cookies, - unset_jwt_cookies, get_raw_jwt, get_current_user, current_user, - get_jti, decode_token, get_csrf_token + unset_jwt_cookies, unset_access_cookies, unset_refresh_cookies, + get_raw_jwt, get_current_user, current_user, get_jti, decode_token, + get_csrf_token ) __version__ = '3.8.1' diff --git a/flask_jwt_extended/utils.py b/flask_jwt_extended/utils.py index feb09999..1e13f35c 100644 --- a/flask_jwt_extended/utils.py +++ b/flask_jwt_extended/utils.py @@ -281,18 +281,23 @@ def unset_jwt_cookies(response): :param response: The Flask response object to delete the JWT cookies in. """ + unset_access_cookies(response) + unset_refresh_cookies(response) + + +def unset_access_cookies(response): + """ + takes a flask response object, and configures it to unset (delete) the + access token from the response cookies. if `jwt_csrf_in_cookies` + (see :ref:`configuration options`) is `true`, this will also remove the + access csrf double submit value from the response cookies as well. + + :param response: the flask response object to delete the jwt cookies in. + """ if not config.jwt_in_cookies: raise RuntimeWarning("unset_refresh_cookies() called without " "'JWT_TOKEN_LOCATION' configured to use cookies") - response.set_cookie(config.refresh_cookie_name, - value='', - expires=0, - secure=config.cookie_secure, - httponly=True, - domain=config.cookie_domain, - path=config.refresh_cookie_path, - samesite=config.cookie_samesite) response.set_cookie(config.access_cookie_name, value='', expires=0, @@ -303,19 +308,44 @@ def unset_jwt_cookies(response): samesite=config.cookie_samesite) if config.csrf_protect and config.csrf_in_cookies: - response.set_cookie(config.refresh_csrf_cookie_name, + response.set_cookie(config.access_csrf_cookie_name, value='', expires=0, secure=config.cookie_secure, httponly=False, domain=config.cookie_domain, - path=config.refresh_csrf_cookie_path, + path=config.access_csrf_cookie_path, samesite=config.cookie_samesite) - response.set_cookie(config.access_csrf_cookie_name, + + +def unset_refresh_cookies(response): + """ + takes a flask response object, and configures it to unset (delete) the + refresh token from the response cookies. if `jwt_csrf_in_cookies` + (see :ref:`configuration options`) is `true`, this will also remove the + refresh csrf double submit value from the response cookies as well. + + :param response: the flask response object to delete the jwt cookies in. + """ + if not config.jwt_in_cookies: + raise RuntimeWarning("unset_refresh_cookies() called without " + "'JWT_TOKEN_LOCATION' configured to use cookies") + + response.set_cookie(config.refresh_cookie_name, + value='', + expires=0, + secure=config.cookie_secure, + httponly=True, + domain=config.cookie_domain, + path=config.refresh_cookie_path, + samesite=config.cookie_samesite) + + if config.csrf_protect and config.csrf_in_cookies: + response.set_cookie(config.refresh_csrf_cookie_name, value='', expires=0, secure=config.cookie_secure, httponly=False, domain=config.cookie_domain, - path=config.access_csrf_cookie_path, + path=config.refresh_csrf_cookie_path, samesite=config.cookie_samesite) diff --git a/tests/test_cookies.py b/tests/test_cookies.py index 72da4572..a54ffca5 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -4,7 +4,7 @@ from flask_jwt_extended import ( jwt_required, JWTManager, jwt_refresh_token_required, create_access_token, create_refresh_token, set_access_cookies, set_refresh_cookies, - unset_jwt_cookies, jwt_optional + unset_jwt_cookies, unset_access_cookies, unset_refresh_cookies, jwt_optional ) def _get_cookie_from_response(response, cookie_name): @@ -46,6 +46,18 @@ def delete_tokens(): unset_jwt_cookies(resp) return resp + @app.route('/delete_access_tokens', methods=['GET']) + def delete_access_tokens(): + resp = jsonify(access_revoked=True) + unset_access_cookies(resp) + return resp + + @app.route('/delete_refresh_tokens', methods=['GET']) + def delete_refresh_tokens(): + resp = jsonify(refresh_revoked=True) + unset_refresh_cookies(resp) + return resp + @app.route('/protected', methods=['GET']) @jwt_required def protected(): @@ -75,12 +87,12 @@ def optional_post_protected(): @pytest.mark.parametrize("options", [ - ('/refresh_token', 'refresh_token_cookie', '/refresh_protected'), - ('/access_token', 'access_token_cookie', '/protected') + ('/refresh_token', 'refresh_token_cookie', '/refresh_protected', '/delete_refresh_tokens'), + ('/access_token', 'access_token_cookie', '/protected', '/delete_access_tokens') ]) def test_jwt_refresh_required_with_cookies(app, options): test_client = app.test_client() - auth_url, cookie_name, protected_url = options + auth_url, cookie_name, protected_url, delete_url = options # Test without cookies response = test_client.get(protected_url) @@ -94,7 +106,17 @@ def test_jwt_refresh_required_with_cookies(app, options): assert response.get_json() == {'foo': 'bar'} # Test after issuing a 'logout' to delete the cookies - test_client.get('/delete_tokens') + test_client.get(delete_url) + response = test_client.get(protected_url) + assert response.status_code == 401 + assert response.get_json() == {'msg': 'Missing cookie "{}"'.format(cookie_name)} + + # log back in once more to test that clearing all tokens works + test_client.get(auth_url) + response = test_client.get(protected_url) + assert response.status_code == 200 + + test_client.get("/delete_tokens") response = test_client.get(protected_url) assert response.status_code == 401 assert response.get_json() == {'msg': 'Missing cookie "{}"'.format(cookie_name)} @@ -217,6 +239,10 @@ def test_setting_cookies_wihout_cookies_enabled(app): assert response.status_code == 500 response = test_client.get('/delete_tokens') assert response.status_code == 500 + response = test_client.get('/delete_access_tokens') + assert response.status_code == 500 + response = test_client.get('/delete_refresh_tokens') + assert response.status_code == 500 def test_default_cookie_options(app):