Skip to content

Commit 10c6802

Browse files
authored
Merge pull request #63 from vimalloc/blacklist_v2
Blacklist v2
2 parents 4b56bf3 + 13301b2 commit 10c6802

26 files changed

+692
-766
lines changed

.gitmodules

-3
This file was deleted.

README.md

+1-6
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,7 @@ $ tox
3737
```
3838

3939
### Generating Documentation
40-
You can generate a local copy of the documentation. First, make sure you have
41-
the flask sphinx theme cloned
42-
```
43-
$ git submodule update --init
44-
```
45-
40+
You can generate a local copy of the documentation. In the `docs` directory, run:
4641
Then, in the docs directory, run
4742
```
4843
$ make clean && make html

docs/_themes

-1
This file was deleted.

docs/blacklist_and_token_revoking.rst

+33-29
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,39 @@ Blacklist and Token Revoking
33

44
This extension supports optional token revoking out of the box. This will
55
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 to 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, namely extra overhead on jwt_required endpoints vs someone
21-
being able to use an access token freely until it expires. In this example, we are
22-
looking at all tokens:
6+
7+
You will have to choose what tokens you want to check against the blacklist. In
8+
most cases, you will probably want to check both refresh and access tokens, which
9+
is the default behavior. However, if the extra overhead of checking tokens is a
10+
concern you could instead only check the refresh tokens, and set the access
11+
tokens to have a short expires time so any damage a compromised token could
12+
cause is minimal.
13+
14+
Blacklisting works by is providing a callback function to this extension, using the
15+
**@jwt.token_in_blacklist_loader** decorator. This method will be called whenever the
16+
specified tokens (``'access'`` and/or ``'refresh'``) are used to access a protected endpoint.
17+
If the callback function says that the token is revoked, we will not allow the
18+
call to continue, otherwise we will allow the call to access the endpoint as normal.
19+
20+
21+
Here is a basic example of this in action.
22+
2323

2424
.. literalinclude:: ../examples/blacklist.py
2525

26-
If you want better performance (ie, not having to check the blacklist store
27-
with every request), you could check only the refresh tokens. This makes it
28-
so any call to a jwt_required endpoint does not need to check the blacklist
29-
store, but on the flip side would allow a compromised access token to be used
30-
until it expired. If using the approach, you should set the access tokens to
31-
have a very short lifetime to help combat this.
32-
33-
It's worth noting that if your selected backend support the `time to live mixin
34-
<http://pythonhosted.org/simplekv/#simplekv.TimeToLiveMixin>`_ (such as redis),
35-
keys will be automatically deleted from the store at some point after they have
36-
expired. This prevents your store from blowing up with old keys without you having
37-
to do any work to prune it back down.
26+
In production, you will likely want to use either a database or in memory store
27+
(such as redis) to store your tokens. In memory stores are great if you are wanting
28+
to revoke a token when the users logs out, as they are blazing fast. A downside
29+
to using redis is that in the case of a power outage or other such event, it's
30+
possible that you might 'forget' that some tokens have been revoked, depending
31+
on if the redis data was synced to disk.
32+
33+
In contrast to that, databases are great if the data persistance is of the highest
34+
importance (for example, if you have very long lived tokens that other developers
35+
use to access your api), or if you want to add some addition features like showing
36+
users all of their active tokens, and letting them revoke and unrevoke those tokens.
37+
38+
For more in depth examples of these, check out:
39+
40+
- https://github.com/vimalloc/flask-jwt-extended/examples/redis_blacklist.py
41+
- https://github.com/vimalloc/flask-jwt-extended/examples/database_blacklist

docs/options.rst

+3-5
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,8 @@ Blacklist Options:
111111

112112
================================= =========================================
113113
``JWT_BLACKLIST_ENABLED`` Enable/disable token blacklisting and revoking. Defaults to ``False``
114-
``JWT_BLACKLIST_STORE`` Where to save created and revoked tokens. `See here
115-
<http://pythonhosted.org/simplekv/>`_ for options.
116-
Only used if blacklisting is enabled.
117-
``JWT_BLACKLIST_TOKEN_CHECKS`` What token types to check against the blacklist. Options are
118-
``'refresh'`` or ``'all'``. Defaults to ``'refresh'``.
114+
``JWT_BLACKLIST_TOKEN_CHECKS`` What token types to check against the blacklist. The options are
115+
``'refresh'`` or ``'access'``. You can pass in a list to check
116+
more then one type. Defaults to ``['access', 'refresh']``.
119117
Only used if blacklisting is enabled.
120118
================================= =========================================

