Skip to content

Commit 1fabb1c

Browse files
committed
Update more documentation, add cookie and csrf docs
1 parent 5383253 commit 1fabb1c

File tree

9 files changed

+220
-639
lines changed

9 files changed

+220
-639
lines changed

README.md

Lines changed: 14 additions & 475 deletions
Large diffs are not rendered by default.
Lines changed: 28 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,31 @@
11
Blacklist and Token Revoking
22
============================
33

4-
This supports optional blacklisting and token revoking out of the box. This will allow you to revoke a specific token so a user can no longer access your endpoints. In order to revoke a token, we need some storage where we can save a list of all the tokens we have created, as well as if they have been revoked or not. In order to make the underlying storage as agnostic as possible, we use simplekv to provide assess to a variety of backends.
5-
6-
In production, it is important to use a backend that can have some sort of persistent storage, so we don't 'forget' that we revoked a token if the flask process is restarted. We also need something that can be safely used by the multiple thread and processes running your application. At present we believe redis is a good fit for this. It has the added benefit of removing expired tokens from the store automatically, so it wont blow up into something huge.
7-
8-
We also have choose what tokens we want to check against the blacklist. We could check all tokens (refresh and access), or only the refresh tokens. There are pros and cons to either way (extra overhead on jwt_required endpoints vs someone being able to use an access token freely until it expires). In this example, we are going to only check refresh tokens, and set the access tokes to a small expires time to help minimize damage that could be done with a stolen access token.
9-
10-
.. code-block:: python
11-
12-
from datetime import timedelta
13-
14-
15-
import simplekv
16-
import simplekv.memory
17-
from flask import Flask, request, jsonify
18-
19-
from flask_jwt_extended import JWTManager, jwt_required, \
20-
get_jwt_identity, revoke_token, unrevoke_token, \
21-
get_stored_tokens, get_all_stored_tokens, create_access_token, \
22-
create_refresh_token, jwt_refresh_token_required
23-
24-
# Setup flask
25-
app = Flask(__name__)
26-
app.secret_key = 'super-secret'
27-
28-
# Configure access token expires time
29-
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(minutes=5)
30-
31-
# Enable and configure the JWT blacklist / token revoke. We are using an in
32-
# memory store for this example. In production, you should use something
33-
# persistant (such as redis, memcached, sqlalchemy). See here for options:
34-
# http://pythonhosted.org/simplekv/
35-
app.config['JWT_BLACKLIST_ENABLED'] = True
36-
app.config['JWT_BLACKLIST_STORE'] = simplekv.memory.DictStore()
37-
app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = 'refresh'
38-
39-
jwt = JWTManager(app)
40-
41-
42-
@app.route('/login', methods=['POST'])
43-
def login():
44-
username = request.json.get('username', None)
45-
password = request.json.get('password', None)
46-
if username != 'test' and password != 'test':
47-
return jsonify({"msg": "Bad username or password"}), 401
48-
49-
ret = {
50-
'access_token': create_access_token(identity=username),
51-
'refresh_token': create_refresh_token(identity=username)
52-
}
53-
return jsonify(ret), 200
54-
55-
56-
@app.route('/refresh', methods=['POST'])
57-
@jwt_refresh_token_required
58-
def refresh():
59-
current_user = get_jwt_identity()
60-
ret = {
61-
'access_token': create_access_token(identity=current_user)
62-
}
63-
return jsonify(ret), 200
64-
65-
66-
# Endpoint for listing tokens that have the same identity as you
67-
@app.route('/auth/tokens', methods=['GET'])
68-
@jwt_required
69-
def list_identity_tokens():
70-
username = get_jwt_identity()
71-
return jsonify(get_stored_tokens(username)), 200
72-
73-
74-
# Endpoint for listing all tokens. In your app, you should either not expose
75-
# this endpoint, or put some addition security on top of it so only trusted users,
76-
# (administrators, etc) can access it
77-
@app.route('/auth/all-tokens')
78-
def list_all_tokens():
79-
return jsonify(get_all_stored_tokens()), 200
80-
81-
82-
# Endpoint for allowing users to revoke their tokens
83-
@app.route('/auth/tokens/revoke/<string:jti>', methods=['PUT'])
84-
@jwt_required
85-
def change_jwt_revoke_state(jti):
86-
username = jwt_get_identity()
87-
try:
88-
token_data = get_stored_token(jti)
89-
if token_data['token']['identity'] != username:
90-
raise KeyError
91-
revoke_token(jti)
92-
return jsonify({"msg": "Token successfully revoked"}), 200
93-
except KeyError:
94-
return jsonify({'msg': 'Token not found'}), 404
95-
96-
97-
# Endpoint for allowing users to unrevoke their tokens
98-
@app.route('/auth/tokens/unrevoke/<string:jti>', methods=['PUT'])
99-
@jwt_required
100-
def change_jwt_unrevoke_state(jti):
101-
username = jwt_get_identity()
102-
try:
103-
token_data = get_stored_token(jti)
104-
if token_data['token']['identity'] != username:
105-
raise KeyError
106-
unrevoke_token(jti)
107-
return jsonify({"msg": "Token successfully unrevoked"}), 200
108-
except KeyError:
109-
return jsonify({'msg': 'Token not found'}), 404
110-
111-
112-
@app.route('/protected', methods=['GET'])
113-
@jwt_required
114-
def protected():
115-
return jsonify({'hello': 'world'})
116-
117-
if __name__ == '__main__':
118-
app.run()
4+
This extension supports optional token revoking out of the box. This will
5+
allow you to revoke a specific token so that it can no longer access your endpoints.
6+
In order to revoke a token, we need some storage where we can save a list of all
7+
the tokens we have created, as well as if they have been revoked or not. In order
8+
to make the underlying storage as agnostic as possible, we use `simplekv
9+
<http://pythonhosted.org/simplekv/>`_ to provide assess to a variety of backends.
10+
11+
In production, it is important to use a backend that can have some sort of
12+
persistent storage, so we don't 'forget' that we revoked a token if the flask
13+
process is restarted. We also need something that can be safely used by the
14+
multiple thread and processes running your application. At present we believe
15+
redis is a good fit for this. It has the added benefit of removing expired tokens
16+
from the store automatically, so it wont blow up into something huge.
17+
18+
We also have choose what tokens we want to check against the blacklist. We could
19+
check all tokens (refresh and access), or only the refresh tokens. There are pros
20+
and cons to either way (extra overhead on jwt_required endpoints vs someone being
21+
able to use an access token freely until it expires). In this example, we are going
22+
to only check refresh tokens, and set the access tokes to a small expires time to
23+
help minimize damage that could be done with a stolen access token.
24+
25+
.. literalinclude:: ../examples/blacklist.py
26+
27+
It's worth noting that if your selected backend support the `time to live mixin
28+
<http://pythonhosted.org/simplekv/#simplekv.TimeToLiveMixin>`_ (such as redis),
29+
keys will be automatically deleted from the store at some point after they have
30+
expired. This prevents your store from blowing up with old keys without you having
31+
to do any work to prune it back down.

