|
1 | 1 | Blacklist and Token Revoking |
2 | 2 | ============================ |
3 | 3 |
|
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. |
0 commit comments