From 7bd98ea8677c40515b982a107c98f31bc565e2bc Mon Sep 17 00:00:00 2001 From: Stephen Wolff Date: Tue, 2 Jul 2019 10:50:37 +0200 Subject: [PATCH 1/3] Use the order of values in JWT_TOKEN_LOCATION to set order of precedence --- docs/options.rst | 4 +- flask_jwt_extended/view_decorators.py | 21 +++++++---- tests/test_multiple_token_locations.py | 51 ++++++++++++++++++++++++-- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/docs/options.rst b/docs/options.rst index be57b088..6697b0e2 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -18,7 +18,9 @@ General Options: ``JWT_TOKEN_LOCATION`` Where to look for a JWT when processing a request. The options are ``'headers'``, ``'cookies'``, ``'query_string'``, or ``'json'``. You can pass in a sequence or a set to check more then one location, such as: - ``('headers', 'cookies')``. Defaults to ``['headers']`` + ``('headers', 'cookies')``. Defaults to ``['headers']``. + The order sets the precedence, so that if a valid token is + found in an earlier location in this list, the request is authenticated. ``JWT_ACCESS_TOKEN_EXPIRES`` How long an access token should live before it expires. This takes any value that can be safely added to a ``datetime.datetime`` object, including ``datetime.timedelta``, `dateutil.relativedelta `_, diff --git a/flask_jwt_extended/view_decorators.py b/flask_jwt_extended/view_decorators.py index f8ae26d0..f4ed0693 100644 --- a/flask_jwt_extended/view_decorators.py +++ b/flask_jwt_extended/view_decorators.py @@ -247,14 +247,19 @@ def _decode_jwt_from_json(request_type): def _decode_jwt_from_request(request_type): # All the places we can get a JWT from in this request get_encoded_token_functions = [] - if config.jwt_in_cookies: - get_encoded_token_functions.append(lambda: _decode_jwt_from_cookies(request_type)) - if config.jwt_in_query_string: - get_encoded_token_functions.append(_decode_jwt_from_query_string) - if config.jwt_in_headers: - get_encoded_token_functions.append(_decode_jwt_from_headers) - if config.jwt_in_json: - get_encoded_token_functions.append(lambda: _decode_jwt_from_json(request_type)) + + locations = config.token_location + + # add the functions in the order specified in JWT_TOKEN_LOCATION + for location in locations: + if location == 'cookies' and config.jwt_in_cookies: + get_encoded_token_functions.append(lambda: _decode_jwt_from_cookies(request_type)) + if location == 'query_string' and config.jwt_in_query_string: + get_encoded_token_functions.append(_decode_jwt_from_query_string) + if location == 'headers' and config.jwt_in_headers: + get_encoded_token_functions.append(_decode_jwt_from_headers) + if location == 'json' and config.jwt_in_json: + get_encoded_token_functions.append(lambda: _decode_jwt_from_json(request_type)) # Try to find the token from one of these locations. It only needs to exist # in one place to be valid (not every location). diff --git a/tests/test_multiple_token_locations.py b/tests/test_multiple_token_locations.py index a21ecf90..0361c462 100644 --- a/tests/test_multiple_token_locations.py +++ b/tests/test_multiple_token_locations.py @@ -73,9 +73,9 @@ def test_json_access(app): @pytest.mark.parametrize("options", [ (['cookies', 'headers'], ('Missing JWT in cookies or headers (Missing cookie ' '"access_token_cookie"; Missing Authorization Header)')), - (['json', 'query_string'], ('Missing JWT in json or query_string (Missing "jwt" ' - 'query paramater; Invalid content-type. Must be ' - 'application/json.)')), + (['json', 'query_string'], ('Missing JWT in json or query_string (Invalid ' + 'content-type. Must be application/json.; ' + 'Missing "jwt" query paramater)')), ]) def test_no_jwt_in_request(app, options): token_locations, expected_err = options @@ -84,3 +84,48 @@ def test_no_jwt_in_request(app, options): response = test_client.get('/protected') assert response.status_code == 401 assert response.get_json() == {'msg': expected_err} + + +@pytest.mark.parametrize("options", [ + (['cookies', 'headers'], 200, None, {'foo': 'bar'}), + (['headers', 'cookies'], 200, None, {'foo': 'bar'}), +]) +def test_order_of_jwt_locations_in_request(app, options): + """ test order doesn't matter if at least one valid token is set""" + token_locations, status_code, expected_err, expected_dict = options + app.config['JWT_TOKEN_LOCATION'] = token_locations + test_client = app.test_client() + test_client.get('/cookie_login') + response = test_client.get('/protected') + + assert response.status_code == status_code + if expected_dict: + assert response.get_json() == expected_dict + else: + assert response.get_json() == {'msg': expected_err} + + +@pytest.mark.parametrize("options", [ + (['cookies', 'headers'], 200, None, {'foo': 'bar'}), + (['headers', 'cookies'], 422, ('Invalid header padding'), None), +]) +def test_order_of_jwt_locations_with_one_invalid_token_in_request(app, options): + """ test order doesn't matter if at least one valid token is set""" + token_locations, status_code, expected_err, expected_dict = options + app.config['JWT_TOKEN_LOCATION'] = token_locations + test_client = app.test_client() + + with app.test_request_context(): + access_token = create_access_token('username') + # invalidate the token, to check token location precedence + access_token = "000000{}".format(access_token[5:]) + access_headers = {'Authorization': 'Bearer {}'.format(access_token)} + # set valid cookies + test_client.get('/cookie_login') + response = test_client.get('/protected', headers=access_headers) + + assert response.status_code == status_code + if expected_dict: + assert response.get_json() == expected_dict + else: + assert response.get_json() == {'msg': expected_err} From 6a8a6343fe46d0d310566ce536e5710b0842a065 Mon Sep 17 00:00:00 2001 From: Stephen Wolff Date: Tue, 2 Jul 2019 10:54:38 +0200 Subject: [PATCH 2/3] Pep line length to make github happy --- flask_jwt_extended/view_decorators.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/flask_jwt_extended/view_decorators.py b/flask_jwt_extended/view_decorators.py index f4ed0693..442ccd5f 100644 --- a/flask_jwt_extended/view_decorators.py +++ b/flask_jwt_extended/view_decorators.py @@ -253,13 +253,15 @@ def _decode_jwt_from_request(request_type): # add the functions in the order specified in JWT_TOKEN_LOCATION for location in locations: if location == 'cookies' and config.jwt_in_cookies: - get_encoded_token_functions.append(lambda: _decode_jwt_from_cookies(request_type)) + get_encoded_token_functions.append( + lambda: _decode_jwt_from_cookies(request_type)) if location == 'query_string' and config.jwt_in_query_string: get_encoded_token_functions.append(_decode_jwt_from_query_string) if location == 'headers' and config.jwt_in_headers: get_encoded_token_functions.append(_decode_jwt_from_headers) if location == 'json' and config.jwt_in_json: - get_encoded_token_functions.append(lambda: _decode_jwt_from_json(request_type)) + get_encoded_token_functions.append( + lambda: _decode_jwt_from_json(request_type)) # Try to find the token from one of these locations. It only needs to exist # in one place to be valid (not every location). From 35dcbe5049e3ca4cffefddee83dbbfdcabf07b40 Mon Sep 17 00:00:00 2001 From: Stephen Wolff Date: Wed, 3 Jul 2019 06:32:55 +0200 Subject: [PATCH 3/3] Remove redundant config check in locations loop --- flask_jwt_extended/view_decorators.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flask_jwt_extended/view_decorators.py b/flask_jwt_extended/view_decorators.py index 442ccd5f..f95a47e2 100644 --- a/flask_jwt_extended/view_decorators.py +++ b/flask_jwt_extended/view_decorators.py @@ -252,14 +252,14 @@ def _decode_jwt_from_request(request_type): # add the functions in the order specified in JWT_TOKEN_LOCATION for location in locations: - if location == 'cookies' and config.jwt_in_cookies: + if location == 'cookies': get_encoded_token_functions.append( lambda: _decode_jwt_from_cookies(request_type)) - if location == 'query_string' and config.jwt_in_query_string: + if location == 'query_string': get_encoded_token_functions.append(_decode_jwt_from_query_string) - if location == 'headers' and config.jwt_in_headers: + if location == 'headers': get_encoded_token_functions.append(_decode_jwt_from_headers) - if location == 'json' and config.jwt_in_json: + if location == 'json': get_encoded_token_functions.append( lambda: _decode_jwt_from_json(request_type))