examples/blacklist.py

+45-91
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,48 @@
1-
import datetime
2-
3-
import simplekv.memory
41
from flask import Flask, request, jsonify
52

6-
from flask_jwt_extended import JWTManager, jwt_required, \
7-
get_jwt_identity, revoke_token, unrevoke_token, \
8-
get_stored_tokens, get_all_stored_tokens, create_access_token, \
9-
create_refresh_token, jwt_refresh_token_required, \
10-
get_raw_jwt, get_stored_token
3+
from flask_jwt_extended import (
4+
JWTManager, jwt_required, get_jwt_identity,
5+
create_access_token, create_refresh_token,
6+
jwt_refresh_token_required, get_raw_jwt
7+
)
118

129

1310
# Setup flask
1411
app = Flask(__name__)
15-
app.secret_key = 'super-secret'
12+
app.secret_key = 'ChangeMe!'
1613

17-
# Enable and configure the JWT blacklist / token revoke. We are using
18-
# an in memory store for this example. In production, you should
19-
# use something persistent (such as redis, memcached, sqlalchemy).
20-
# See here for options: http://pythonhosted.org/simplekv/
14+
# Enable blacklisting and specify what kind of tokens to check
15+
# against the blacklist
2116
app.config['JWT_BLACKLIST_ENABLED'] = True
22-
app.config['JWT_BLACKLIST_STORE'] = simplekv.memory.DictStore()
23-
24-
# Check all tokens (access and refresh) to see if they have been revoked.
25-
# You can alternately check only the refresh tokens here, by setting this
26-
# to 'refresh' instead of 'all'
27-
app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = 'all'
28-
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = datetime.timedelta(minutes=5)
29-
17+
app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['access', 'refresh']
3018
jwt = JWTManager(app)
3119

20+
# A storage engine to save revoked tokens. In production if
21+
# speed is the primary concern, redis is a good bet. If data
22+
# persistence is more important for you, postgres is another
23+
# great option. In this example, we will be using an in memory
24+
# store, just to show you how this might work. For more
25+
# complete examples, check out these:
26+
# https://github.com/vimalloc/flask-jwt-extended/examples/redis_blacklist.py
27+
# https://github.com/vimalloc/flask-jwt-extended/examples/database_blacklist
28+
blacklist = set()
29+
30+
31+
# For this example, we are just checking if the tokens jti
32+
# (unique identifier) is in the blacklist set. This could
33+
# be made more complex, for example storing all tokens
34+
# into the blacklist with a revoked status when created,
35+
# and returning the revoked status in this call. This
36+
# would allow you to have a list of all created tokens,
37+
# and to consider tokens that aren't in the blacklist
38+
# (aka tokens you didn't create) as revoked. These are
39+
# just two options, and this can be tailored to whatever
40+
# your application needs.
41+
@jwt.token_in_blacklist_loader
42+
def check_if_token_in_blacklist(decrypted_token):
43+
jti = decrypted_token['jti']
44+
return jti in blacklist
45+
3246

3347
# Standard login endpoint
3448
@app.route('/login', methods=['POST'])
@@ -45,7 +59,8 @@ def login():
4559
return jsonify(ret), 200
4660

4761

48-
# Standard refresh endpoint
62+
# Standard refresh endpoint. A blacklisted refresh token
63+
# will not be able to access this endpoint
4964
@app.route('/refresh', methods=['POST'])
5065
@jwt_refresh_token_required
5166
def refresh():
@@ -56,87 +71,26 @@ def refresh():
5671
return jsonify(ret), 200
5772

5873

59-
# Helper method to revoke the current token used to access
60-
# a protected endpoint
61-
def _revoke_current_token():
62-
current_token = get_raw_jwt()
63-
jti = current_token['jti']
64-
revoke_token(jti)
65-
66-
6774
# Endpoint for revoking the current users access token
68-
@app.route('/logout', methods=['POST'])
75+
@app.route('/logout', methods=['DELETE'])
6976
@jwt_required
7077
def logout():
71-
try:
72-
_revoke_current_token()
73-
except KeyError:
74-
return jsonify({
75-
'msg': 'Access token not found in the blacklist store'
76-
}), 500
78+
jti = get_raw_jwt()['jti']
79+
blacklist.add(jti)
7780
return jsonify({"msg": "Successfully logged out"}), 200
7881

