Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,14 @@ These are only applicable if ``JWT_TOKEN_LOCATION`` is set to use cookies and
Only applicable if ``JWT_CSRF_IN_COOKIES`` is ``True``
``JWT_REFRESH_CSRF_COOKIE_PATH`` Path of the CSRF refresh cookie. Defaults to ``'/'``.
Only applicable if ``JWT_CSRF_IN_COOKIES`` is ``True``
``JWT_CSRF_CHECK_FORM`` When no CSRF token can be found in the header, check the form data. Defaults to
``False``.
``JWT_ACCESS_CSRF_FIELD_NAME`` Name of the form field that should contain the CSRF double submit value for access
tokens when no header is present. Only applicable if ``JWT_CSRF_CHECK_FORM`` is
``True``. Defaults to ``'csrf_token'``.
``JWT_REFRESH_CSRF_FIELD_NAME`` Name of the form field that should contain the CSRF double submit value for refresh
tokens when no header is present. Only applicable if ``JWT_CSRF_CHECK_FORM`` is
``True``. Defaults to ``'csrf_token'``.
================================= =========================================


Expand Down
36 changes: 36 additions & 0 deletions docs/tokens_in_cookies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,39 @@ to the caller, like such:
set_access_cookies(resp, access_token)
set_refresh_cookies(resp, refresh_token)
return resp, 200


Typically JWT is used with API servers using JSON payloads, often via AJAX. However you may have an endpoint that
receives POST requests directly from an HTML form. Without AJAX, you can't set the CSRF headers to pass your token to
the server. In this scenario you can send the token in a hidden form field. To accomplish this, first configure JWT to
check the form for CSRF tokens. Now it's not necessary to send the csrf in a separate cookie, you can render it
directly into your HTML template:


.. code-block:: python

app.config['JWT_CSRF_CHECK_FORM'] = True

...

@app.route('/protected', methods=['GET', 'POST'])
@jwt_optional
def protected():
if request.method == "GET":
return render_template(
"form.html", csrf_token=(get_raw_jwt() or {}).get("csrf")
)
else:
# handle POST request
current_user = get_jwt_identity()


In the HTML template, pass the token back to the server via a hidden input.

.. code-block:: html

<form method="POST">
...
<input name="csrf_token" type="hidden" value="{{ csrf_token }}">
<button>Submit</button>
</form>
12 changes: 12 additions & 0 deletions flask_jwt_extended/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,18 @@ def refresh_csrf_header_name(self):
return self._get_depreciated_csrf_header_name() or \
current_app.config['JWT_REFRESH_CSRF_HEADER_NAME']

@property
def csrf_check_form(self):
return current_app.config['JWT_CSRF_CHECK_FORM']

@property
def access_csrf_field_name(self):
return current_app.config['JWT_ACCESS_CSRF_FIELD_NAME']

@property
def refresh_csrf_field_name(self):
return current_app.config['JWT_REFRESH_CSRF_FIELD_NAME']

@property
def access_expires(self):
delta = current_app.config['JWT_ACCESS_TOKEN_EXPIRES']
Expand Down
3 changes: 3 additions & 0 deletions flask_jwt_extended/jwt_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ def _set_default_configuration_options(app):
app.config.setdefault('JWT_REFRESH_CSRF_COOKIE_NAME', 'csrf_refresh_token')
app.config.setdefault('JWT_ACCESS_CSRF_COOKIE_PATH', '/')
app.config.setdefault('JWT_REFRESH_CSRF_COOKIE_PATH', '/')
app.config.setdefault('JWT_CSRF_CHECK_FORM', False)
app.config.setdefault('JWT_ACCESS_CSRF_FIELD_NAME', 'csrf_token')
app.config.setdefault('JWT_REFRESH_CSRF_FIELD_NAME', 'csrf_token')

# How long an a token will live before they expire.
app.config.setdefault('JWT_ACCESS_TOKEN_EXPIRES', datetime.timedelta(minutes=15))
Expand Down
6 changes: 5 additions & 1 deletion flask_jwt_extended/view_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,18 +198,22 @@ def _decode_jwt_from_cookies(request_type):
if request_type == 'access':
cookie_key = config.access_cookie_name
csrf_header_key = config.access_csrf_header_name
csrf_field_key = config.access_csrf_field_name
else:
cookie_key = config.refresh_cookie_name
csrf_header_key = config.refresh_csrf_header_name
csrf_field_key = config.refresh_csrf_field_name

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

if config.csrf_protect and request.method in config.csrf_request_methods:
csrf_value = request.headers.get(csrf_header_key, None)
if not csrf_value and config.csrf_check_form:
csrf_value = request.form.get(csrf_field_key, None)
if not csrf_value:
raise CSRFError("Missing CSRF token in headers")
raise CSRFError("Missing CSRF token")
else:
csrf_value = None

Expand Down
48 changes: 45 additions & 3 deletions tests/test_cookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def test_default_access_csrf_protection(app, options):
# Test you cannot post without the additional csrf protection
response = test_client.post(post_url)
assert response.status_code == 401
assert response.get_json() == {'msg': 'Missing CSRF token in headers'}
assert response.get_json() == {'msg': 'Missing CSRF token'}

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


@pytest.mark.parametrize("options", [
('/refresh_token', 'csrf_refresh_token', '/post_refresh_protected'),
('/access_token', 'csrf_access_token', '/post_protected')
])
def test_csrf_with_default_form_field(app, options):
app.config['JWT_CSRF_CHECK_FORM'] = True
test_client = app.test_client()
auth_url, csrf_cookie_name, post_url = options

# Get the jwt cookies and csrf double submit tokens
response = test_client.get(auth_url)
csrf_token = _get_cookie_from_response(response, csrf_cookie_name)[csrf_cookie_name]

# Test that you can post with the csrf double submit value
csrf_data = {'csrf_token': csrf_token}
response = test_client.post(post_url, data=csrf_data)
assert response.status_code == 200
assert response.get_json() == {'foo': 'bar'}


@pytest.mark.parametrize("options", [
('/refresh_token', 'csrf_refresh_token', '/post_refresh_protected'),
('/access_token', 'csrf_access_token', '/post_protected')
])
def test_csrf_with_custom_form_field(app, options):
app.config['JWT_CSRF_CHECK_FORM'] = True
app.config['JWT_ACCESS_CSRF_FIELD_NAME'] = 'FOO'
app.config['JWT_REFRESH_CSRF_FIELD_NAME'] = 'FOO'
test_client = app.test_client()
auth_url, csrf_cookie_name, post_url = options

# Get the jwt cookies and csrf double submit tokens
response = test_client.get(auth_url)
csrf_token = _get_cookie_from_response(response, csrf_cookie_name)[csrf_cookie_name]

# Test that you can post with the csrf double submit value
csrf_data = {'FOO': csrf_token}
response = test_client.post(post_url, data=csrf_data)
assert response.status_code == 200
assert response.get_json() == {'foo': 'bar'}


@pytest.mark.parametrize("options", [
('/refresh_token', 'csrf_refresh_token', '/refresh_protected', '/post_refresh_protected'), # nopep8
('/access_token', 'csrf_access_token', '/protected', '/post_protected')
Expand All @@ -222,7 +264,7 @@ def test_custom_csrf_methods(app, options):
# Insure GET requests now fail without csrf
response = test_client.get(get_url)
assert response.status_code == 401
assert response.get_json() == {'msg': 'Missing CSRF token in headers'}
assert response.get_json() == {'msg': 'Missing CSRF token'}

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