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
2 changes: 2 additions & 0 deletions docs/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ These are only applicable if ``JWT_TOKEN_LOCATION`` is set to use cookies.
``JWT_SESSION_COOKIE`` If the cookies should be session cookies (deleted when the
browser is closed) or persistent cookies (never expire).
Defaults to ``True`` (session cookies).
``JWT_COOKIE_SAMESITE`` If the cookies should be sent in a cross-site browsing context.
Defaults to ``None``, which means cookies are always sent.
``JWT_COOKIE_CSRF_PROTECT`` Enable/disable CSRF protection when using cookies. Defaults to ``True``.
================================= =========================================

Expand Down
4 changes: 4 additions & 0 deletions flask_jwt_extended/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ def cookie_domain(self):
def session_cookie(self):
return current_app.config['JWT_SESSION_COOKIE']

@property
def cookie_samesite(self):
return current_app.config['JWT_COOKIE_SAMESITE']

@property
def csrf_protect(self):
return self.jwt_in_cookies and current_app.config['JWT_COOKIE_CSRF_PROTECT']
Expand Down
1 change: 1 addition & 0 deletions flask_jwt_extended/jwt_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def _set_default_configuration_options(app):
app.config.setdefault('JWT_COOKIE_SECURE', False)
app.config.setdefault('JWT_COOKIE_DOMAIN', None)
app.config.setdefault('JWT_SESSION_COOKIE', True)
app.config.setdefault('JWT_COOKIE_SAMESITE', None)

# Options for using double submit csrf protection
app.config.setdefault('JWT_COOKIE_CSRF_PROTECT', True)
Expand Down
24 changes: 16 additions & 8 deletions flask_jwt_extended/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ def set_access_cookies(response, encoded_access_token, max_age=None):
secure=config.cookie_secure,
httponly=True,
domain=config.cookie_domain,
path=config.access_cookie_path)
path=config.access_cookie_path,
samesite=config.cookie_samesite)

# If enabled, set the csrf double submit access cookie
if config.csrf_protect and config.csrf_in_cookies:
Expand All @@ -204,7 +205,8 @@ def set_access_cookies(response, encoded_access_token, max_age=None):
secure=config.cookie_secure,
httponly=False,
domain=config.cookie_domain,
path=config.access_csrf_cookie_path)
path=config.access_csrf_cookie_path,
samesite=config.cookie_samesite)


def set_refresh_cookies(response, encoded_refresh_token, max_age=None):
Expand Down Expand Up @@ -232,7 +234,8 @@ def set_refresh_cookies(response, encoded_refresh_token, max_age=None):
secure=config.cookie_secure,
httponly=True,
domain=config.cookie_domain,
path=config.refresh_cookie_path)
path=config.refresh_cookie_path,
samesite=config.cookie_samesite)

# If enabled, set the csrf double submit refresh cookie
if config.csrf_protect and config.csrf_in_cookies:
Expand All @@ -242,7 +245,8 @@ def set_refresh_cookies(response, encoded_refresh_token, max_age=None):
secure=config.cookie_secure,
httponly=False,
domain=config.cookie_domain,
path=config.refresh_csrf_cookie_path)
path=config.refresh_csrf_cookie_path,
samesite=config.cookie_samesite)


def unset_jwt_cookies(response):
Expand All @@ -262,14 +266,16 @@ def unset_jwt_cookies(response):
secure=config.cookie_secure,
httponly=True,
domain=config.cookie_domain,
path=config.refresh_cookie_path)
path=config.refresh_cookie_path,
samesite=config.cookie_samesite)
response.set_cookie(config.access_cookie_name,
value='',
expires=0,
secure=config.cookie_secure,
httponly=True,
domain=config.cookie_domain,
path=config.access_cookie_path)
path=config.access_cookie_path,
samesite=config.cookie_samesite)

if config.csrf_protect and config.csrf_in_cookies:
response.set_cookie(config.refresh_csrf_cookie_name,
Expand All @@ -278,11 +284,13 @@ def unset_jwt_cookies(response):
secure=config.cookie_secure,
httponly=False,
domain=config.cookie_domain,
path=config.refresh_csrf_cookie_path)
path=config.refresh_csrf_cookie_path,
samesite=config.cookie_samesite)
response.set_cookie(config.access_csrf_cookie_name,
value='',
expires=0,
secure=config.cookie_secure,
httponly=False,
domain=config.cookie_domain,
path=config.access_csrf_cookie_path)
path=config.access_csrf_cookie_path,
samesite=config.cookie_samesite)
3 changes: 3 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def test_default_configs(app):
assert config.cookie_secure is False
assert config.cookie_domain is None
assert config.session_cookie is True
assert config.cookie_samesite is None

assert config.csrf_protect is False
assert config.csrf_request_methods == ['POST', 'PUT', 'PATCH', 'DELETE']
Expand Down Expand Up @@ -68,6 +69,7 @@ def test_override_configs(app):
app.config['JWT_COOKIE_SECURE'] = True
app.config['JWT_COOKIE_DOMAIN'] = ".example.com"
app.config['JWT_SESSION_COOKIE'] = False
app.config['JWT_COOKIE_SAMESITE'] = "Strict"

