@@ -25,6 +25,14 @@ Live (TTL) functionality when storing a JWT. Here is an example using redis:
2525
2626.. literalinclude :: ../examples/blocklist_redis.py
2727
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+
2836Database
2937~~~~~~~~
3038If 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
3341etc. Here is an example using SQLAlchemy:
3442
3543.. 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