Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitmodules

This file was deleted.

7 changes: 1 addition & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,7 @@ $ tox
```

### Generating Documentation
You can generate a local copy of the documentation. First, make sure you have
the flask sphinx theme cloned
```
$ git submodule update --init
```

You can generate a local copy of the documentation. In the `docs` directory, run:
Then, in the docs directory, run
```
$ make clean && make html
Expand Down
1 change: 0 additions & 1 deletion docs/_themes
Submodule _themes deleted from d5b657
62 changes: 33 additions & 29 deletions docs/blacklist_and_token_revoking.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,39 @@ Blacklist and Token Revoking

This extension supports optional token revoking out of the box. This will
allow you to revoke a specific token so that it 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
<http://pythonhosted.org/simplekv/>`_ to provide assess to a variety of backends.

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.

We also have to 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, namely extra overhead on jwt_required endpoints vs someone
being able to use an access token freely until it expires. In this example, we are
looking at all tokens:

You will have to choose what tokens you want to check against the blacklist. In
most cases, you will probably want to check both refresh and access tokens, which
is the default behavior. However, if the extra overhead of checking tokens is a
concern you could instead only check the refresh tokens, and set the access
tokens to have a short expires time so any damage a compromised token could
cause is minimal.

Blacklisting works by is providing a callback function to this extension, using the
**@jwt.token_in_blacklist_loader** decorator. This method will be called whenever the
specified tokens (``'access'`` and/or ``'refresh'``) are used to access a protected endpoint.
If the callback function says that the token is revoked, we will not allow the
call to continue, otherwise we will allow the call to access the endpoint as normal.


Here is a basic example of this in action.


.. literalinclude:: ../examples/blacklist.py

If you want better performance (ie, not having to check the blacklist store
with every request), you could check only the refresh tokens. This makes it
so any call to a jwt_required endpoint does not need to check the blacklist
store, but on the flip side would allow a compromised access token to be used
until it expired. If using the approach, you should set the access tokens to
have a very short lifetime to help combat this.

It's worth noting that if your selected backend support the `time to live mixin
<http://pythonhosted.org/simplekv/#simplekv.TimeToLiveMixin>`_ (such as redis),
keys will be automatically deleted from the store at some point after they have
expired. This prevents your store from blowing up with old keys without you having
to do any work to prune it back down.
In production, you will likely want to use either a database or in memory store
(such as redis) to store your tokens. In memory stores are great if you are wanting
to revoke a token when the users logs out, as they are blazing fast. A downside
to using redis is that in the case of a power outage or other such event, it's
possible that you might 'forget' that some tokens have been revoked, depending
on if the redis data was synced to disk.

In contrast to that, databases are great if the data persistance is of the highest
importance (for example, if you have very long lived tokens that other developers
use to access your api), or if you want to add some addition features like showing
users all of their active tokens, and letting them revoke and unrevoke those tokens.

For more in depth examples of these, check out:

- https://github.com/vimalloc/flask-jwt-extended/examples/redis_blacklist.py
- https://github.com/vimalloc/flask-jwt-extended/examples/database_blacklist
8 changes: 3 additions & 5 deletions docs/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,8 @@ Blacklist Options:

================================= =========================================
``JWT_BLACKLIST_ENABLED`` Enable/disable token blacklisting and revoking. Defaults to ``False``
``JWT_BLACKLIST_STORE`` Where to save created and revoked tokens. `See here
<http://pythonhosted.org/simplekv/>`_ for options.
Only used if blacklisting is enabled.
``JWT_BLACKLIST_TOKEN_CHECKS`` What token types to check against the blacklist. Options are
``'refresh'`` or ``'all'``. Defaults to ``'refresh'``.
``JWT_BLACKLIST_TOKEN_CHECKS`` What token types to check against the blacklist. The options are
``'refresh'`` or ``'access'``. You can pass in a list to check
more then one type. Defaults to ``['access', 'refresh']``.
Only used if blacklisting is enabled.
================================= =========================================
136 changes: 45 additions & 91 deletions examples/blacklist.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,48 @@
import datetime

import simplekv.memory
from flask import Flask, request, jsonify

from flask_jwt_extended import JWTManager, jwt_required, \
get_jwt_identity, revoke_token, unrevoke_token, \
get_stored_tokens, get_all_stored_tokens, create_access_token, \
create_refresh_token, jwt_refresh_token_required, \
get_raw_jwt, get_stored_token
from flask_jwt_extended import (
JWTManager, jwt_required, get_jwt_identity,
create_access_token, create_refresh_token,
jwt_refresh_token_required, get_raw_jwt
)


# Setup flask
app = Flask(__name__)
app.secret_key = 'super-secret'
app.secret_key = 'ChangeMe!'

# Enable and configure the JWT blacklist / token revoke. We are using
# an in memory store for this example. In production, you should
# use something persistent (such as redis, memcached, sqlalchemy).
# See here for options: http://pythonhosted.org/simplekv/
# Enable blacklisting and specify what kind of tokens to check
# against the blacklist
app.config['JWT_BLACKLIST_ENABLED'] = True
app.config['JWT_BLACKLIST_STORE'] = simplekv.memory.DictStore()