app.config['JWT_COOKIE_CSRF_PROTECT'] = True
app.config['JWT_CSRF_METHODS'] = ['GET']
Expand Down Expand Up @@ -103,6 +105,7 @@ def test_override_configs(app):
assert config.cookie_secure is True
assert config.cookie_domain == ".example.com"
assert config.session_cookie is False
assert config.cookie_samesite == "Strict"

assert config.csrf_protect is True
assert config.csrf_request_methods == ['GET']
Expand Down
36 changes: 21 additions & 15 deletions tests/test_cookies.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
import pytest
from flask import Flask, jsonify, json
try:
from http.cookies import SimpleCookie
except ImportError:
from Cookie import SimpleCookie

from flask_jwt_extended import (
jwt_required, JWTManager, jwt_refresh_token_required, create_access_token,
create_refresh_token, set_access_cookies, set_refresh_cookies,
unset_jwt_cookies
)


def _get_cookie_from_response(response, cookie_name):
cookie_headers = response.headers.getlist('Set-Cookie')
for header in cookie_headers:
cookie = SimpleCookie()
cookie.load(header)
if cookie_name in cookie:
return cookie[cookie_name]
attributes = header.split(';')
if cookie_name in attributes[0]:
cookie = {}
for attr in attributes:
split = attr.split('=')
cookie[split[0].strip().lower()] = split[1] if len(split) > 1 else True
return cookie
return None


@pytest.fixture(scope='function')
def app():
app = Flask(__name__)
Expand Down Expand Up @@ -111,7 +108,7 @@ def test_default_access_csrf_protection(app, 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).value
csrf_token = _get_cookie_from_response(response, csrf_cookie_name)[csrf_cookie_name]

# Test you cannot post without the additional csrf protection
response = test_client.post(post_url)
Expand Down Expand Up @@ -173,7 +170,7 @@ def test_csrf_with_custom_header_names(app, 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).value
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_headers = {'FOO': csrf_token}
Expand All @@ -194,7 +191,7 @@ def test_custom_csrf_methods(app, 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).value
csrf_token = _get_cookie_from_response(response, csrf_cookie_name)[csrf_cookie_name]

# Insure we can now do posts without csrf
response = test_client.post(post_url)
Expand Down Expand Up @@ -240,11 +237,13 @@ def test_default_cookie_options(app):
assert access_cookie is not None
assert access_cookie['path'] == '/'
assert access_cookie['httponly'] is True
assert 'samesite' not in access_cookie

access_csrf_cookie = _get_cookie_from_response(response, 'csrf_access_token')
assert access_csrf_cookie is not None
assert access_csrf_cookie['path'] == '/'
assert access_csrf_cookie['httponly'] == ''
assert 'httponly' not in access_csrf_cookie
assert 'samesite' not in access_csrf_cookie

# Test the default refresh cookies
response = test_client.get('/refresh_token')
Expand All @@ -255,11 +254,13 @@ def test_default_cookie_options(app):
assert refresh_cookie is not None
assert refresh_cookie['path'] == '/'
assert refresh_cookie['httponly'] is True
assert 'samesite' not in refresh_cookie

refresh_csrf_cookie = _get_cookie_from_response(response, 'csrf_refresh_token')
assert refresh_csrf_cookie is not None
assert refresh_csrf_cookie['path'] == '/'
assert refresh_csrf_cookie['httponly'] == ''
assert 'httponly' not in refresh_csrf_cookie
assert 'samesite' not in refresh_csrf_cookie


def test_custom_cookie_options(app):
Expand All @@ -268,6 +269,7 @@ def test_custom_cookie_options(app):
app.config['JWT_COOKIE_SECURE'] = True
app.config['JWT_COOKIE_DOMAIN'] = 'test.com'
app.config['JWT_SESSION_COOKIE'] = False
app.config['JWT_COOKIE_SAMESITE'] = 'Strict'

# Test access cookies with changed options
response = test_client.get('/access_token')
Expand All @@ -281,13 +283,15 @@ def test_custom_cookie_options(app):
assert access_cookie['expires'] != ''
assert access_cookie['httponly'] is True
assert access_cookie['secure'] is True
assert access_cookie['samesite'] == 'Strict'

access_csrf_cookie = _get_cookie_from_response(response, 'csrf_access_token')
assert access_csrf_cookie is not None
assert access_csrf_cookie['path'] == '/'
assert access_csrf_cookie['secure'] is True
assert access_csrf_cookie['domain'] == 'test.com'
assert access_csrf_cookie['expires'] != ''
assert access_csrf_cookie['samesite'] == 'Strict'

# Test refresh cookies with changed options
response = test_client.get('/refresh_token')
Expand All @@ -301,13 +305,15 @@ def test_custom_cookie_options(app):
assert refresh_cookie['httponly'] is True
assert refresh_cookie['secure'] is True
assert refresh_cookie['expires'] != ''
assert refresh_cookie['samesite'] == 'Strict'

refresh_csrf_cookie = _get_cookie_from_response(response, 'csrf_refresh_token')
assert refresh_csrf_cookie is not None
assert refresh_csrf_cookie['path'] == '/'
assert refresh_csrf_cookie['secure'] is True
assert refresh_csrf_cookie['domain'] == 'test.com'
assert refresh_csrf_cookie['expires'] != ''
assert refresh_csrf_cookie['samesite'] == 'Strict'


def test_custom_cookie_names_and_paths(app):
Expand Down