Skip to content

Commit 024fd32

Browse files
farshianavimalloc
authored andcommitted
Add support for cookie 'samesite' option
1 parent 32a1f3c commit 024fd32

File tree

6 files changed

+47
-23
lines changed

6 files changed

+47
-23
lines changed

docs/options.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ These are only applicable if ``JWT_TOKEN_LOCATION`` is set to use cookies.
8080
``JWT_SESSION_COOKIE`` If the cookies should be session cookies (deleted when the
8181
browser is closed) or persistent cookies (never expire).
8282
Defaults to ``True`` (session cookies).
83+
``JWT_COOKIE_SAMESITE`` If the cookies should be sent in a cross-site browsing context.
84+
Defaults to ``None``, which means cookies are always sent.
8385
``JWT_COOKIE_CSRF_PROTECT`` Enable/disable CSRF protection when using cookies. Defaults to ``True``.
8486
================================= =========================================
8587

flask_jwt_extended/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ def cookie_domain(self):
9797
def session_cookie(self):
9898
return current_app.config['JWT_SESSION_COOKIE']
9999

100+
@property
101+
def cookie_samesite(self):
102+
return current_app.config['JWT_COOKIE_SAMESITE']
103+
100104
@property
101105
def csrf_protect(self):
102106
return self.jwt_in_cookies and current_app.config['JWT_COOKIE_CSRF_PROTECT']

flask_jwt_extended/jwt_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ def _set_default_configuration_options(app):
150150
app.config.setdefault('JWT_COOKIE_SECURE', False)
151151
app.config.setdefault('JWT_COOKIE_DOMAIN', None)
152152
app.config.setdefault('JWT_SESSION_COOKIE', True)
153+
app.config.setdefault('JWT_COOKIE_SAMESITE', None)
153154

154155
# Options for using double submit csrf protection
155156
app.config.setdefault('JWT_COOKIE_CSRF_PROTECT', True)

flask_jwt_extended/utils.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,8 @@ def set_access_cookies(response, encoded_access_token, max_age=None):
194194
secure=config.cookie_secure,
195195
httponly=True,
196196
domain=config.cookie_domain,
197-
path=config.access_cookie_path)
197+
path=config.access_cookie_path,
198+
samesite=config.cookie_samesite)
198199

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

209211

210212
def set_refresh_cookies(response, encoded_refresh_token, max_age=None):
@@ -232,7 +234,8 @@ def set_refresh_cookies(response, encoded_refresh_token, max_age=None):
232234
secure=config.cookie_secure,
233235
httponly=True,
234236
domain=config.cookie_domain,
235-
path=config.refresh_cookie_path)
237+
path=config.refresh_cookie_path,
238+
samesite=config.cookie_samesite)
236239

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

247251

248252
def unset_jwt_cookies(response):
@@ -262,14 +266,16 @@ def unset_jwt_cookies(response):
262266
secure=config.cookie_secure,
263267
httponly=True,
264268
domain=config.cookie_domain,
265-
path=config.refresh_cookie_path)
269+
path=config.refresh_cookie_path,
270+
samesite=config.cookie_samesite)
266271
response.set_cookie(config.access_cookie_name,
267272
value='',
268273
expires=0,
269274
secure=config.cookie_secure,
270275
httponly=True,
271276
domain=config.cookie_domain,
272-
path=config.access_cookie_path)
277+
path=config.access_cookie_path,
278+
samesite=config.cookie_samesite)
273279

274280
if config.csrf_protect and config.csrf_in_cookies:
275281
response.set_cookie(config.refresh_csrf_cookie_name,
@@ -278,11 +284,13 @@ def unset_jwt_cookies(response):
278284
secure=config.cookie_secure,
279285
httponly=False,
280286
domain=config.cookie_domain,
281-
path=config.refresh_csrf_cookie_path)
287+
path=config.refresh_csrf_cookie_path,
288+
samesite=config.cookie_samesite)
282289
response.set_cookie(config.access_csrf_cookie_name,
283290
value='',
284291
expires=0,
285292
secure=config.cookie_secure,
286293
httponly=False,
287294
domain=config.cookie_domain,
288-
path=config.access_csrf_cookie_path)
295+
path=config.access_csrf_cookie_path,
296+
samesite=config.cookie_samesite)

tests/test_config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def test_default_configs(app):
3030
assert config.cookie_secure is False
3131
assert config.cookie_domain is None
3232
assert config.session_cookie is True
33+
assert config.cookie_samesite is None
3334

3435
assert config.csrf_protect is False
3536
assert config.csrf_request_methods == ['POST', 'PUT', 'PATCH', 'DELETE']
@@ -68,6 +69,7 @@ def test_override_configs(app):
6869
app.config['JWT_COOKIE_SECURE'] = True
6970
app.config['JWT_COOKIE_DOMAIN'] = ".example.com"
7071
app.config['JWT_SESSION_COOKIE'] = False
72+
app.config['JWT_COOKIE_SAMESITE'] = "Strict"
7173

