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.

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 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.

from quart import Quart, request, jsonify

from quart_jwt_extended import (
    JWTManager,
    jwt_required,
    get_jwt_identity,
    create_access_token,
    create_refresh_token,
    jwt_refresh_token_required,
    get_raw_jwt,
)


# Setup quart
app = Quart(__name__)

# Enable blacklisting and specify what kind of tokens to check
# against the blacklist
app.config["JWT_SECRET_KEY"] = "super-secret"  # Change this!
app.config["JWT_BLACKLIST_ENABLED"] = True
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/greenape/quart-jwt-extended/blob/master/examples/redis_blacklist.py
# https://github.com/greenape/quart-jwt-extended/tree/master/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"])
async def login():
    username = (await request.get_json()).get("username", None)
    password = (await request.get_json()).get("password", None)
    if username != "test" or password != "test":
        return {"msg": "Bad username or password"}, 401

    ret = {
        "access_token": create_access_token(identity=username),
        "refresh_token": create_refresh_token(identity=username),
    }
    return ret, 200


# Standard refresh endpoint. A blacklisted refresh token
# will not be able to access this endpoint
@app.route("/refresh", methods=["POST"])
@jwt_refresh_token_required
async def refresh():
    current_user = get_jwt_identity()
    ret = {"access_token": create_access_token(identity=current_user)}
    return ret, 200


# Endpoint for revoking the current users access token
@app.route("/logout", methods=["DELETE"])
@jwt_required
async def logout():
    jti = get_raw_jwt()["jti"]
    blacklist.add(jti)
    return {"msg": "Successfully logged out"}, 200


# Endpoint for revoking the current users refresh token
@app.route("/logout2", methods=["DELETE"])
@jwt_refresh_token_required
async def logout2():
    jti = get_raw_jwt()["jti"]
    blacklist.add(jti)
    return {"msg": "Successfully logged out"}, 200


# This will now prevent users with blacklisted tokens from
# accessing this endpoint
@app.route("/protected", methods=["GET"])
@jwt_required
async def protected():
    return {"hello": "world"}


if __name__ == "__main__":
    app.run()

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: