Skip to content

Commit e6812d5

Browse files
committed
Easier way (hopefully) for users to handle jwt in cookie and csrf protection
Refs #5
1 parent b077a73 commit e6812d5

File tree

4 files changed

+169
-106
lines changed

4 files changed

+169
-106
lines changed

examples/token_in_cookie.py

Lines changed: 32 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
1-
import binascii
2-
import json
3-
import os
4-
51
from flask import Flask, jsonify, request
6-
from flask import Response
72

83
from flask_jwt_extended import JWTManager, jwt_required, create_access_token, \
9-
jwt_refresh_token_required, create_refresh_token, get_jwt_identity
4+
jwt_refresh_token_required, create_refresh_token, get_jwt_identity,\
5+
set_access_cookies, set_refresh_cookie
6+
7+
8+
# NOTE: This is being actively worked on, and is not complete yet. At present,
9+
# this code will not work! It should be rolled out next week sometime
10+
1011

1112
app = Flask(__name__)
1213
app.secret_key = 'super-secret' # Change this!
1314
jwt = JWTManager(app)
1415

1516

16-
# TODO add additional_claims as optional arg to create_token methods
17-
# TODO config option to check for tokens in cookie instead of request headers (or both)
18-
# TODO config option to do xsrf double submit verification on protected endpoints
17+
# Configure application to store jwts in cookies with double submit csrf protection
18+
app.config['JWT_TOKEN_LOCATION'] = 'cookie'
19+
app.config['JWT_COOKIE_HTTPONLY'] = True
20+
app.config['JWT_COOKIE_SECURE'] = True
1921

20-
def _create_xsrf_token():
21-
return binascii.hexlify(os.urandom(60))
22+
app.config['JWT_ACCESS_COOKIE_NAME'] = 'access_token_cookie'
23+
app.config['JWT_ACCESS_COOKIE_PATH'] = '/api/'
24+
25+
app.config['JWT_REFRESH_COOKIE_NAME'] = 'refresh_token_cookie'
26+
app.config['JWT_REFRESH_COOKIE_PATH'] = '/token/refresh'
27+
28+
app.config['JWT_COOKIE_CSRF_PROTECT'] = True
29+
app.config['JWT_ACCESS_CSRF_COOKIE_NAME'] = 'x_xsrf_access_token'
30+
app.config['JWT_REFRESH_CSRF_COOKIE_NAME'] = 'x_xsrf_refresh_token'
2231

2332

2433
@app.route('/token/auth', methods=['POST'])
@@ -28,95 +37,27 @@ def login():
2837
if username != 'test' and password != 'test':
2938
return jsonify({"msg": "Bad username or password"}), 401
3039

31-
# Create the x-xsrf-token we will use for CSRF double submit verification
32-
x_xsrf_access_token = _create_xsrf_token()
33-
x_xsrf_refresh_token = _create_xsrf_token()
34-
access_claims = {'X-XSRF-TOKEN': x_xsrf_access_token}
35-
refresh_claims = {'X-XSRF-TOKEN': x_xsrf_refresh_token}
36-
37-
# Create the access and refresh tokens with the x-xsrf-token included
38-
access_token = create_access_token(identity=username,
39-
additional_claims=access_claims)
40-
refresh_token = create_refresh_token(identity=username,
41-
additional_claims=refresh_claims)
42-
43-
# Create the response we will send back to the caller.
44-
data = json.dumps({'login': True})
45-
resp = Response(response=data, status=200, mimetype="application/json")
46-
47-
# Save the access and refresh tokens in a cookie with this request.
48-
# The secure option insures that the cookie is only sent over https,
49-
# httponly makes it so javascript cannot access this cookie, and prevents
50-
# XSS attacks (we are still vulnerable to CSRF though), and path says to
51-
# only send this cookie if it matches the path. Using the path, we can have
52-
# access tokens only sent when we go to protected endpoints, and refresh
53-
# tokens only sent when we go to the refresh endpoint
54-
resp.set_cookie('access_token',
55-
value=access_token,
56-
secure=True,
57-
httponly=True,
58-
path='/api/')
59-
resp.set_cookie('refresh_token',
60-
value=refresh_token,
61-
secure=True,
62-
httponly=True,
63-
path='/token/refresh')
64-
65-
# Set the X-XSRF-TOKEN in a not httponly token (which can be accessed by
66-
# javascript, but only by javascript running on this domain). From here on
67-
# out, we will need to set the X-XSRF-TOKEN header for each request, getting
68-
# the xsrf token from this cookie. On the backend, we will be verifying the
69-
# xsrf token in the header matches the xsrf token in the JWT. The end result
70-
# of this is that attackers will not be able to perform CSRF attacks, as they
71-
# could send the JWT back with the request, but without the additional xsrf
72-
# header they will not get accepted, and they cannot access the xsrf token
73-
# as this cookie can only be accessed by javascript running from the same
74-
# domain (and the JWT is httponly and cannot be accessed by any javascript).
75-
# Additionally, the users access and refresh token can not be stolen via
76-
# XSS (again, because they are httponly), but XSS attacks could still be
77-
# used to perform actions for a user without stealing their cookie.
78-
resp.set_cookie('x_xsrf_access_token',
79-
value=x_xsrf_access_token,
80-
secure=True,
81-
httponly=False,
82-
path='/api/')
83-
resp.set_cookie('x_xsrf_refresh_token',
84-
value=x_xsrf_refresh_token,
85-
secure=True,
86-
httponly=False,
87-
path='/token/refresh')
40+
# Create the tokens we will be sending back to the user
41+
access_token = create_access_token(identity=username)
42+
refresh_token = create_refresh_token(identity=username)
8843