docs/index.rst

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,13 @@ Welcome to flask-jwt-extended's documentation!
1111

1212
installation
1313
basic_usage
14-
1514
add_custom_data_claims
1615
refresh_tokens
1716
token_freshness
1817
changing_default_behavior
1918
options
2019
blacklist_and_token_revoking
21-
testing_and_coverage
22-
23-
documentation
24-
20+
tokens_in_cookies
2521

2622

2723
Indices and tables

docs/options.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ You can change many options for how this extension works via
1010
The available options are:
1111

1212
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
13+
1314
================================= =========================================
1415
``JWT_TOKEN_LOCATION`` Where to find the JWT in the request. The options are ``'headers'`` or
1516
``'cookies'``. Defaults to ``'headers'``
16-
``JWT_HEADER_NAME`` What header to look for the JWT in a request. Only has an effect if
17-
JWT_TOKEN_LOCATION is 'headers'. Defaults to ``'Authorization'``
17+
``JWT_HEADER_NAME`` What header to look for the JWT in a request. Only used if we are sending
18+
the JWT in via headers. Defaults to ``'Authorization'``
1819
``JWT_HEADER_TYPE`` What type of header the JWT is in. Defaults to ``'Bearer'``. This can be
1920
an empty string, in which case the header only contains the JWT
20-
``JWT_COOKIE_CSRF_PROTECT`` Enable/disable CSRF protection when using 'cookies' as the JWT_TOKEN_LOCATION.
21-
This has no affect if using 'headers' as the JWT_TOKEN_LOCATION
21+
``JWT_COOKIE_CSRF_PROTECT`` Enable/disable CSRF protection. Only used when sending the JWT in via cookies
2222
``JWT_ACCESS_CSRF_COOKIE_NAME`` Name of the CSRF access cookie. Defaults to ``'csrf_access_token'``. Only used
2323
if using cookies with CSRF protection enabled
2424
``JWT_REFRESH_CSRF_COOKIE_NAME`` Name of the CSRF refresh cookie. Defaults to ``'csrf_refresh_token'``. Only used
@@ -33,7 +33,7 @@ The available options are:
3333
<https://pyjwt.readthedocs.io/en/latest/algorithms.html>`_ for the options. Defaults
3434
to ``'HS256'``. Note that Asymmetric (Public-key) Algorithms are not currently supported.
3535
``JWT_BLACKLIST_ENABLED`` Enable/disable token blackliting and revoking. Defaults to ``False``
36-
``JWT_BLACKLIST_STORE`` Where to save revoked tokens. `See here
36+
``JWT_BLACKLIST_STORE`` Where to save created and revoked tokens. `See here
3737
<http://pythonhosted.org/simplekv/>`_ for options.
3838
``JWT_BLACKLIST_CHECKS`` What token types to check against the blacklist. Options are
3939
``'refresh'`` or ``'all'``. Defaults to ``'refresh'``

docs/tokens_in_cookies.rst

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
JWT in Cookies
2+
==============
3+
If the frontend that is consuming this backend is a website, you may be tempted
4+
to store your JWTs in the browser localStorage or sessionStorage. There is nothing
5+
necessarily wrong with this, but if you have any sort of XSS vulnerability on your
6+
site, an attacker will be able to trivially steal your refresh and access tokens.
7+
If you want some additional security on your site, you can save your JWTs in a
8+
httponly cookie instead, which keeps javascript from being able to access the
9+
cookie. See this great blog for a more in depth analysis between these options:
10+
https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage.
11+
12+
Here is a basic example of how to store JWTs in cookies:
13+
14+
.. literalinclude:: ../examples/jwt_in_cookie.py
15+
16+
This isn't the full story however. We can now keep our cookie from being stolen via XSS
17+
attacks, but have traded that for a vulnerability to CSRF attacks. To combat
18+
CSRF, we are going to use a technique called double submit verification.
19+
20+
When we create a JWT, we will also create a random string and store it in the JWT. This token is saved
21+
in a cookie with httponly set to True, so it cannot be accessed via javascript.
22+
We will then create a secondary cookie that contains only the random string, but
23+
has httponly set to False, so that it can be accessed via javascript running on
24+
your website. Now in order to access a protected endpoint,
25+
you will need to add a custom header that contains the the random string in it,
26+
and if that header doesn't exist or it doesn't match the string that is stored
27+
in the JWT, the request will be kicked out as unauthorized.
28+
29+
To break this down, if an attacker attempts to perform a CSRF attack they will
30+
send the JWT (via the cookie) to a protected endpoint, but without the random
31+
string in the requests header, they wont be able to access the endpoint. They
32+
cannot access the random string, unless they can run javascript on your website
33+
(likely via an XSS attack), and if they are able to perform an XSS attack, they
34+
will not be able to steal the actual access and refresh JWTs, as javascript is
35+
still not able to access those httponly cookies.
36+
37+
This obviously isn't a golden bullet. If an attacker can perform an XSS attack they can
38+
still access protected endpoint from people who visit your site. However, it is better
39+
then if they were able to steal the access and refresh tokens tokens from
40+
local/session storage, and do whatever they wanted with them. If this additional
41+
security is worth the added complexity of using cookies and double submit CSRF
42+
protection is a choice you will have to make.
43+
44+
Here is an example of what this would look like:
45+
46+
.. literalinclude:: ../examples/csrf_protection_with_cookies.py

