Skip to content

Commit 7068f12

Browse files
author
Landon Gilbert-Bland
committed
Fix jwt optional edge cases and default to more helpful error messages
refs #421
1 parent 39eba9c commit 7068f12

File tree

3 files changed

+82
-29
lines changed

3 files changed

+82
-29
lines changed

flask_jwt_extended/view_decorators.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def verify_jwt_in_request(optional=False, fresh=False, refresh=False, locations=
6767
jwt_data, jwt_header, jwt_location = _decode_jwt_from_request(
6868
locations, fresh
6969
)
70-
except (NoAuthorizationError, InvalidHeaderError):
70+
except NoAuthorizationError:
7171
if not optional:
7272
raise
7373
_request_ctx_stack.top.jwt = {}
@@ -143,34 +143,39 @@ def _decode_jwt_from_headers():
143143
# Verify we have the auth header
144144
auth_header = request.headers.get(header_name, "").strip().strip(",")
145145
if not auth_header:
146-
raise NoAuthorizationError("Missing {} Header".format(header_name))
146+
raise NoAuthorizationError(f"Missing {header_name} Header")
147147

148148
# Make sure the header is in a valid format that we are expecting, ie
149-
# <HeaderName>: <HeaderType(optional)> <JWT>
150-
jwt_header = None
151-
152-
# Check if header is comma delimited, ie
149+
# <HeaderName>: <HeaderType(optional)> <JWT>.
150+
#
151+
# Also handle the fact that the header that can be comma delimited, ie
153152
# <HeaderName>: <field> <value>, <field> <value>, etc...
154153
if header_type:
155154
field_values = split(r",\s*", auth_header)
156-
jwt_header = [s for s in field_values if s.split()[0] == header_type]
157-
if len(jwt_header) < 1 or len(jwt_header[0].split()) != 2:
158-
msg = "Bad {} header. Expected value '{} <JWT>'".format(
159-
header_name, header_type
155+
jwt_headers = [s for s in field_values if s.split()[0] == header_type]
156+
if len(jwt_headers) != 1:
157+
msg = (
158+
f"Missing '{header_type}' type in '{header_name}' header. "
159+
f"Expected '{header_name}: {header_type} <JWT>'"
160+
)
161+
raise NoAuthorizationError(msg)
162+
163+
parts = jwt_headers[0].split()
164+
if len(parts) != 2:
165+
msg = (
166+
f"Bad {header_name} header. "
167+
f"Expected '{header_name}: {header_type} <JWT>'"
160168
)
161169
raise InvalidHeaderError(msg)
162-
jwt_header = jwt_header[0]
163-
else:
164-
jwt_header = auth_header
165170

166-
parts = jwt_header.split()
167-
if not header_type:
171+
encoded_token = parts[1]
172+
else:
173+
parts = auth_header.split()
168174
if len(parts) != 1:
169-
msg = "Bad {} header. Expected value '<JWT>'".format(header_name)
175+
msg = f"Bad {header_name} header. Expected '{header_name}: <JWT>'"
170176
raise InvalidHeaderError(msg)
177+
171178
encoded_token = parts[0]
172-
else:
173-
encoded_token = parts[1]
174179

175180
return encoded_token, None
176181

tests/test_headers.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,12 @@ def test_default_headers(app):
3131
# Ensure other authorization types don't work
3232
access_headers = {"Authorization": "Basic basiccreds"}
3333
response = test_client.get("/protected", headers=access_headers)
34-
expected_json = {"msg": "Bad Authorization header. Expected value 'Bearer <JWT>'"}
35-
assert response.status_code == 422
36-
assert response.get_json() == expected_json
34+
error_msg = (
35+
"Missing 'Bearer' type in 'Authorization' header. "
36+
"Expected 'Authorization: Bearer <JWT>'"
37+
)
38+
assert response.status_code == 401
39+
assert response.get_json() == {"msg": error_msg}
3740

3841
# Ensure default headers work
3942
access_headers = {"Authorization": "Bearer {}".format(access_token)}
@@ -108,9 +111,12 @@ def test_custom_header_type(app):
108111
# Insure 'default' headers no longer work
109112
access_headers = {"Authorization": "Bearer {}".format(access_token)}
110113
response = test_client.get("/protected", headers=access_headers)
111-
expected_json = {"msg": "Bad Authorization header. Expected value 'JWT <JWT>'"}
112-
assert response.status_code == 422
113-
assert response.get_json() == expected_json
114+
error_msg = (
115+
"Missing 'JWT' type in 'Authorization' header. "
116+
"Expected 'Authorization: JWT <JWT>'"
117+
)
118+
assert response.status_code == 401
119+
assert response.get_json() == {"msg": error_msg}
114120

115121
# Insure new headers do work
116122
access_headers = {"Authorization": "JWT {}".format(access_token)}
@@ -141,7 +147,7 @@ def test_custom_header_type(app):
141147
app.config["JWT_HEADER_TYPE"] = ""
142148
access_headers = {"Authorization": "Bearer {}".format(access_token)}
143149
response = test_client.get("/protected", headers=access_headers)
144-
expected_json = {"msg": "Bad Authorization header. Expected value '<JWT>'"}
150+
expected_json = {"msg": "Bad Authorization header. Expected 'Authorization: <JWT>'"}
145151
assert response.get_json() == expected_json
146152
assert response.status_code == 422
147153

@@ -172,7 +178,7 @@ def test_header_without_jwt(app):
172178
response = test_client.get("/protected", headers=access_headers)
173179
assert response.status_code == 422
174180
assert response.get_json() == {
175-
"msg": "Bad Authorization header. Expected value 'Bearer <JWT>'"
181+
"msg": "Bad Authorization header. Expected 'Authorization: Bearer <JWT>'"
176182
}
177183

178184

tests/test_view_decorators.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,55 @@ def test_jwt_optional(app, delta_func):
178178
assert response.status_code == 422
179179
assert response.get_json() == {"msg": "Only non-refresh tokens are allowed"}
180180

181+
response = test_client.get(url, headers=make_headers(expired_token))
182+
assert response.status_code == 401
183+
assert response.get_json() == {"msg": "Token has expired"}
184+
185+
186+
def test_jwt_optional_with_no_valid_jwt(app):
187+
url = "/optional_protected"
188+
test_client = app.test_client()
189+
190+
# No auth headers
181191
response = test_client.get(url, headers=None)
182192
assert response.status_code == 200
183193
assert response.get_json() == {"foo": "bar"}
184194

185-
response = test_client.get(url, headers=make_headers(expired_token))
186-
assert response.status_code == 401
187-
assert response.get_json() == {"msg": "Token has expired"}
195+
# auth header with type that isn't configured to be checked
196+
response = test_client.get(url, headers={"Authorization": "basic creds"})
197+
assert response.status_code == 200
198+
assert response.get_json() == {"foo": "bar"}
199+
200+
# auth header with Bearer type but no JWT
201+
response = test_client.get(url, headers={"Authorization": "Bearer "})
202+
assert response.status_code == 422
203+
assert response.get_json() == {
204+
"msg": "Bad Authorization header. Expected 'Authorization: Bearer <JWT>'"
205+
}
206+
207+
# Bearer token malformed
208+
response = test_client.get(url, headers={"Authorization": "Bearer xxx"})
209+
assert response.status_code == 422
210+
assert response.get_json() == {"msg": "Not enough segments"}
211+
212+
# auth header comma seperated with no bearer token
213+
response = test_client.get(url, headers={"Authorization": "Foo 1, Bar 2, Baz, 3"})
214+
assert response.status_code == 200
215+
assert response.get_json() == {"foo": "bar"}
216+
217+
# auth header comma seperated with missing bearer token
218+
response = test_client.get(url, headers={"Authorization": "Foo 1, Bearer, Baz, 3"})
219+
assert response.status_code == 422
220+
assert response.get_json() == {
221+
"msg": "Bad Authorization header. Expected 'Authorization: Bearer <JWT>'"
222+
}
223+
224+
# Bearer token comma seperated with malformed bearer token
225+
response = test_client.get(
226+
url, headers={"Authorization": "Foo 1, Bearer 2, Baz, 3"}
227+
)
228+
assert response.status_code == 422
229+
assert response.get_json() == {"msg": "Not enough segments"}
188230

189231

190232
def test_override_jwt_location(app):

0 commit comments

Comments
 (0)