Navigation

If the frontend that is consuming this backend is a website, you may be storing
JWTs in the browser localStorage or sessionStorage. There is nothing
wrong with this, but if you have any sort of XSS vulnerability on your
site, an attacker will be able to trivially steal your refresh and access tokens.
If you want some additional security on your site, you can save your JWTs in a
httponly cookie instead, which keeps javascript from being able to access the
cookie. See this great blog for a more in depth analysis between these options:
http://www.redotheweb.com/2015/11/09/api-security.html

Here is a basic example of how to store JWTs in cookies:

fromflaskimportFlask,jsonify,requestfromflask_jwt_extendedimport(JWTManager,jwt_required,create_access_token,jwt_refresh_token_required,create_refresh_token,get_jwt_identity,set_access_cookies,set_refresh_cookies,unset_jwt_cookies)# NOTE: This is just a basic example of how to enable cookies. This is# vulnerable to CSRF attacks, and should not be used as is. See# csrf_protection_with_cookies.py for a more complete example!app=Flask(__name__)# Configure application to store JWTs in cookies. Whenever you make# a request to a protected endpoint, you will need to send in the# access or refresh JWT via a cookie.app.config['JWT_TOKEN_LOCATION']=['cookies']# Set the cookie paths, so that you are only sending your access token# cookie to the access endpoints, and only sending your refresh token# to the refresh endpoint. Technically this is optional, but it is in# your best interest to not send additional cookies in the request if# they aren't needed.app.config['JWT_ACCESS_COOKIE_PATH']='/api/'app.config['JWT_REFRESH_COOKIE_PATH']='/token/refresh'# Disable CSRF protection for this example. In almost every case,# this is a bad idea. See examples/csrf_protection_with_cookies.py# for how safely store JWTs in cookiesapp.config['JWT_COOKIE_CSRF_PROTECT']=False# Set the secret key to sign the JWTs withapp.config['JWT_SECRET_KEY']='super-secret'# Change this!jwt=JWTManager(app)# Use the set_access_cookie() and set_refresh_cookie() on a response# object to set the JWTs in the response cookies. You can configure# the cookie names and other settings via various app.config options@app.route('/token/auth',methods=['POST'])deflogin():username=request.json.get('username',None)password=request.json.get('password',None)ifusername!='test'orpassword!='test':returnjsonify({'login':False}),401# Create the tokens we will be sending back to the useraccess_token=create_access_token(identity=username)refresh_token=create_refresh_token(identity=username)# Set the JWT cookies in the responseresp=jsonify({'login':True})set_access_cookies(resp,access_token)set_refresh_cookies(resp,refresh_token)returnresp,200# Same thing as login here, except we are only setting a new cookie# for the access token.@app.route('/token/refresh',methods=['POST'])@jwt_refresh_token_requireddefrefresh():# Create the new access tokencurrent_user=get_jwt_identity()access_token=create_access_token(identity=current_user)# Set the JWT access cookie in the responseresp=jsonify({'refresh':True})set_access_cookies(resp,access_token)returnresp,200# Because the JWTs are stored in an httponly cookie now, we cannot# log the user out by simply deleting the cookie in the frontend.# We need the backend to send us a response to delete the cookies# in order to logout. unset_jwt_cookies is a helper function to# do just that.@app.route('/token/remove',methods=['POST'])deflogout():resp=jsonify({'logout':True})unset_jwt_cookies(resp)returnresp,200# We do not need to make any changes to our protected endpoints. They# will all still function the exact same as they do when sending the# JWT in via a header instead of a cookie@app.route('/api/example',methods=['GET'])@jwt_requireddefprotected():username=get_jwt_identity()returnjsonify({'hello':'from {}'.format(username)}),200if__name__=='__main__':app.run()

This isn’t the full story however. We can now keep our cookie from being stolen via XSS
attacks, but have traded that for a vulnerability to CSRF attacks. To combat
CSRF, we are going to use a technique called double submit verification.

When we create a JWT, we will also create a random string and store it in the JWT. This token is saved
in a cookie with httponly set to True, so it cannot be accessed via javascript.
We will then create a secondary cookie that contains only the random string, but
has httponly set to False, so that it can be accessed via javascript running on
your website. Now in order to access a protected endpoint,
you will need to add a custom header that contains the the random string in it,
and if that header doesn’t exist or it doesn’t match the string that is stored
in the JWT, the request will be kicked out as unauthorized.

To break this down, if an attacker attempts to perform a CSRF attack they will
send the JWT (via the cookie) to a protected endpoint, but without the random
string in the requests header, they wont be able to access the endpoint. They
cannot access the random string, unless they can run javascript on your website
(likely via an XSS attack), and if they are able to perform an XSS attack, they
will not be able to steal the actual access and refresh JWTs, as javascript is
still not able to access those httponly cookies.