7982

8083
# Endpoint for revoking the current users refresh token
81-
@app.route('/logout2', methods=['POST'])
84+
@app.route('/logout2', methods=['DELETE'])
8285
@jwt_refresh_token_required
8386
def logout2():
84-
try:
85-
_revoke_current_token()
86-
except KeyError:
87-
return jsonify({
88-
'msg': 'Refresh token not found in the blacklist store'
89-
}), 500
87+
jti = get_raw_jwt()['jti']
88+
blacklist.add(jti)
9089
return jsonify({"msg": "Successfully logged out"}), 200
9190

9291

93-
# Endpoint for listing tokens that have the same identity as you
94-
# NOTE: This is currently very inefficient.
95-
@app.route('/auth/tokens', methods=['GET'])
96-
@jwt_required
97-
def list_identity_tokens():
98-
username = get_jwt_identity()
99-
return jsonify(get_stored_tokens(username)), 200
100-
101-
102-
# Endpoint for listing all tokens. In your app, you should either
103-
# not expose this endpoint, or put some addition security on top
104-
# of it so only trusted users (administrators, etc) can access it
105-
@app.route('/auth/all-tokens')
106-
def list_all_tokens():
107-
return jsonify(get_all_stored_tokens()), 200
108-
109-
110-
# Endpoint for allowing users to revoke their own tokens.
111-
@app.route('/auth/tokens/revoke/<string:jti>', methods=['PUT'])
112-
@jwt_required
113-
def change_jwt_revoke_state(jti):
114-
username = get_jwt_identity()
115-
try:
116-
token_data = get_stored_token(jti)
117-
if token_data['token']['identity'] != username:
118-
raise KeyError
119-
revoke_token(jti)
120-
return jsonify({"msg": "Token successfully revoked"}), 200
121-
except KeyError:
122-
return jsonify({'msg': 'Token not found'}), 404
123-
124-
125-
# Endpoint for allowing users to un-revoke their own tokens.
126-
@app.route('/auth/tokens/unrevoke/<string:jti>', methods=['PUT'])
127-
@jwt_required
128-
def change_jwt_unrevoke_state(jti):
129-
username = get_jwt_identity()
130-
try:
131-
token_data = get_stored_token(jti)
132-
if token_data['token']['identity'] != username:
133-
raise KeyError
134-
unrevoke_token(jti)
135-
return jsonify({"msg": "Token successfully unrevoked"}), 200
136-
except KeyError:
137-
return jsonify({'msg': 'Token not found'}), 404
138-
139-
92+
# This will now prevent users with blacklisted tokens from
93+
# accessing this endpoint
14094
@app.route('/protected', methods=['GET'])
14195
@jwt_required
14296
def protected():

examples/database_blacklist/README.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Blacklist with a database
2+
Database are a common choice for storing blacklist tokens. It has many
3+
benefits over an in memory store, like redis. The most obvious benefit of
4+
using a database is data consistency. If you add something to the database,
5+
you don't need to worry about it vanishing in an event like a power outage.
6+
This is huge if you need to revoke long lived keys (for example, keys that
7+
you give to another developer so they can access your API). Another advantage
8+
of using a database is that you have easy access to all of the relational
9+
data stored in there. You can easily and efficiently get a list of all tokens
10+
that belong to a given user, and revoke or unrevoke those tokens with ease.
11+
This is very handy if you want to provide a user with a way to see all the
12+
active tokens they have with your service.
13+
14+
Databases also have some cons compared to an in memory store, namely that
15+
they are potentially slower, and they may grow huge over time and need to be
16+
manually pruned back down.
17+
18+
This project contains example code for you you might implement a blacklist
19+
using a database, with some more complex features that might benefit your
20+
application. For ease of use, we will use flask-sqlalchey with an in
21+
memory data store, but in production I would highly recommend using postgres.
22+
Please note that this code is only an example, and although I do my best to
23+
insure its quality, it has not been thoroughly tested.

examples/database_blacklist/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)