From ab978ee9911d72dc3bc6bd07b0081a30a5d4e6d2 Mon Sep 17 00:00:00 2001 From: Croug Date: Thu, 5 Sep 2019 22:42:49 +0000 Subject: [PATCH 01/10] Make header reading compliant with RFC7230, section 3.2.2 --- flask_jwt_extended/view_decorators.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/flask_jwt_extended/view_decorators.py b/flask_jwt_extended/view_decorators.py index 5f8df774..ca9a3410 100644 --- a/flask_jwt_extended/view_decorators.py +++ b/flask_jwt_extended/view_decorators.py @@ -170,12 +170,24 @@ def _decode_jwt_from_headers(): header_type = config.header_type # Verify we have the auth header - jwt_header = request.headers.get(header_name, None) - if not jwt_header: + auth_header = request.headers.get(header_name, None) + if not auth_header: raise NoAuthorizationError("Missing {} Header".format(header_name)) # Make sure the header is in a valid format that we are expecting, ie # : + + field_values = auth_header.split(', |,') + + jwt_header = [s for s in field_values if s.startswith(header_type)] + if len(jwt_header < 1): + msg = "{} header does not contain type {}".format( + header_name, + header_type + ) + raise InvalidHeaderError(msg) + jwt_header = jwt_header[0] + parts = jwt_header.split() if not header_type: if len(parts) != 1: From f18a55b22745a4a4446b4be6b6f078a218d865d8 Mon Sep 17 00:00:00 2001 From: Croug Date: Thu, 5 Sep 2019 22:42:49 +0000 Subject: [PATCH 02/10] Make header reading compliant with RFC7230, section 3.2.2 --- flask_jwt_extended/view_decorators.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/flask_jwt_extended/view_decorators.py b/flask_jwt_extended/view_decorators.py index 5f8df774..ca9a3410 100644 --- a/flask_jwt_extended/view_decorators.py +++ b/flask_jwt_extended/view_decorators.py @@ -170,12 +170,24 @@ def _decode_jwt_from_headers(): header_type = config.header_type # Verify we have the auth header - jwt_header = request.headers.get(header_name, None) - if not jwt_header: + auth_header = request.headers.get(header_name, None) + if not auth_header: raise NoAuthorizationError("Missing {} Header".format(header_name)) # Make sure the header is in a valid format that we are expecting, ie # : + + field_values = auth_header.split(', |,') + + jwt_header = [s for s in field_values if s.startswith(header_type)] + if len(jwt_header < 1): + msg = "{} header does not contain type {}".format( + header_name, + header_type + ) + raise InvalidHeaderError(msg) + jwt_header = jwt_header[0] + parts = jwt_header.split() if not header_type: if len(parts) != 1: From f3f9808fc732f25113adb22d19a1f17642fc4c8e Mon Sep 17 00:00:00 2001 From: Croug Date: Fri, 6 Sep 2019 16:49:33 +0000 Subject: [PATCH 03/10] Only attempt to parse comma delimited headers if header_type specified --- flask_jwt_extended/view_decorators.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/flask_jwt_extended/view_decorators.py b/flask_jwt_extended/view_decorators.py index ca9a3410..d6d2500d 100644 --- a/flask_jwt_extended/view_decorators.py +++ b/flask_jwt_extended/view_decorators.py @@ -176,17 +176,23 @@ def _decode_jwt_from_headers(): # Make sure the header is in a valid format that we are expecting, ie # : + jwt_header = None - field_values = auth_header.split(', |,') + # Check if header is comma delimited, ie + # : , , etc... + if header_type and len(header_type.strip()) > 0: + field_values = auth_header.split(', |,') - jwt_header = [s for s in field_values if s.startswith(header_type)] - if len(jwt_header < 1): - msg = "{} header does not contain type {}".format( - header_name, - header_type - ) - raise InvalidHeaderError(msg) - jwt_header = jwt_header[0] + jwt_header = [s for s in field_values if s.startswith(header_type)] + if len(jwt_header < 1): + msg = "{} header does not contain type {}".format( + header_name, + header_type + ) + raise InvalidHeaderError(msg) + jwt_header = jwt_header[0] + else: + jwt_header = auth_header parts = jwt_header.split() if not header_type: From 56813ad2f0b639e4e6ba4756af3a4ed8b88d08fd Mon Sep 17 00:00:00 2001 From: Croug Date: Fri, 6 Sep 2019 20:11:07 +0000 Subject: [PATCH 04/10] Add unit tests for multi field headers --- flask_jwt_extended/config.py | 2 +- flask_jwt_extended/view_decorators.py | 10 ++--- tests/test_headers.py | 59 ++++++++++++++++++++++++++- tests/test_view_decorators.py | 2 +- 4 files changed, 65 insertions(+), 8 deletions(-) diff --git a/flask_jwt_extended/config.py b/flask_jwt_extended/config.py index aa60c3a8..64eea2bd 100644 --- a/flask_jwt_extended/config.py +++ b/flask_jwt_extended/config.py @@ -87,7 +87,7 @@ def header_name(self): @property def header_type(self): - return current_app.config['JWT_HEADER_TYPE'] + return current_app.config.get('JWT_HEADER_TYPE') @property def query_string_name(self): diff --git a/flask_jwt_extended/view_decorators.py b/flask_jwt_extended/view_decorators.py index d6d2500d..7da848c3 100644 --- a/flask_jwt_extended/view_decorators.py +++ b/flask_jwt_extended/view_decorators.py @@ -1,6 +1,7 @@ from functools import wraps from datetime import datetime from calendar import timegm +from re import split from werkzeug.exceptions import BadRequest @@ -180,12 +181,11 @@ def _decode_jwt_from_headers(): # Check if header is comma delimited, ie # : , , etc... - if header_type and len(header_type.strip()) > 0: - field_values = auth_header.split(', |,') - + if header_type: + field_values = split(r',\s*', auth_header) jwt_header = [s for s in field_values if s.startswith(header_type)] - if len(jwt_header < 1): - msg = "{} header does not contain type {}".format( + if len(jwt_header) < 1: + msg = "Bad {} header. Expected value '{} '".format( header_name, header_type ) diff --git a/tests/test_headers.py b/tests/test_headers.py index db3ffe46..5b11ece5 100644 --- a/tests/test_headers.py +++ b/tests/test_headers.py @@ -19,13 +19,46 @@ def access_protected(): return app +def test_default_headers(app): + app.config + test_client = app.test_client() + + with app.test_request_context(): + access_token = create_access_token('username') + + # Ensure other authorization types don't work + access_headers = {'Authorization': 'Basic basiccreds'} + response = test_client.get('/protected', headers=access_headers) + expected_json = {'msg': "Bad Authorization header. Expected value 'Bearer '"} + assert response.status_code == 422 + assert response.get_json() == expected_json + + # Ensure default headers work + access_headers = {'Authorization': 'Bearer {}'.format(access_token)} + response = test_client.get('/protected', headers=access_headers) + assert response.status_code == 200 + assert response.get_json() == {'foo': 'bar'} + + # Ensure default headers work with multiple field values + access_headers = {'Authorization': 'Bearer {}, Basic randomcredshere'.format(access_token)} + response = test_client.get('/protected', headers=access_headers) + assert response.status_code == 200 + assert response.get_json() == {'foo': 'bar'} + + # Ensure default headers work with multiple field values in any position + access_headers = {'Authorization': 'Basic randomcredshere, Bearer {}'.format(access_token)} + response = test_client.get('/protected', headers=access_headers) + assert response.status_code == 200 + assert response.get_json() == {'foo': 'bar'} + + def test_custom_header_name(app): app.config['JWT_HEADER_NAME'] = 'Foo' test_client = app.test_client() with app.test_request_context(): access_token = create_access_token('username') - + # Insure 'default' headers no longer work access_headers = {'Authorization': 'Bearer {}'.format(access_token)} response = test_client.get('/protected', headers=access_headers) @@ -38,6 +71,18 @@ def test_custom_header_name(app): assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} + # Ensure new headers work with multiple field values + access_headers = {'Foo': 'Bearer {}, Basic randomcredshere'.format(access_token)} + response = test_client.get('/protected', headers=access_headers) + assert response.status_code == 200 + assert response.get_json() == {'foo': 'bar'} + + # Ensure new headers work with multiple field values in any position + access_headers = {'Foo': 'Basic randomcredshere, Bearer {}'.format(access_token)} + response = test_client.get('/protected', headers=access_headers) + assert response.status_code == 200 + assert response.get_json() == {'foo': 'bar'} + def test_custom_header_type(app): app.config['JWT_HEADER_TYPE'] = 'JWT' @@ -58,6 +103,18 @@ def test_custom_header_type(app): response = test_client.get('/protected', headers=access_headers) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} + + # Ensure new headers work with multiple field values + access_headers = {'Authorization': 'JWT {}, Basic randomcredshere'.format(access_token)} + response = test_client.get('/protected', headers=access_headers) + assert response.status_code == 200 + assert response.get_json() == {'foo': 'bar'} + + # Ensure new headers work with multiple field values in any position + access_headers = {'Authorization': 'Basic randomcredshere, JWT {}'.format(access_token)} + response = test_client.get('/protected', headers=access_headers) + assert response.status_code == 200 + assert response.get_json() == {'foo': 'bar'} # Insure new headers without a type also work app.config['JWT_HEADER_TYPE'] = '' diff --git a/tests/test_view_decorators.py b/tests/test_view_decorators.py index 4e51d8f1..05cb7d22 100644 --- a/tests/test_view_decorators.py +++ b/tests/test_view_decorators.py @@ -360,4 +360,4 @@ def test_different_token_algorightm(app): response = test_client.get(url, headers=make_headers(token)) assert response.status_code == 422 - assert response.get_json() == {'msg': 'The specified alg value is not allowed'} + assert response.get_json() == {'msg': 'The specified alg value is not allowed'} \ No newline at end of file From cc5a93c92cbe0aa68033fbc2526186b2f4e1123e Mon Sep 17 00:00:00 2001 From: Croug Date: Fri, 6 Sep 2019 21:57:15 +0000 Subject: [PATCH 05/10] Remove redundant checks --- flask_jwt_extended/view_decorators.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/flask_jwt_extended/view_decorators.py b/flask_jwt_extended/view_decorators.py index 7da848c3..7b9be4ed 100644 --- a/flask_jwt_extended/view_decorators.py +++ b/flask_jwt_extended/view_decorators.py @@ -201,12 +201,6 @@ def _decode_jwt_from_headers(): raise InvalidHeaderError(msg) encoded_token = parts[0] else: - if parts[0] != header_type or len(parts) != 2: - msg = "Bad {} header. Expected value '{} '".format( - header_name, - header_type - ) - raise InvalidHeaderError(msg) encoded_token = parts[1] return encoded_token, None From cb437bab498053224f42e8d039a08c70a60fab72 Mon Sep 17 00:00:00 2001 From: Croug Date: Tue, 10 Sep 2019 20:39:48 +0000 Subject: [PATCH 06/10] Fix ambiguity between header fields beginning with same characters --- flask_jwt_extended/view_decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_jwt_extended/view_decorators.py b/flask_jwt_extended/view_decorators.py index 7b9be4ed..402a14d1 100644 --- a/flask_jwt_extended/view_decorators.py +++ b/flask_jwt_extended/view_decorators.py @@ -183,7 +183,7 @@ def _decode_jwt_from_headers(): # : , , etc... if header_type: field_values = split(r',\s*', auth_header) - jwt_header = [s for s in field_values if s.startswith(header_type)] + jwt_header = [s for s in field_values if s.split()[0]==header_type] if len(jwt_header) < 1: msg = "Bad {} header. Expected value '{} '".format( header_name, From 8023de6d623d89984aacd5a3a814c74e4c15463d Mon Sep 17 00:00:00 2001 From: Croug Date: Tue, 10 Sep 2019 20:48:52 +0000 Subject: [PATCH 07/10] Fix pep8 errors --- flask_jwt_extended/view_decorators.py | 2 +- tests/test_headers.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/flask_jwt_extended/view_decorators.py b/flask_jwt_extended/view_decorators.py index 402a14d1..71c72d37 100644 --- a/flask_jwt_extended/view_decorators.py +++ b/flask_jwt_extended/view_decorators.py @@ -183,7 +183,7 @@ def _decode_jwt_from_headers(): # : , , etc... if header_type: field_values = split(r',\s*', auth_header) - jwt_header = [s for s in field_values if s.split()[0]==header_type] + jwt_header = [s for s in field_values if s.split()[0] == header_type] if len(jwt_header) < 1: msg = "Bad {} header. Expected value '{} '".format( header_name, diff --git a/tests/test_headers.py b/tests/test_headers.py index 5b11ece5..1874d93b 100644 --- a/tests/test_headers.py +++ b/tests/test_headers.py @@ -25,7 +25,7 @@ def test_default_headers(app): with app.test_request_context(): access_token = create_access_token('username') - + # Ensure other authorization types don't work access_headers = {'Authorization': 'Basic basiccreds'} response = test_client.get('/protected', headers=access_headers) @@ -40,13 +40,13 @@ def test_default_headers(app): assert response.get_json() == {'foo': 'bar'} # Ensure default headers work with multiple field values - access_headers = {'Authorization': 'Bearer {}, Basic randomcredshere'.format(access_token)} + access_headers = {'Authorization': 'Bearer {}, Basic creds'.format(access_token)} response = test_client.get('/protected', headers=access_headers) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} # Ensure default headers work with multiple field values in any position - access_headers = {'Authorization': 'Basic randomcredshere, Bearer {}'.format(access_token)} + access_headers = {'Authorization': 'Basic creds, Bearer {}'.format(access_token)} response = test_client.get('/protected', headers=access_headers) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} @@ -58,7 +58,7 @@ def test_custom_header_name(app): with app.test_request_context(): access_token = create_access_token('username') - + # Insure 'default' headers no longer work access_headers = {'Authorization': 'Bearer {}'.format(access_token)} response = test_client.get('/protected', headers=access_headers) @@ -103,15 +103,15 @@ def test_custom_header_type(app): response = test_client.get('/protected', headers=access_headers) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} - + # Ensure new headers work with multiple field values - access_headers = {'Authorization': 'JWT {}, Basic randomcredshere'.format(access_token)} + access_headers = {'Authorization': 'JWT {}, Basic creds'.format(access_token)} response = test_client.get('/protected', headers=access_headers) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} # Ensure new headers work with multiple field values in any position - access_headers = {'Authorization': 'Basic randomcredshere, JWT {}'.format(access_token)} + access_headers = {'Authorization': 'Basic creds, JWT {}'.format(access_token)} response = test_client.get('/protected', headers=access_headers) assert response.status_code == 200 assert response.get_json() == {'foo': 'bar'} From 69af9b658b7d4c753a8e95bb75f3f4de339b21cb Mon Sep 17 00:00:00 2001 From: Croug Date: Tue, 10 Sep 2019 20:50:42 +0000 Subject: [PATCH 08/10] Undo unintentional change --- flask_jwt_extended/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_jwt_extended/config.py b/flask_jwt_extended/config.py index 64eea2bd..aa60c3a8 100644 --- a/flask_jwt_extended/config.py +++ b/flask_jwt_extended/config.py @@ -87,7 +87,7 @@ def header_name(self): @property def header_type(self): - return current_app.config.get('JWT_HEADER_TYPE') + return current_app.config['JWT_HEADER_TYPE'] @property def query_string_name(self): From 84b6fd86f69161c87c7751a23ac4689d8365f8dc Mon Sep 17 00:00:00 2001 From: Croug Date: Tue, 10 Sep 2019 21:00:16 +0000 Subject: [PATCH 09/10] Add newline to end of file --- tests/test_view_decorators.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_view_decorators.py b/tests/test_view_decorators.py index 05cb7d22..75dc26a4 100644 --- a/tests/test_view_decorators.py +++ b/tests/test_view_decorators.py @@ -360,4 +360,5 @@ def test_different_token_algorightm(app): response = test_client.get(url, headers=make_headers(token)) assert response.status_code == 422 - assert response.get_json() == {'msg': 'The specified alg value is not allowed'} \ No newline at end of file + assert response.get_json() == {'msg': 'The specified alg value is not allowed'} + \ No newline at end of file From cd33d546562372b8150d3faee507dc07e0756159 Mon Sep 17 00:00:00 2001 From: Croug Date: Tue, 10 Sep 2019 21:01:39 +0000 Subject: [PATCH 10/10] Fix more pep8 issues --- tests/test_view_decorators.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_view_decorators.py b/tests/test_view_decorators.py index 75dc26a4..4e51d8f1 100644 --- a/tests/test_view_decorators.py +++ b/tests/test_view_decorators.py @@ -361,4 +361,3 @@ def test_different_token_algorightm(app): response = test_client.get(url, headers=make_headers(token)) assert response.status_code == 422 assert response.get_json() == {'msg': 'The specified alg value is not allowed'} - \ No newline at end of file