44+
# Set the JWTs and the CSRF double submit protection cookies in this response
45+
resp = jsonify({'login': True}), 200
46+
set_access_cookies(resp, access_token)
47+
set_refresh_cookie(resp, refresh_token)
8948
return resp
9049

9150

9251
@app.route('/token/refresh', methods=['POST'])
9352
@jwt_refresh_token_required
9453
def refresh():
95-
# New xsrf token to use with the new jwt
96-
x_xsrf_token = _create_xsrf_token()
97-
98-
# Create the new jwt
99-
claims = {'X-XSRF-TOKEN': x_xsrf_token}
54+
# Create the new access token
10055
current_user = get_jwt_identity()
101-
access_token = create_access_token(identity=current_user, additional_claims=claims)
102-
103-
# Create the respons to send back to the caller
104-
data = json.dumps({'refresh': True})
105-
resp = Response(response=data, status=200, mimetype="application/json")
106-
107-
# Set the JWT and XSRF TOKEN in the cookie with the same options and
108-
# security that we used for the original access token
109-
resp.set_cookie('access_token',
110-
value=access_token,
111-
secure=True,
112-
httponly=True,
113-
path='/api/')
114-
resp.set_cookie('x_xsrf_access_token',
115-
value=x_xsrf_token,
116-
secure=True,
117-
httponly=False,
118-
path='/api/')
56+
access_token = create_access_token(identity=current_user)
11957

58+
# Set the access JWT and CSRF double submit protection cookies in this response
59+
resp = jsonify({'refresh': True}), 200
60+
set_access_cookies(resp, access_token)
12061
return resp
12162

12263

flask_jwt_extended/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .jwt_manager import JWTManager
22
from .utils import (jwt_required, fresh_jwt_required, jwt_refresh_token_required,
33
create_refresh_token, create_access_token, get_jwt_identity,
4-
get_jwt_claims)
4+
get_jwt_claims, set_access_cookies, set_refresh_cookie)
55
from .blacklist import (revoke_token, unrevoke_token, get_stored_tokens,
66
get_all_stored_tokens, get_stored_token)

flask_jwt_extended/config.py

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,31 @@
1010
# See: http://pythonhosted.org/simplekv/index.html#simplekv.TimeToLiveMixin
1111

1212

13-
# Where to look for the JWT. Available options are cookie and header
14-
REQUEST_JWT_LOCATION = 'header'
13+
# TODO support for cookies and headers at the same time. This could be useful
14+
# for using cookies in a web browser (more secure), and headers in a mobile
15+
# app (don't have to worry about csrf/xss there, and headers are easier to
16+
# manage in that environment)
1517

16-
# Options for where to get the JWT if using a header approach
18+
# Where to look for the JWT. Available options are cookie, header, and either
19+
TOKEN_LOCATION = 'headers'
20+
21+
# Options for JWTs when the TOKEN_LOCATION is headers
1722
HEADER_NAME = 'Authorization'
1823
HEADER_TYPE = 'Bearer'
1924