This obviously isn’t a golden bullet. If an attacker can perform an XSS attack they can
still access protected endpoints from people who visit your site. However, it is better
then if they were able to steal the access and refresh tokens tokens from
local/session storage, and use them whenever they wanted. If this additional
security is worth the added complexity of using cookies and double submit CSRF
protection is a choice you will have to make.

Here is an example of using cookies with CSRF protection:

fromflaskimportFlask,jsonify,requestfromflask_jwt_extendedimport(JWTManager,jwt_required,create_access_token,jwt_refresh_token_required,create_refresh_token,get_jwt_identity,set_access_cookies,set_refresh_cookies,unset_jwt_cookies)app=Flask(__name__)# Configure application to store JWTs in cookiesapp.config['JWT_TOKEN_LOCATION']=['cookies']# Only allow JWT cookies to be sent over https. In production, this# should likely be Trueapp.config['JWT_COOKIE_SECURE']=False# Set the cookie paths, so that you are only sending your access token# cookie to the access endpoints, and only sending your refresh token# to the refresh endpoint. Technically this is optional, but it is in# your best interest to not send additional cookies in the request if# they aren't needed.app.config['JWT_ACCESS_COOKIE_PATH']='/api/'app.config['JWT_REFRESH_COOKIE_PATH']='/token/refresh'# Enable csrf double submit protection. See this for a thorough# explanation: http://www.redotheweb.com/2015/11/09/api-security.htmlapp.config['JWT_COOKIE_CSRF_PROTECT']=True# Set the secret key to sign the JWTs withapp.config['JWT_SECRET_KEY']='super-secret'# Change this!jwt=JWTManager(app)# By default, the CRSF cookies will be called csrf_access_token and# csrf_refresh_token, and in protected endpoints we will look for the# CSRF token in the 'X-CSRF-TOKEN' header. You can modify all of these# with various app.config options. Check the options page for details.# With JWT_COOKIE_CSRF_PROTECT set to True, set_access_cookies() and# set_refresh_cookies() will now also set the non-httponly CSRF cookies# as well@app.route('/token/auth',methods=['POST'])deflogin():username=request.json.get('username',None)password=request.json.get('password',None)ifusername!='test'orpassword!='test':returnjsonify({'login':False}),401# Create the tokens we will be sending back to the useraccess_token=create_access_token(identity=username)refresh_token=create_refresh_token(identity=username)# Set the JWTs and the CSRF double submit protection cookies# in this responseresp=jsonify({'login':True})set_access_cookies(resp,access_token)set_refresh_cookies(resp,refresh_token)returnresp,200@app.route('/token/refresh',methods=['POST'])@jwt_refresh_token_requireddefrefresh():# Create the new access tokencurrent_user=get_jwt_identity()access_token=create_access_token(identity=current_user)# Set the access JWT and CSRF double submit protection cookies# in this responseresp=jsonify({'refresh':True})set_access_cookies(resp,access_token)returnresp,200# Because the JWTs are stored in an httponly cookie now, we cannot# log the user out by simply deleting the cookie in the frontend.# We need the backend to send us a response to delete the cookies# in order to logout. unset_jwt_cookies is a helper function to# do just that.@app.route('/token/remove',methods=['POST'])deflogout():resp=jsonify({'logout':True})unset_jwt_cookies(resp)returnresp,200@app.route('/api/example',methods=['GET'])@jwt_requireddefprotected():username=get_jwt_identity()returnjsonify({'hello':'from {}'.format(username)}),200if__name__=='__main__':app.run()

By default, the CSRF double submit values are sent back as additional cookies
to the caller. If you prefer, you can disable that, and send them back directly
to the caller, like such:

app.config['JWT_CSRF_IN_COOKIES']=False#...#...#...@app.route('/token/auth',methods=['POST'])deflogin():username=request.json.get('username',None)password=request.json.get('password',None)ifusername!='test'orpassword!='test':returnjsonify({'login':False}),401# Create the tokens we will be sending back to the useraccess_token=create_access_token(identity=username)refresh_token=create_refresh_token(identity=username)# Return the double submit values in the resulting JSON# instead of in additional cookiesresp=jsonify({'access_csrf':get_csrf_token(access_token),'refresh_csrf':get_csrf_token(refresh_token)})# We still need to call these functions to set the# JWTs in the cookiesset_access_cookies(resp,access_token)set_refresh_cookies(resp,refresh_token)returnresp,200