# Check all tokens (access and refresh) to see if they have been revoked.
# You can alternately check only the refresh tokens here, by setting this
# to 'refresh' instead of 'all'
app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = 'all'
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = datetime.timedelta(minutes=5)

app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['access', 'refresh']
jwt = JWTManager(app)

# A storage engine to save revoked tokens. In production if
# speed is the primary concern, redis is a good bet. If data
# persistence is more important for you, postgres is another
# great option. In this example, we will be using an in memory
# store, just to show you how this might work. For more
# complete examples, check out these:
# https://github.com/vimalloc/flask-jwt-extended/examples/redis_blacklist.py
# https://github.com/vimalloc/flask-jwt-extended/examples/database_blacklist
blacklist = set()


# For this example, we are just checking if the tokens jti
# (unique identifier) is in the blacklist set. This could
# be made more complex, for example storing all tokens
# into the blacklist with a revoked status when created,
# and returning the revoked status in this call. This
# would allow you to have a list of all created tokens,
# and to consider tokens that aren't in the blacklist
# (aka tokens you didn't create) as revoked. These are
# just two options, and this can be tailored to whatever
# your application needs.
@jwt.token_in_blacklist_loader
def check_if_token_in_blacklist(decrypted_token):
jti = decrypted_token['jti']
return jti in blacklist


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


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


# Helper method to revoke the current token used to access
# a protected endpoint
def _revoke_current_token():
current_token = get_raw_jwt()
jti = current_token['jti']
revoke_token(jti)


# Endpoint for revoking the current users access token
@app.route('/logout', methods=['POST'])
@app.route('/logout', methods=['DELETE'])
@jwt_required
def logout():
try:
_revoke_current_token()
except KeyError:
return jsonify({
'msg': 'Access token not found in the blacklist store'
}), 500
jti = get_raw_jwt()['jti']
blacklist.add(jti)
return jsonify({"msg": "Successfully logged out"}), 200


# Endpoint for revoking the current users refresh token
@app.route('/logout2', methods=['POST'])
@app.route('/logout2', methods=['DELETE'])
@jwt_refresh_token_required
def logout2():
try:
_revoke_current_token()
except KeyError:
return jsonify({
'msg': 'Refresh token not found in the blacklist store'
}), 500
jti = get_raw_jwt()['jti']
blacklist.add(jti)
return jsonify({"msg": "Successfully logged out"}), 200


# Endpoint for listing tokens that have the same identity as you
# NOTE: This is currently very inefficient.
@app.route('/auth/tokens', methods=['GET'])
@jwt_required
def list_identity_tokens():
username = get_jwt_identity()
return jsonify(get_stored_tokens(username)), 200


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


# Endpoint for allowing users to revoke their own tokens.
@app.route('/auth/tokens/revoke/<string:jti>', methods=['PUT'])
@jwt_required
def change_jwt_revoke_state(jti):
username = get_jwt_identity()
try:
token_data = get_stored_token(jti)
if token_data['token']['identity'] != username:
raise KeyError
revoke_token(jti)
return jsonify({"msg": "Token successfully revoked"}), 200
except KeyError:
return jsonify({'msg': 'Token not found'}), 404


# Endpoint for allowing users to un-revoke their own tokens.
@app.route('/auth/tokens/unrevoke/<string:jti>', methods=['PUT'])
@jwt_required
def change_jwt_unrevoke_state(jti):
username = get_jwt_identity()
try:
token_data = get_stored_token(jti)
if token_data['token']['identity'] != username:
raise KeyError
unrevoke_token(jti)
return jsonify({"msg": "Token successfully unrevoked"}), 200
except KeyError:
return jsonify({'msg': 'Token not found'}), 404


# This will now prevent users with blacklisted tokens from
# accessing this endpoint
@app.route('/protected', methods=['GET'])
@jwt_required
def protected():
Expand Down
23 changes: 23 additions & 0 deletions examples/database_blacklist/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Blacklist with a database
Database are a common choice for storing blacklist tokens. It has many
benefits over an in memory store, like redis. The most obvious benefit of
using a database is data consistency. If you add something to the database,
you don't need to worry about it vanishing in an event like a power outage.
This is huge if you need to revoke long lived keys (for example, keys that
you give to another developer so they can access your API). Another advantage
of using a database is that you have easy access to all of the relational
data stored in there. You can easily and efficiently get a list of all tokens
that belong to a given user, and revoke or unrevoke those tokens with ease.
This is very handy if you want to provide a user with a way to see all the
active tokens they have with your service.

Databases also have some cons compared to an in memory store, namely that
they are potentially slower, and they may grow huge over time and need to be
manually pruned back down.

This project contains example code for you you might implement a blacklist
using a database, with some more complex features that might benefit your
application. For ease of use, we will use flask-sqlalchey with an in
memory data store, but in production I would highly recommend using postgres.
Please note that this code is only an example, and although I do my best to
insure its quality, it has not been thoroughly tested.
Empty file.
Loading