20-
# Options for where to get and handling JWTs if using a cookie approach
21-
COOKIE_ACCESS_TOKEN_NAME = 'access_token'
22-
COOKIE_REFRESH_TOKEN_NAME = 'refresh_token'
23-
COOKIE_CSRF_DOUBLE_SUBMIT = False
24-
COOKIE_XSRF_ACCESS_NAME = 'xsrf_access_token'
25-
COOKIE_XSRF_REFRESH_NAME = 'xsrf_refresh_token'
25+
# Option for JWTs when the TOKEN_LOCATION is cookies
26+
COOKIE_SECURE = False
27+
ACCESS_COOKIE_NAME = 'access_token_cookie'
28+
REFRESH_COOKIE_NAME = 'refresh_token_cookie'
29+
ACCESS_COOKIE_PATH = None
30+
REFRESH_COOKIE_PATH = None
31+
32+
# Options for using double submit for verifying CSRF tokens
33+
COOKIE_CSRF_PROTECT = True
34+
ACCESS_CSRF_COOKIE_NAME = 'csrf_access_token'
35+
REFRESH_CSRF_COOKIE_NAME = 'csrf_refresh_token'
36+
ACCESS_CSRF_HEADER_NAME = 'X-CSRF-ACCESS-TOKEN'
37+
REFRESH_CSRF_HEADER_NAME = 'X-CSRF-REFRESH-TOKEN'
2638

2739
# How long an a token will live before they expire.
2840
ACCESS_TOKEN_EXPIRES = datetime.timedelta(minutes=15)
@@ -35,17 +47,64 @@
3547

3648
# Options for blacklisting/revoking tokens
3749
BLACKLIST_ENABLED = False
38-
BLACKLIST_STORE = None
50+
BLACKLIST_STORE = None # simplekv object: https://pypi.python.org/pypi/simplekv/
3951
BLACKLIST_TOKEN_CHECKS = 'refresh' # valid options are 'all', and 'refresh'
4052

4153

54+
def get_token_location():
55+
location = current_app.config.get('JWT_TOKEN_LOCATION', TOKEN_LOCATION)
56+
if location not in ['headers', 'cookies']:
57+
raise RuntimeError('JWT_LOCATION_LOCATION must be "headers" or "cookies"')
58+
return location
59+
60+
4261
def get_jwt_header_name():
4362
name = current_app.config.get('JWT_HEADER_NAME', HEADER_NAME)
4463
if not name:
4564
raise RuntimeError("JWT_HEADER_NAME must be set")
4665
return name
4766

4867

68+
def get_cookie_secure():
69+
return current_app.config.get('JWT_COOKIE_SECURE', COOKIE_SECURE)
70+
71+
72+
def get_access_cookie_name():
73+
return current_app.config.get('JWT_ACCESS_COOKIE_NAME', ACCESS_COOKIE_NAME)
74+
75+
76+
def get_refresh_cookie_name():
77+
return current_app.config.get('JWT_REFRESH_COOKIE_NAME', REFRESH_COOKIE_NAME)
78+
79+
80+
def get_access_cookie_path():
81+
return current_app.config.get('JWT_ACCESS_COOKIE_PATH', ACCESS_COOKIE_PATH)
82+
83+
84+
def get_refresh_cookie_path():
85+
return current_app.config.get('JWT_REFRESH_COOKIE_PATH', REFRESH_COOKIE_PATH)
86+
87+
88+
def get_cookie_csrf_protect():
89+
return current_app.config.get('JWT_COOKIE_CSRF_PROTECT', COOKIE_CSRF_PROTECT)
90+
91+
92+
def get_access_csrf_cookie_name():
93+
return current_app.config.get('JWT_ACCESS_CSRF_COOKIE_NAME', ACCESS_CSRF_COOKIE_NAME)
94+
95+
96+
def get_refresh_csrf_cookie_name():
97+
return current_app.config.get('JWT_REFRESH_CSRF_COOKIE_NAME', REFRESH_CSRF_COOKIE_NAME)
98+
99+
100+
def get_access_csrf_header_name():
101+
return current_app.config.get('JWT_ACCESS_CSRF_HEADER_NAME', ACCESS_CSRF_HEADER_NAME)
102+
103+
104+
def get_refresh_csrf_header_name():
105+
return current_app.config.get('JWT_REFRESH_CSRF_HEADER_NAME', REFRESH_CSRF_HEADER_NAME)
106+
107+
49108
def get_jwt_header_type():
50109
return current_app.config.get('JWT_HEADER_TYPE', HEADER_TYPE)
51110

flask_jwt_extended/utils.py

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import datetime
22
import json
3+
import os
34
import uuid
4-
55
from functools import wraps
66

