Skip to content

Commit e1d51a5

Browse files
stephendwolffvimalloc
authored andcommitted
Use the order of values in JWT_TOKEN_LOCATION to set order of precedence (#256)
* Use the order of values in JWT_TOKEN_LOCATION to set order of precedence * Pep line length to make github happy * Remove redundant config check in locations loop
1 parent d4a34e5 commit e1d51a5

File tree

3 files changed

+66
-12
lines changed

3 files changed

+66
-12
lines changed

docs/options.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ General Options:
1818
``JWT_TOKEN_LOCATION`` Where to look for a JWT when processing a request. The
1919
options are ``'headers'``, ``'cookies'``, ``'query_string'``, or ``'json'``. You can pass
2020
in a sequence or a set to check more then one location, such as:
21-
``('headers', 'cookies')``. Defaults to ``['headers']``
21+
``('headers', 'cookies')``. Defaults to ``['headers']``.
22+
The order sets the precedence, so that if a valid token is
23+
found in an earlier location in this list, the request is authenticated.
2224
``JWT_ACCESS_TOKEN_EXPIRES`` How long an access token should live before it expires. This
2325
takes any value that can be safely added to a ``datetime.datetime`` object, including
2426
``datetime.timedelta``, `dateutil.relativedelta <https://dateutil.readthedocs.io/en/stable/relativedelta.html>`_,

flask_jwt_extended/view_decorators.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -247,14 +247,21 @@ def _decode_jwt_from_json(request_type):
247247
def _decode_jwt_from_request(request_type):
248248
# All the places we can get a JWT from in this request
249249
get_encoded_token_functions = []
250-
if config.jwt_in_cookies:
251-
get_encoded_token_functions.append(lambda: _decode_jwt_from_cookies(request_type))
252-
if config.jwt_in_query_string:
253-
get_encoded_token_functions.append(_decode_jwt_from_query_string)
254-
if config.jwt_in_headers:
255-
get_encoded_token_functions.append(_decode_jwt_from_headers)
256-
if config.jwt_in_json:
257-
get_encoded_token_functions.append(lambda: _decode_jwt_from_json(request_type))
250+
251+
locations = config.token_location
252+
253+
# add the functions in the order specified in JWT_TOKEN_LOCATION
254+
for location in locations:
255+
if location == 'cookies':
256+
get_encoded_token_functions.append(
257+
lambda: _decode_jwt_from_cookies(request_type))
258+
if location == 'query_string':
259+
get_encoded_token_functions.append(_decode_jwt_from_query_string)
260+
if location == 'headers':
261+
get_encoded_token_functions.append(_decode_jwt_from_headers)
262+
if location == 'json':
263+
get_encoded_token_functions.append(
264+
lambda: _decode_jwt_from_json(request_type))
258265

259266
# Try to find the token from one of these locations. It only needs to exist
260267
# in one place to be valid (not every location).

tests/test_multiple_token_locations.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ def test_json_access(app):
7373
@pytest.mark.parametrize("options", [
7474
(['cookies', 'headers'], ('Missing JWT in cookies or headers (Missing cookie '
7575
'"access_token_cookie"; Missing Authorization Header)')),
76-
(['json', 'query_string'], ('Missing JWT in json or query_string (Missing "jwt" '
77-
'query paramater; Invalid content-type. Must be '
78-
'application/json.)')),
76+
(['json', 'query_string'], ('Missing JWT in json or query_string (Invalid '
77+
'content-type. Must be application/json.; '
78+
'Missing "jwt" query paramater)')),
7979
])
8080
def test_no_jwt_in_request(app, options):
8181
token_locations, expected_err = options
@@ -84,3 +84,48 @@ def test_no_jwt_in_request(app, options):
8484
response = test_client.get('/protected')
8585
assert response.status_code == 401
8686
assert response.get_json() == {'msg': expected_err}
87+
88+
89+
@pytest.mark.parametrize("options", [
90+
(['cookies', 'headers'], 200, None, {'foo': 'bar'}),
91+
(['headers', 'cookies'], 200, None, {'foo': 'bar'}),
92+
])
93+
def test_order_of_jwt_locations_in_request(app, options):
94+
""" test order doesn't matter if at least one valid token is set"""
95+
token_locations, status_code, expected_err, expected_dict = options
96+
app.config['JWT_TOKEN_LOCATION'] = token_locations
97+
test_client = app.test_client()
98+
test_client.get('/cookie_login')
99+
response = test_client.get('/protected')
100+
101+
assert response.status_code == status_code
102+
if expected_dict:
103+
assert response.get_json() == expected_dict
104+
else:
105+
assert response.get_json() == {'msg': expected_err}
106+
107+
108+
@pytest.mark.parametrize("options", [
109+
(['cookies', 'headers'], 200, None, {'foo': 'bar'}),
110+
(['headers', 'cookies'], 422, ('Invalid header padding'), None),
111+
])
112+
def test_order_of_jwt_locations_with_one_invalid_token_in_request(app, options):
113+
""" test order doesn't matter if at least one valid token is set"""
114+
token_locations, status_code, expected_err, expected_dict = options
115+
app.config['JWT_TOKEN_LOCATION'] = token_locations
116+
test_client = app.test_client()
117+
118+
with app.test_request_context():
119+
access_token = create_access_token('username')
120+
# invalidate the token, to check token location precedence
121+
access_token = "000000{}".format(access_token[5:])
122+
access_headers = {'Authorization': 'Bearer {}'.format(access_token)}
123+
# set valid cookies
124+
test_client.get('/cookie_login')
125+
response = test_client.get('/protected', headers=access_headers)
126+
127+
assert response.status_code == status_code
128+
if expected_dict:
129+
assert response.get_json() == expected_dict
130+
else:
131+
assert response.get_json() == {'msg': expected_err}

0 commit comments

Comments
 (0)