7274
app.config['JWT_COOKIE_CSRF_PROTECT'] = True
7375
app.config['JWT_CSRF_METHODS'] = ['GET']
@@ -103,6 +105,7 @@ def test_override_configs(app):
103105
assert config.cookie_secure is True
104106
assert config.cookie_domain == ".example.com"
105107
assert config.session_cookie is False
108+
assert config.cookie_samesite == "Strict"
106109

107110
assert config.csrf_protect is True
108111
assert config.csrf_request_methods == ['GET']

tests/test_cookies.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
11
import pytest
22
from flask import Flask, jsonify, json
3-
try:
4-
from http.cookies import SimpleCookie
5-
except ImportError:
6-
from Cookie import SimpleCookie
73

84
from flask_jwt_extended import (
95
jwt_required, JWTManager, jwt_refresh_token_required, create_access_token,
106
create_refresh_token, set_access_cookies, set_refresh_cookies,
117
unset_jwt_cookies
128
)
139

14-
1510
def _get_cookie_from_response(response, cookie_name):
1611
cookie_headers = response.headers.getlist('Set-Cookie')
1712
for header in cookie_headers:
18-
cookie = SimpleCookie()
19-
cookie.load(header)
20-
if cookie_name in cookie:
21-
return cookie[cookie_name]
13+
attributes = header.split(';')
14+
if cookie_name in attributes[0]:
15+
cookie = {}
16+
for attr in attributes:
17+
split = attr.split('=')
18+
cookie[split[0].strip().lower()] = split[1] if len(split) > 1 else True
19+
return cookie
2220
return None
2321

24-
2522
@pytest.fixture(scope='function')
2623
def app():
2724
app = Flask(__name__)
@@ -111,7 +108,7 @@ def test_default_access_csrf_protection(app, options):
111108

112109
# Get the jwt cookies and csrf double submit tokens
113110
response = test_client.get(auth_url)
114-
csrf_token = _get_cookie_from_response(response, csrf_cookie_name).value
111+
csrf_token = _get_cookie_from_response(response, csrf_cookie_name)[csrf_cookie_name]
115112

116113
# Test you cannot post without the additional csrf protection
117114
response = test_client.post(post_url)
@@ -173,7 +170,7 @@ def test_csrf_with_custom_header_names(app, options):
173170

174171
# Get the jwt cookies and csrf double submit tokens
175172
response = test_client.get(auth_url)
176-
csrf_token = _get_cookie_from_response(response, csrf_cookie_name).value
173+
csrf_token = _get_cookie_from_response(response, csrf_cookie_name)[csrf_cookie_name]
177174

178175
# Test that you can post with the csrf double submit value
179176
csrf_headers = {'FOO': csrf_token}
@@ -194,7 +191,7 @@ def test_custom_csrf_methods(app, options):
194191

195192
# Get the jwt cookies and csrf double submit tokens
196193
response = test_client.get(auth_url)
197-
csrf_token = _get_cookie_from_response(response, csrf_cookie_name).value
194+
csrf_token = _get_cookie_from_response(response, csrf_cookie_name)[csrf_cookie_name]
198195

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

244242
access_csrf_cookie = _get_cookie_from_response(response, 'csrf_access_token')
245243
assert access_csrf_cookie is not None
246244
assert access_csrf_cookie['path'] == '/'
247-
assert access_csrf_cookie['httponly'] == ''
245+
assert 'httponly' not in access_csrf_cookie
246+
assert 'samesite' not in access_csrf_cookie
248247

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

259259
refresh_csrf_cookie = _get_cookie_from_response(response, 'csrf_refresh_token')
260260
assert refresh_csrf_cookie is not None
261261
assert refresh_csrf_cookie['path'] == '/'
262-
assert refresh_csrf_cookie['httponly'] == ''
262+
assert 'httponly' not in refresh_csrf_cookie
263+
assert 'samesite' not in refresh_csrf_cookie
263264

264265

265266
def test_custom_cookie_options(app):
@@ -268,6 +269,7 @@ def test_custom_cookie_options(app):
268269
app.config['JWT_COOKIE_SECURE'] = True
269270
app.config['JWT_COOKIE_DOMAIN'] = 'test.com'
270271
app.config['JWT_SESSION_COOKIE'] = False
272+
app.config['JWT_COOKIE_SAMESITE'] = 'Strict'
271273

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

285288
access_csrf_cookie = _get_cookie_from_response(response, 'csrf_access_token')
286289
assert access_csrf_cookie is not None
287290
assert access_csrf_cookie['path'] == '/'
288291
assert access_csrf_cookie['secure'] is True
289292
assert access_csrf_cookie['domain'] == 'test.com'
290293
assert access_csrf_cookie['expires'] != ''
294+
assert access_csrf_cookie['samesite'] == 'Strict'
291295

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

305310
refresh_csrf_cookie = _get_cookie_from_response(response, 'csrf_refresh_token')
306311
assert refresh_csrf_cookie is not None
307312
assert refresh_csrf_cookie['path'] == '/'
308313
assert refresh_csrf_cookie['secure'] is True
309314
assert refresh_csrf_cookie['domain'] == 'test.com'
310315
assert refresh_csrf_cookie['expires'] != ''
316+
assert refresh_csrf_cookie['samesite'] == 'Strict'
311317

312318

313319
def test_custom_cookie_names_and_paths(app):

0 commit comments

Comments
 (0)