Skip to content

Commit aa1c7c2

Browse files
colevscodevimalloc
authored andcommitted
adding csrf check form functionality (#269)
1 parent 574a7dd commit aa1c7c2

File tree

6 files changed

+109
-4
lines changed

6 files changed

+109
-4
lines changed

docs/options.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,14 @@ These are only applicable if ``JWT_TOKEN_LOCATION`` is set to use cookies and
157157
Only applicable if ``JWT_CSRF_IN_COOKIES`` is ``True``
158158
``JWT_REFRESH_CSRF_COOKIE_PATH`` Path of the CSRF refresh cookie. Defaults to ``'/'``.
159159
Only applicable if ``JWT_CSRF_IN_COOKIES`` is ``True``
160+
``JWT_CSRF_CHECK_FORM`` When no CSRF token can be found in the header, check the form data. Defaults to
161+
``False``.
162+
``JWT_ACCESS_CSRF_FIELD_NAME`` Name of the form field that should contain the CSRF double submit value for access
163+
tokens when no header is present. Only applicable if ``JWT_CSRF_CHECK_FORM`` is
164+
``True``. Defaults to ``'csrf_token'``.
165+
``JWT_REFRESH_CSRF_FIELD_NAME`` Name of the form field that should contain the CSRF double submit value for refresh
166+
tokens when no header is present. Only applicable if ``JWT_CSRF_CHECK_FORM`` is
167+
``True``. Defaults to ``'csrf_token'``.
160168
================================= =========================================
161169

162170

docs/tokens_in_cookies.rst

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,39 @@ to the caller, like such:
7878
set_access_cookies(resp, access_token)
7979
set_refresh_cookies(resp, refresh_token)
8080
return resp, 200
81+
82+
83+
Typically JWT is used with API servers using JSON payloads, often via AJAX. However you may have an endpoint that
84+
receives POST requests directly from an HTML form. Without AJAX, you can't set the CSRF headers to pass your token to
85+
the server. In this scenario you can send the token in a hidden form field. To accomplish this, first configure JWT to
86+
check the form for CSRF tokens. Now it's not necessary to send the csrf in a separate cookie, you can render it
87+
directly into your HTML template:
88+
89+
90+
.. code-block:: python
91+
92+
app.config['JWT_CSRF_CHECK_FORM'] = True
93+
94+
...
95+
96+
@app.route('/protected', methods=['GET', 'POST'])
97+
@jwt_optional
98+
def protected():
99+
if request.method == "GET":
100+
return render_template(
101+
"form.html", csrf_token=(get_raw_jwt() or {}).get("csrf")
102+
)
103+
else:
104+
# handle POST request
105+
current_user = get_jwt_identity()
106+
107+
108+
In the HTML template, pass the token back to the server via a hidden input.
109+
110+
.. code-block:: html
111+
112+
<form method="POST">
113+
...
114+
<input name="csrf_token" type="hidden" value="{{ csrf_token }}">
115+
<button>Submit</button>
116+
</form>

flask_jwt_extended/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,18 @@ def refresh_csrf_header_name(self):
184184
return self._get_depreciated_csrf_header_name() or \
185185
current_app.config['JWT_REFRESH_CSRF_HEADER_NAME']
186186

187+
@property
188+
def csrf_check_form(self):
189+
return current_app.config['JWT_CSRF_CHECK_FORM']
190+
191+
@property
192+
def access_csrf_field_name(self):
193+
return current_app.config['JWT_ACCESS_CSRF_FIELD_NAME']
194+
195+
@property
196+
def refresh_csrf_field_name(self):
197+
return current_app.config['JWT_REFRESH_CSRF_FIELD_NAME']
198+
187199
@property
188200
def access_expires(self):
189201
delta = current_app.config['JWT_ACCESS_TOKEN_EXPIRES']

flask_jwt_extended/jwt_manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ def _set_default_configuration_options(app):
196196
app.config.setdefault('JWT_REFRESH_CSRF_COOKIE_NAME', 'csrf_refresh_token')
197197
app.config.setdefault('JWT_ACCESS_CSRF_COOKIE_PATH', '/')
198198
app.config.setdefault('JWT_REFRESH_CSRF_COOKIE_PATH', '/')
199+
app.config.setdefault('JWT_CSRF_CHECK_FORM', False)
200+
app.config.setdefault('JWT_ACCESS_CSRF_FIELD_NAME', 'csrf_token')
201+
app.config.setdefault('JWT_REFRESH_CSRF_FIELD_NAME', 'csrf_token')
199202

200203
# How long an a token will live before they expire.
201204
app.config.setdefault('JWT_ACCESS_TOKEN_EXPIRES', datetime.timedelta(minutes=15))

flask_jwt_extended/view_decorators.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,18 +198,22 @@ def _decode_jwt_from_cookies(request_type):
198198
if request_type == 'access':
199199
cookie_key = config.access_cookie_name
200200
csrf_header_key = config.access_csrf_header_name
201+
csrf_field_key = config.access_csrf_field_name
201202
else:
202203
cookie_key = config.refresh_cookie_name
203204
csrf_header_key = config.refresh_csrf_header_name
205+
csrf_field_key = config.refresh_csrf_field_name
204206

205207
encoded_token = request.cookies.get(cookie_key)
206208
if not encoded_token:
207209
raise NoAuthorizationError('Missing cookie "{}"'.format(cookie_key))
208210

209211
if config.csrf_protect and request.method in config.csrf_request_methods:
210212
csrf_value = request.headers.get(csrf_header_key, None)
213+
if not csrf_value and config.csrf_check_form:
214+
csrf_value = request.form.get(csrf_field_key, None)
211215
if not csrf_value:
212-
raise CSRFError("Missing CSRF token in headers")
216+
raise CSRFError("Missing CSRF token")
213217
else:
214218
csrf_value = None
215219

tests/test_cookies.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def test_default_access_csrf_protection(app, options):
139139
# Test you cannot post without the additional csrf protection
140140
response = test_client.post(post_url)
141141
assert response.status_code == 401
142-
assert response.get_json() == {'msg': 'Missing CSRF token in headers'}
142+
assert response.get_json() == {'msg': 'Missing CSRF token'}
143143

144144
# Test that you can post with the csrf double submit value
145145
csrf_headers = {'X-CSRF-TOKEN': csrf_token}
@@ -201,6 +201,48 @@ def test_csrf_with_custom_header_names(app, options):
201201
assert response.get_json() == {'foo': 'bar'}
202202

203203

204+
@pytest.mark.parametrize("options", [
205+
('/refresh_token', 'csrf_refresh_token', '/post_refresh_protected'),
206+
('/access_token', 'csrf_access_token', '/post_protected')
207+
])
208+
def test_csrf_with_default_form_field(app, options):
209+
app.config['JWT_CSRF_CHECK_FORM'] = True
210+
test_client = app.test_client()
211+
auth_url, csrf_cookie_name, post_url = options
212+
213+
# Get the jwt cookies and csrf double submit tokens
214+
response = test_client.get(auth_url)
215+
csrf_token = _get_cookie_from_response(response, csrf_cookie_name)[csrf_cookie_name]
216+
217+
# Test that you can post with the csrf double submit value
218+
csrf_data = {'csrf_token': csrf_token}
219+
response = test_client.post(post_url, data=csrf_data)
220+
assert response.status_code == 200
221+
assert response.get_json() == {'foo': 'bar'}
222+
223+
224+
@pytest.mark.parametrize("options", [
225+
('/refresh_token', 'csrf_refresh_token', '/post_refresh_protected'),
226+
('/access_token', 'csrf_access_token', '/post_protected')
227+
])
228+
def test_csrf_with_custom_form_field(app, options):
229+
app.config['JWT_CSRF_CHECK_FORM'] = True
230+
app.config['JWT_ACCESS_CSRF_FIELD_NAME'] = 'FOO'
231+
app.config['JWT_REFRESH_CSRF_FIELD_NAME'] = 'FOO'
232+
test_client = app.test_client()
233+
auth_url, csrf_cookie_name, post_url = options
234+
235+
# Get the jwt cookies and csrf double submit tokens
236+
response = test_client.get(auth_url)
237+
csrf_token = _get_cookie_from_response(response, csrf_cookie_name)[csrf_cookie_name]
238+
239+
# Test that you can post with the csrf double submit value
240+
csrf_data = {'FOO': csrf_token}
241+
response = test_client.post(post_url, data=csrf_data)
242+
assert response.status_code == 200
243+
assert response.get_json() == {'foo': 'bar'}
244+
245+
204246
@pytest.mark.parametrize("options", [
205247
('/refresh_token', 'csrf_refresh_token', '/refresh_protected', '/post_refresh_protected'), # nopep8
206248
('/access_token', 'csrf_access_token', '/protected', '/post_protected')
@@ -222,7 +264,7 @@ def test_custom_csrf_methods(app, options):
222264
# Insure GET requests now fail without csrf
223265
response = test_client.get(get_url)
224266
assert response.status_code == 401
225-
assert response.get_json() == {'msg': 'Missing CSRF token in headers'}
267+
assert response.get_json() == {'msg': 'Missing CSRF token'}
226268

227269
# Insure GET requests now succeed with csrf
228270
csrf_headers = {'X-CSRF-TOKEN': csrf_token}
@@ -430,4 +472,4 @@ def test_jwt_optional_with_csrf_enabled(app):
430472
csrf_token = csrf_cookie['csrf_access_token']
431473
response = test_client.post('/optional_post_protected')
432474
assert response.status_code == 401
433-
assert response.get_json() == {'msg': 'Missing CSRF token in headers'}
475+
assert response.get_json() == {'msg': 'Missing CSRF token'}

0 commit comments

Comments
 (0)