7+
import binascii
78
import jwt
89
import six
9-
from werkzeug.local import LocalProxy
10-
from flask import request, jsonify, current_app
10+
from flask import request, current_app
1111
try:
1212
from flask import _app_ctx_stack as ctx_stack
1313
except ImportError: # pragma: no cover
1414
from flask import _request_ctx_stack as ctx_stack
1515

1616
from flask_jwt_extended.config import get_access_expires, get_refresh_expires, \
17-
get_algorithm, get_blacklist_enabled, get_blacklist_checks, get_jwt_header_type
17+
get_algorithm, get_blacklist_enabled, get_blacklist_checks, get_jwt_header_type, \
18+
get_access_cookie_name, get_cookie_secure, get_access_cookie_path, \
19+
get_cookie_csrf_protect, get_access_csrf_cookie_name, \
20+
get_refresh_cookie_name, get_refresh_cookie_path, \
21+
get_refresh_csrf_cookie_name
1822
from flask_jwt_extended.exceptions import JWTEncodeError, JWTDecodeError, \
1923
InvalidHeaderError, NoAuthHeaderError, WrongTokenError, RevokedTokenError, \
2024
FreshTokenRequired
@@ -37,6 +41,11 @@ def get_jwt_claims():
3741
return getattr(ctx_stack.top, 'jwt_user_claims', {})
3842

3943

44+
# TODO set csrf token in jwt when creating tokens (if enabled)
45+
def _create_xsrf_token():
46+
return binascii.hexlify(os.urandom(60))
47+
48+
4049
def _encode_access_token(identity, secret, algorithm, token_expire_delta,
4150
fresh, user_claims):
4251
"""
@@ -303,6 +312,7 @@ def create_access_token(identity, fresh=True):
303312
access_expire_delta = get_access_expires()
304313
algorithm = get_algorithm()
305314
user_claims = current_app.jwt_manager.user_claims_callback(identity)
315+
306316
access_token = _encode_access_token(identity, secret, algorithm, access_expire_delta,
307317
fresh=fresh, user_claims=user_claims)
308318
return access_token
@@ -313,3 +323,56 @@ def _get_secret_key():
313323
if not key:
314324
raise RuntimeError('flask SECRET_KEY must be set')
315325
return key
326+
327+
328+
def _get_csrf_token(encoded_token):
329+
secret = _get_secret_key()
330+
algorithm = get_algorithm()
331+
token = _decode_jwt(encoded_token, secret, algorithm)
332+
try:
333+
return token['csrf']
334+
except KeyError:
335+
raise RuntimeError('JWT does not have csrf token set. Is '
336+
'JWT_COOKIE_CSRF_PROTECT set to True?')
337+
338+
339+
def set_access_cookies(response, encoded_access_token):
340+
"""
341+
Takes a flask response object, and configures it to set the encoded access
342+
token in a cookie (as well as a csrf access cookie if enabled)
343+
"""
344+
# Set the access JWT in the cookie
345+
response.set_cookie(get_access_cookie_name(),
346+
value=encoded_access_token,
347+
secure=get_cookie_secure(),
348+
httponly=True,
349+
path=get_access_cookie_path())
350+
351+
# If enabled, set the csrf double submit access cookie
352+
if get_cookie_csrf_protect():
353+
response.set_cookie(get_access_csrf_cookie_name(),
354+
value=_get_csrf_token(encoded_access_token),
355+
secure=get_cookie_secure(),
356+
httponly=False,
357+
path=get_access_cookie_path())
358+
359+
360+
def set_refresh_cookie(response, encoded_refresh_token):
361+
"""
362+
Takes a flask response object, and configures it to set the encoded refresh
363+
token in a cookie (as well as a csrf refresh cookie if enabled)
364+
"""
365+
# Set the refresh JWT in the cookie
366+
response.set_cookie(get_refresh_cookie_name(),
367+
value=encoded_refresh_token,
368+
secure=get_cookie_secure(),
369+
httponly=True,
370+
path=get_refresh_cookie_path())
371+
372+
# If enabled, set the csrf double submit refresh cookie
373+
if get_cookie_csrf_protect():
374+
response.set_cookie(get_refresh_csrf_cookie_name(),
375+
value=_get_csrf_token(encoded_refresh_token),
376+
secure=get_cookie_secure(),
377+
httponly=False,
378+
path=get_refresh_cookie_path())

0 commit comments

Comments
 (0)