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..f95a47e2 100644 --- a/flask_jwt_extended/view_decorators.py +++ b/flask_jwt_extended/view_decorators.py @@ -247,14 +247,21 @@ 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': + get_encoded_token_functions.append( + lambda: _decode_jwt_from_cookies(request_type)) + if location == 'query_string': + get_encoded_token_functions.append(_decode_jwt_from_query_string) + if location == 'headers': + get_encoded_token_functions.append(_decode_jwt_from_headers) + if location == '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}