Skip to content

Commit 401c602

Browse files
committed
Blacklist/revoke tokens example
1 parent ca576a4 commit 401c602

File tree

2 files changed

+220
-2
lines changed

2 files changed

+220
-2
lines changed

README.md

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,127 @@ will only check refresh tokens, and 'all' which will check refresh and access to
340340
to 'refresh'
341341

342342
### Blacklist and Token Revoking
343-
TODO
343+
This supports blacklisting and token revoking out of the box. This will allow you
344+
to revoke a specific token so a user can no longer access your endpoints, without
345+
having to change your secret key and thus revoke all the users tokens. In order
346+
to revoke a token, we need some storage where we can save a list of all the tokens
347+
we have created, as well as if they have been blacklisted or not. In order to make
348+
the underlying storage as agnostic as possible, we use [simplekv] (http://pythonhosted.org/simplekv/)
349+
to provide assess to a variaty of backends.
350+
351+
In production, it is important to use a backend that can have some sort of
352+
persistent storage, so we don't forget that we revoked a token, as well as
353+
something that can be safely used by the multiple thread and processes running
354+
your application. At present we believe redis is a good fit for this (it has the
355+
added benefit of removing expired tokens from the store automatically, so it
356+
wont blow up into something huge). But the choice is of course yours.
357+
358+
We also have to make a choice of if we want to check the blacklist against all
359+
requests, or only against refresh token requests. There are pros and cons to either
360+
way (extra overhead on jwt_required endpoints vs someone being able to use an
361+
access token freely until it expires). In this example, we are going to only check
362+
refresh tokens, and set the access tokes to a small expires time to help minimize
363+
damange that could be done.
364+
```python
365+
from datetime import timedelta
366+
367+
import simplekv
368+
import simplekv.memory
369+
from flask import Flask, request, jsonify
370+
371+
from flask_jwt_extended import JWTManager, jwt_required, \
372+
get_jwt_identity, revoke_token, unrevoke_token, \
373+
get_stored_tokens, get_all_stored_tokens, create_access_token, \
374+
create_refresh_token, jwt_refresh_token_required
375+
376+
# Setup flask
377+
app = Flask(__name__)
378+
app.secret_key = 'super-secret'
379+
380+
# Configure access token expires time
381+
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(minutes=5)
382+
383+
# Enable and configure the JWT blacklist / token revoke. We are using an in
384+
# memory store for this example. In production, you should use something
385+
# else (csuch as redis, memcached, sqlalchemy). See here for options:
386+
# http://pythonhosted.org/simplekv/
387+
app.config['JWT_BLACKLIST_ENABLED'] = True
388+
app.config['JWT_BLACKLIST_STORE'] = simplekv.memory.DictStore()
389+
app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = 'refresh'
390+
391+
jwt = JWTManager(app)
392+
393+
394+
@app.route('/login', methods=['POST'])
395+
def login():
396+
username = request.json.get('username', None)
397+
password = request.json.get('password', None)
398+
if username != 'test' and password != 'test':
399+
return jsonify({"msg": "Bad username or password"}), 401
400+
401+
ret = {
402+
'access_token': create_access_token(identity=username),
403+
'refresh_token': create_refresh_token(identity=username)
404+
}
405+
return jsonify(ret), 200
406+
407+
408+
@app.route('/refresh', methods=['POST'])
409+
@jwt_refresh_token_required
410+
def refresh():
411+
current_user = get_jwt_identity()
412+
ret = {
413+
'access_token': create_access_token(identity=current_user)
414+
}
415+
return jsonify(ret), 200
416+
417+
418+
# Endpoint for listing tokens that have the same identity as you
419+
@app.route('/auth/tokens', methods=['GET'])
420+
@jwt_required
421+
def list_identity_tokens():
422+
username = get_jwt_identity()
423+
return jsonify(get_stored_tokens(username)), 200
424+
425+
426+
# Endpoint for listing all tokens. In your app, you should either not expose
427+
# this, or put some addition security on top of it so only trusted users,
428+
# (administrators, etc) can access it
429+
@app.route('/auth/all-tokens')
430+
def list_all_tokens():
431+
return jsonify(get_all_stored_tokens()), 200
432+
433+
434+
# Endpoint for revoking a token
435+
@app.route('/auth/tokens/revoke/<string:jti>', methods=['PUT'])
436+
@jwt_required
437+
def change_jwt_revoke_state(jti):
438+
try:
439+
revoke_token(jti)
440+
return jsonify({"msg": "Token successfully revoked"}), 200
441+
except KeyError:
442+
return jsonify({'msg': 'Token not foun'}), 404
443+
444+
445+
# Endpoint for un-revoking a token
446+
@app.route('/auth/tokens/unrevoke/<string:jti>', methods=['PUT'])
447+
@jwt_required
448+
def change_jwt_revoke_state(jti):
449+
try:
450+
unrevoke_token(jti)
451+
return jsonify({"msg": "Token successfully unrevoked"}), 200
452+
except KeyError:
453+
return jsonify({'msg': 'Token not foun'}), 404
454+
455+
456+
@app.route('/protected', methods=['GET'])
457+
@jwt_required
458+
def protected():
459+
return jsonify({'hello': 'world'})
460+
461+
if __name__ == '__main__':
462+
app.run()
463+
```
344464

345465

346466
# Testing and Code Coverage
@@ -350,4 +470,4 @@ tox
350470
```
351471

352472
# Documentation
353-
Readthedocs coming soon!
473+
Readthedocs coming soon(tm)!

examples/blacklist.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from datetime import timedelta
2+
3+
import simplekv
4+
import simplekv.memory
5+
from flask import Flask, request, jsonify
6+
7+
from flask_jwt_extended import JWTManager, jwt_required, \
8+
get_jwt_identity, revoke_token, unrevoke_token, \
9+
get_stored_tokens, get_all_stored_tokens, create_access_token, \
10+
create_refresh_token, jwt_refresh_token_required
11+
12+
# Setup flask
13+
app = Flask(__name__)
14+
app.secret_key = 'super-secret'
15+
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+
# else (csuch as redis, memcached, sqlalchemy). See here for options:
22+
# http://pythonhosted.org/simplekv/
23+
app.config['JWT_BLACKLIST_ENABLED'] = True
24+
app.config['JWT_BLACKLIST_STORE'] = simplekv.memory.DictStore()
25+
app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = 'refresh'
26+
27+
jwt = JWTManager(app)
28+
29+
30+
@app.route('/login', methods=['POST'])
31+
def login():
32+
username = request.json.get('username', None)
33+
password = request.json.get('password', None)
34+
if username != 'test' and password != 'test':
35+
return jsonify({"msg": "Bad username or password"}), 401
36+
37+
ret = {
38+
'access_token': create_access_token(identity=username),
39+
'refresh_token': create_refresh_token(identity=username)
40+
}
41+
return jsonify(ret), 200
42+
43+
44+
@app.route('/refresh', methods=['POST'])
45+
@jwt_refresh_token_required
46+
def refresh():
47+
current_user = get_jwt_identity()
48+
ret = {
49+
'access_token': create_access_token(identity=current_user)
50+
}
51+
return jsonify(ret), 200
52+
53+
54+
# Endpoint for listing tokens that have the same identity as you
55+
@app.route('/auth/tokens', methods=['GET'])
56+
@jwt_required
57+
def list_identity_tokens():
58+
username = get_jwt_identity()
59+
return jsonify(get_stored_tokens(username)), 200
60+
61+
62+
# Endpoint for listing all tokens. In your app, you should either not expose
63+
# this, or put some addition security on top of it so only trusted users,
64+
# (administrators, etc) can access it
65+
@app.route('/auth/all-tokens')
66+
def list_all_tokens():
67+
return jsonify(get_all_stored_tokens()), 200
68+
69+
70+
# Endpoint for revoking a token
71+
@app.route('/auth/tokens/revoke/<string:jti>', methods=['PUT'])
72+
@jwt_required
73+
def change_jwt_revoke_state(jti):
74+
try:
75+
revoke_token(jti)
76+
return jsonify({"msg": "Token successfully revoked"}), 200
77+
except KeyError:
78+
return jsonify({'msg': 'Token not foun'}), 404
79+
80+
81+
# Endpoint for un-revoking a token
82+
@app.route('/auth/tokens/unrevoke/<string:jti>', methods=['PUT'])
83+
@jwt_required
84+
def change_jwt_revoke_state(jti):
85+
try:
86+
unrevoke_token(jti)
87+
return jsonify({"msg": "Token successfully unrevoked"}), 200
88+
except KeyError:
89+
return jsonify({'msg': 'Token not foun'}), 404
90+
91+
92+
@app.route('/protected', methods=['GET'])
93+
@jwt_required
94+
def protected():
95+
return jsonify({'hello': 'world'})
96+
97+
if __name__ == '__main__':
98+
app.run()

0 commit comments

Comments
 (0)