From ecb18f7233c56c6e052fb3c0eb8a5bc04d05c9c9 Mon Sep 17 00:00:00 2001 From: "Ludovico O. Russo" Date: Fri, 9 Feb 2018 01:50:36 +0100 Subject: [PATCH 1/4] add test to test options method (#119) --- tests/test_options_method.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/test_options_method.py diff --git a/tests/test_options_method.py b/tests/test_options_method.py new file mode 100644 index 00000000..7047b195 --- /dev/null +++ b/tests/test_options_method.py @@ -0,0 +1,35 @@ +from flask import Flask, Blueprint +from flask_jwt_extended import JWTManager, jwt_required, create_access_token +import pytest + +@pytest.fixture(scope='function') +def app(): + app = Flask(__name__) + app.config['JWT_SECRET_KEY'] = 'secret' + JWTManager(app) + + protected_bp = Blueprint('protected', __name__) + + # This protects the entire blueprint, + # Also the OPTIONS method + @protected_bp.before_request + @jwt_required + def protect(): + pass + + @protected_bp.route('/protected', methods=["GET"]) + @jwt_required + def protected(): + return 'ok' + + app.register_blueprint(protected_bp) + return app + + +def test_access_protected_enpoint(app): + client = app.test_client() + assert client.get('/protected').status_code == 401 # ok + +def test_access_protected_enpoint_options(app): + client = app.test_client() + assert client.options('/protected').status_code == 200 # test fails From ea5fa128ee5309902d6d75361689179817448110 Mon Sep 17 00:00:00 2001 From: "Ludovico O. Russo" Date: Fri, 9 Feb 2018 01:57:38 +0100 Subject: [PATCH 2/4] fix issue #119 --- flask_jwt_extended/config.py | 6 ++++-- flask_jwt_extended/view_decorators.py | 13 +++++++------ tests/test_options_method.py | 1 - 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/flask_jwt_extended/config.py b/flask_jwt_extended/config.py index d55284b2..5434a1cf 100644 --- a/flask_jwt_extended/config.py +++ b/flask_jwt_extended/config.py @@ -236,6 +236,8 @@ def identity_claim_key(self): def user_claims_key(self): return current_app.config['JWT_USER_CLAIMS'] -config = _Config() - + @property + def exempt_methods(self): + return set(["OPTIONS"]) +config = _Config() diff --git a/flask_jwt_extended/view_decorators.py b/flask_jwt_extended/view_decorators.py index 8454b13a..b68397f0 100644 --- a/flask_jwt_extended/view_decorators.py +++ b/flask_jwt_extended/view_decorators.py @@ -32,12 +32,13 @@ def jwt_required(fn): """ @wraps(fn) def wrapper(*args, **kwargs): - jwt_data = _decode_jwt_from_request(request_type='access') - ctx_stack.top.jwt = jwt_data - if not verify_token_claims(jwt_data[config.user_claims_key]): - raise UserClaimsVerificationError('User claims verification failed') - _load_user(jwt_data[config.identity_claim_key]) - return fn(*args, **kwargs) + if request.method not in config.exempt_methods: + jwt_data = _decode_jwt_from_request(request_type='access') + ctx_stack.top.jwt = jwt_data + if not verify_token_claims(jwt_data[config.user_claims_key]): + raise UserClaimsVerificationError('User claims verification failed') + _load_user(jwt_data[config.identity_claim_key]) + return fn(*args, **kwargs) return wrapper diff --git a/tests/test_options_method.py b/tests/test_options_method.py index 7047b195..4e03d669 100644 --- a/tests/test_options_method.py +++ b/tests/test_options_method.py @@ -25,7 +25,6 @@ def protected(): app.register_blueprint(protected_bp) return app - def test_access_protected_enpoint(app): client = app.test_client() assert client.get('/protected').status_code == 401 # ok From fe5e5bf8311daa08bd7fb21dc6f443dcb30ddd48 Mon Sep 17 00:00:00 2001 From: "Ludovico O. Russo" Date: Fri, 9 Feb 2018 10:11:35 +0100 Subject: [PATCH 3/4] Ignore options also in `fresh_jwt_required` and `jwt_refresh_token_required`, improve and add tests --- flask_jwt_extended/config.py | 2 +- flask_jwt_extended/view_decorators.py | 36 +++++++++--------- tests/test_options_method.py | 55 ++++++++++++++++++--------- 3 files changed, 56 insertions(+), 37 deletions(-) diff --git a/flask_jwt_extended/config.py b/flask_jwt_extended/config.py index 5434a1cf..e2cb65e8 100644 --- a/flask_jwt_extended/config.py +++ b/flask_jwt_extended/config.py @@ -238,6 +238,6 @@ def user_claims_key(self): @property def exempt_methods(self): - return set(["OPTIONS"]) + return {"OPTIONS"} config = _Config() diff --git a/flask_jwt_extended/view_decorators.py b/flask_jwt_extended/view_decorators.py index b68397f0..fa86829b 100644 --- a/flask_jwt_extended/view_decorators.py +++ b/flask_jwt_extended/view_decorators.py @@ -38,7 +38,7 @@ def wrapper(*args, **kwargs): if not verify_token_claims(jwt_data[config.user_claims_key]): raise UserClaimsVerificationError('User claims verification failed') _load_user(jwt_data[config.identity_claim_key]) - return fn(*args, **kwargs) + return fn(*args, **kwargs) return wrapper @@ -82,19 +82,20 @@ def fresh_jwt_required(fn): """ @wraps(fn) def wrapper(*args, **kwargs): - jwt_data = _decode_jwt_from_request(request_type='access') - ctx_stack.top.jwt = jwt_data - fresh = jwt_data['fresh'] - if isinstance(fresh, bool): - if not fresh: - raise FreshTokenRequired('Fresh token required') - else: - now = timegm(datetime.utcnow().utctimetuple()) - if fresh < now: - raise FreshTokenRequired('Fresh token required') - if not verify_token_claims(jwt_data[config.user_claims_key]): - raise UserClaimsVerificationError('User claims verification failed') - _load_user(jwt_data[config.identity_claim_key]) + if request.method not in config.exempt_methods: + jwt_data = _decode_jwt_from_request(request_type='access') + ctx_stack.top.jwt = jwt_data + fresh = jwt_data['fresh'] + if isinstance(fresh, bool): + if not fresh: + raise FreshTokenRequired('Fresh token required') + else: + now = timegm(datetime.utcnow().utctimetuple()) + if fresh < now: + raise FreshTokenRequired('Fresh token required') + if not verify_token_claims(jwt_data[config.user_claims_key]): + raise UserClaimsVerificationError('User claims verification failed') + _load_user(jwt_data[config.identity_claim_key]) return fn(*args, **kwargs) return wrapper @@ -108,9 +109,10 @@ def jwt_refresh_token_required(fn): """ @wraps(fn) def wrapper(*args, **kwargs): - jwt_data = _decode_jwt_from_request(request_type='refresh') - ctx_stack.top.jwt = jwt_data - _load_user(jwt_data[config.identity_claim_key]) + if request.method not in config.exempt_methods: + jwt_data = _decode_jwt_from_request(request_type='refresh') + ctx_stack.top.jwt = jwt_data + _load_user(jwt_data[config.identity_claim_key]) return fn(*args, **kwargs) return wrapper diff --git a/tests/test_options_method.py b/tests/test_options_method.py index 4e03d669..54afd312 100644 --- a/tests/test_options_method.py +++ b/tests/test_options_method.py @@ -1,5 +1,7 @@ from flask import Flask, Blueprint -from flask_jwt_extended import JWTManager, jwt_required, create_access_token +from flask_jwt_extended import ( + JWTManager, jwt_required, fresh_jwt_required, jwt_refresh_token_required + ) import pytest @pytest.fixture(scope='function') @@ -8,27 +10,42 @@ def app(): app.config['JWT_SECRET_KEY'] = 'secret' JWTManager(app) - protected_bp = Blueprint('protected', __name__) - - # This protects the entire blueprint, - # Also the OPTIONS method - @protected_bp.before_request + @app.route('/jwt_required', methods=["GET", "OPTIONS"]) @jwt_required - def protect(): - pass + def jwt_required_endpoint(): + return b'ok' + + @app.route('/fresh_jwt_required', methods=["GET", "OPTIONS"]) + @fresh_jwt_required + def fresh_jwt_required_endpoint(): + return b'ok' + + @app.route('/jwt_refresh_token_required', methods=["GET", "OPTIONS"]) + @jwt_refresh_token_required + def jwt_refresh_token_required_endpoint(): + return b'ok' + - @protected_bp.route('/protected', methods=["GET"]) - @jwt_required - def protected(): - return 'ok' - app.register_blueprint(protected_bp) return app -def test_access_protected_enpoint(app): - client = app.test_client() - assert client.get('/protected').status_code == 401 # ok +def test_access_jwt_required_enpoint(app): + # Test the options method shoud not be + # affected by jwt required + res = app.test_client().options('/jwt_required') + assert res.status_code == 200 + assert res.data == b'ok' + +def test_access_jwt_refresh_token_required_enpoint(app): + # Test the options method shoud not be + # affected by jwt required + res = app.test_client().options('/jwt_refresh_token_required') + assert res.status_code == 200 + assert res.data == b'ok' -def test_access_protected_enpoint_options(app): - client = app.test_client() - assert client.options('/protected').status_code == 200 # test fails +def test_access_fresh_jwt_required_enpoint(app): + # Test the options method shoud not be + # affected by jwt required + res = app.test_client().options('/fresh_jwt_required') + assert res.status_code == 200 + assert res.data == b'ok' From 4ad8d642cc48f11d8d3f23d364e1c5d86417cb7f Mon Sep 17 00:00:00 2001 From: "Ludovico O. Russo" Date: Fri, 9 Feb 2018 10:23:33 +0100 Subject: [PATCH 4/4] this should solve Non-ASCII character '\xc2' error --- tests/test_options_method.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_options_method.py b/tests/test_options_method.py index 54afd312..2527c464 100644 --- a/tests/test_options_method.py +++ b/tests/test_options_method.py @@ -30,22 +30,16 @@ def jwt_refresh_token_required_endpoint(): return app def test_access_jwt_required_enpoint(app): - # Test the options method shoud not be - # affected by jwt required res = app.test_client().options('/jwt_required') assert res.status_code == 200 assert res.data == b'ok' def test_access_jwt_refresh_token_required_enpoint(app): - # Test the options method shoud not be - # affected by jwt required res = app.test_client().options('/jwt_refresh_token_required') assert res.status_code == 200 assert res.data == b'ok' def test_access_fresh_jwt_required_enpoint(app): - # Test the options method shoud not be - # affected by jwt required res = app.test_client().options('/fresh_jwt_required') assert res.status_code == 200 assert res.data == b'ok'