examples/blacklist.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
from datetime import timedelta
1+
import datetime
22

3-
import simplekv
43
import simplekv.memory
54
from flask import Flask, request, jsonify
65

@@ -9,24 +8,28 @@
98
get_stored_tokens, get_all_stored_tokens, create_access_token, \
109
create_refresh_token, jwt_refresh_token_required, get_stored_token
1110

11+
1212
# Setup flask
1313
app = Flask(__name__)
1414
app.secret_key = 'super-secret'
1515

16-
# Configure access token expires time
17-
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(minutes=5)
18-
19-
# Enable and configure the JWT blacklist / token revoke. We are using an in
20-
# memory store for this example. In production, you should use something
21-
# persistant (such as redis, memcached, sqlalchemy). See here for options:
22-
# http://pythonhosted.org/simplekv/
16+
# Enable and configure the JWT blacklist / token revoke. We are using
17+
# an in memory store for this example. In production, you should
18+
# use something persistent (such as redis, memcached, sqlalchemy).
19+
# See here for options: http://pythonhosted.org/simplekv/
2320
app.config['JWT_BLACKLIST_ENABLED'] = True
2421
app.config['JWT_BLACKLIST_STORE'] = simplekv.memory.DictStore()
22+
23+
# Only check the refresh token for being revoked, and set a small time to live
24+
# on the access tokens to prevent a compromised one from being used for a long
25+
# period of time
2526
app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = 'refresh'
27+
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = datetime.timedelta(minutes=3)
2628

2729
jwt = JWTManager(app)
2830

2931

32+
# Standard login endpoint
3033
@app.route('/login', methods=['POST'])
3134
def login():
3235
username = request.json.get('username', None)
@@ -41,6 +44,7 @@ def login():
4144
return jsonify(ret), 200
4245

4346

47+
# Standard refresh endpoint
4448
@app.route('/refresh', methods=['POST'])
4549
@jwt_refresh_token_required
4650
def refresh():
@@ -59,15 +63,15 @@ def list_identity_tokens():
5963
return jsonify(get_stored_tokens(username)), 200
6064

6165

62-
# Endpoint for listing all tokens. In your app, you should either not expose
63-
# this endpoint, or put some addition security on top of it so only trusted users,
64-
# (administrators, etc) can access it
66+
# Endpoint for listing all tokens. In your app, you should either
67+
# not expose this endpoint, or put some addition security on top
68+
# of it so only trusted users (administrators, etc) can access it
6569
@app.route('/auth/all-tokens')
6670
def list_all_tokens():
6771
return jsonify(get_all_stored_tokens()), 200
6872

6973

70-
# Endpoint for allowing users to revoke their tokens
74+
# Endpoint for allowing users to revoke their own tokens.
7175
@app.route('/auth/tokens/revoke/<string:jti>', methods=['PUT'])
7276
@jwt_required
7377
def change_jwt_revoke_state(jti):
@@ -82,6 +86,7 @@ def change_jwt_revoke_state(jti):
8286
return jsonify({'msg': 'Token not found'}), 404
8387

8488

89+
# Endpoint for allowing users to un-revoke their own tokens.
8590
@app.route('/auth/tokens/unrevoke/<string:jti>', methods=['PUT'])
8691
@jwt_required
8792
def change_jwt_unrevoke_state(jti):

0 commit comments

Comments
 (0)