@@ -25,6 +25,14 @@ Live (TTL) functionality when storing a JWT. Here is an example using redis:
25
25
26
26
.. literalinclude :: ../examples/blocklist_redis.py
27
27
28
+
29
+ .. warning ::
30
+ Note that configuring redis to be disk-persistent is an absolutely necessity for
31
+ production use. Otherwise, events like power outages or server crashes/reboots
32
+ would cause all invalidated tokens to become valid again (assuming the
33
+ secret key does not change). This is especially concering for long-lived
34
+ refresh tokens, discussed below.
35
+
28
36
Database
29
37
~~~~~~~~
30
38
If you need to keep track of information about revoked JWTs our recommendation is
@@ -33,3 +41,82 @@ revoked tokens, such as when it was revoked, who revoked it, can it be un-revoke
33
41
etc. Here is an example using SQLAlchemy:
34
42
35
43
.. literalinclude :: ../examples/blocklist_database.py
44
+
45
+ Revoking Refresh Tokens
46
+ ~~~~~~~~~~~~~~~~~~~~~~~
47
+ It is critical to note that a user's refresh token must also be revoked
48
+ when logging out; otherwise, this refresh token could just be used to generate
49
+ a new access token. Usually this falls to the responsibility of the frontend
50
+ application, which must send two separate requests to the backend in order to
51
+ revoke these tokens.
52
+
53
+ This can be implemented via two separate routes marked with ``@jwt_required() ``
54
+ and ``@jwt_required(refresh=True) `` to revoke access and refresh tokens, respectively.
55
+ However, it is more convenient to provide a single endpoint where the frontend
56
+ can send a DELETE for each token. The following is an example:
57
+
58
+ .. code-block :: python
59
+
60
+ @app.route (" /logout" , methods = [" DELETE" ])
61
+ @jwt_required (verify_type = False )
62
+ def logout ():
63
+ token = get_jwt()
64
+ jti = token[" jti" ]
65
+ ttype = token[" type" ]
66
+ jwt_redis_blocklist.set(jti, " " , ex = ACCESS_EXPIRES )
67
+
68
+ # Returns "Access token revoked" or "Refresh token revoked"
69
+ return jsonify(msg = f " { ttype.capitalize()} token successfully revoked " )
70
+
71
+ or, for the database format:
72
+
73
+ .. code-block :: python
74
+
75
+ class TokenBlocklist (db .Model ):
76
+ id = db.Column(db.Integer, primary_key = True )
77
+ jti = db.Column(db.String(36 ), nullable = False , index = True )
78
+ type = db.Column(db.String(16 ), nullable = False )
79
+ user_id = db.Column(
80
+ db.ForeignKey(' person.id' ),
81
+ default = lambda : get_current_user().id,
82
+ nullable = False ,
83
+ )
84
+ created_at = db.Column(
85
+ db.DateTime,
86
+ server_default = func.now(),
87
+ nullable = False ,
88
+ )
89
+
90
+ @app.route (" /logout" , methods = [" DELETE" ])
91
+ @jwt_required (verify_type = False )
92
+ def modify_token ():
93
+ token = get_jwt()
94
+ jti = token[" jti" ]
95
+ ttype = token[" type" ]
96
+ now = datetime.now(timezone.utc)
97
+ db.session.add(TokenBlocklist(jti = jti, type = ttype, created_at = now))
98
+ db.session.commit()
99
+ return jsonify(msg = f " { ttype.capitalize()} token successfully revoked " )
100
+
101
+
102
+ Token type and user columns are not required and can be omitted. That being said, including
103
+ these can help to audit that the frontend is performing its revoking job correctly and revoking both tokens.
104
+
105
+ Alternatively, there are a few ways to revoke both tokens at once:
106
+
107
+ #. Send the access token in the header (per usual), and send the refresh token in
108
+ the DELETE request body. This saves a request but still needs frontend changes, so may not
109
+ be worth implementing
110
+ #. Embed the refresh token's jti in the access token. The revoke route should be authenticated
111
+ with the access token. Upon revoking the access token, extract the refresh jti from it
112
+ and invalidate both. This has the advantage of requiring no extra work from the frontend.
113
+ #. Store every generated tokens jti in a database upon creation. Have a boolean column to represent
114
+ whether it is valid or not, which the ``token_in_blocklist_loader `` should respond based upon.
115
+ Upon revoking a token, mark that token row as invalid, as well as all other tokens from the same
116
+ user generated at the same time. This would also allow for a "log out everywhere" option where
117
+ all tokens for a user are invalidated at once, which is otherwise not easily possibile
118
+
119
+
120
+ The best option of course depends and needs to be chosen based upon the circumstances. If there
121
+ if ever a time where an unknown, untracked token needs to be immediately invalidated, this can
122
+ be accomplished by changing the secret key.
0 commit comments