Refreshing Tokens

In most web applications, it would not be ideal if a user was logged out in the middle of doing something because their JWT expired. Unfortunately we can’t just change the expires time on a JWT on each request, as once a JWT is created it cannot be modified. Lets take a look at some options for solving this problem by refreshing JWTs.

Implicit Refreshing With Cookies

One huge benefit to storing your JWTs in cookies (when your frontend is a website) is that the frontend does not have to handle any logic when it comes to refreshing a token. It can all happen implicitly with the cookies your Flask application sets.

The basic idea here is that at the end of every request, we will check if there is a JWT that is close to expiring. If we find a JWT that is nearly expired, we will replace the current cookie containing the JWT with a new JWT that has a longer time until it expires.

This is our recommended approach when your frontend is a website.

from datetime import datetime
from datetime import timedelta
from datetime import timezone

from flask import Flask
from flask import jsonify

from flask_jwt_extended import create_access_token
from flask_jwt_extended import get_jwt
from flask_jwt_extended import get_jwt_identity
from flask_jwt_extended import jwt_required
from flask_jwt_extended import JWTManager
from flask_jwt_extended import set_access_cookies
from flask_jwt_extended import unset_jwt_cookies

app = Flask(__name__)

# If true this will only allow the cookies that contain your JWTs to be sent
# over https. In production, this should always be set to True
app.config["JWT_COOKIE_SECURE"] = False
app.config["JWT_TOKEN_LOCATION"] = ["cookies"]
app.config["JWT_SECRET_KEY"] = "super-secret"  # Change this in your code!
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)

jwt = JWTManager(app)


# Using an `after_request` callback, we refresh any token that is within 30
# minutes of expiring. Change the timedeltas to match the needs of your application.
@app.after_request
def refresh_expiring_jwts(response):
    try:
        exp_timestamp = get_jwt()["exp"]
        now = datetime.now(timezone.utc)
        target_timestamp = datetime.timestamp(now + timedelta(minutes=30))
        if target_timestamp > exp_timestamp:
            access_token = create_access_token(identity=get_jwt_identity())
            set_access_cookies(response, access_token)
        return response
    except (RuntimeError, KeyError):
        # Case where there is not a valid JWT. Just return the original respone
        return response


@app.route("/login", methods=["POST"])
def login():
    response = jsonify({"msg": "login successful"})
    access_token = create_access_token(identity="example_user")
    set_access_cookies(response, access_token)
    return response


@app.route("/logout", methods=["POST"])
def logout():
    response = jsonify({"msg": "logout successful"})
    unset_jwt_cookies(response)
    return response


@app.route("/protected")
@jwt_required()
def protected():
    return jsonify(foo="bar")


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

Explicit Refreshing With Refresh Tokens

Alternatively, this extension comes out of the box with refresh token support. A refresh token is a long lived JWT that can only be used to creating new access tokens.

You have a couple choices about how to utilize a refresh token. You could store the expires time of your access token on your frontend, and each time you make an API request first check if the current access token is near or already expired, and refresh it as needed. This approach is pretty simple and will work fine in most cases, but do be aware that if your frontend has a clock that is significantly off, you might run into issues.

An alternative approach involves making an API request with your access token and then checking the result to see if it worked. If the result of the request is an error message saying that your token is expired, use the refresh token to generate a new access token and redo the request with the new token. This approach will work regardless of the clock on your frontend, but it does require having some potentially more complicated logic.

Using refresh tokens is our recommended approach when your frontend is not a website (mobile, api only, etc).

from datetime import timedelta

from flask import Flask
from flask import jsonify

from flask_jwt_extended import create_access_token
from flask_jwt_extended import create_refresh_token
from flask_jwt_extended import get_jwt_identity
from flask_jwt_extended import jwt_required
from flask_jwt_extended import JWTManager

app = Flask(__name__)

app.config["JWT_SECRET_KEY"] = "super-secret"  # Change this!
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)
app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=30)
jwt = JWTManager(app)


@app.route("/login", methods=["POST"])
def login():
    access_token = create_access_token(identity="example_user")
    refresh_token = create_refresh_token(identity="example_user")
    return jsonify(access_token=access_token, refresh_token=refresh_token)


# We are using the `refresh=True` options in jwt_required to only allow
# refresh tokens to access this route.
@app.route("/refresh", methods=["POST"])
@jwt_required(refresh=True)
def refresh():
    identity = get_jwt_identity()
    access_token = create_access_token(identity=identity)
    return jsonify(access_token=access_token)


@app.route("/protected", methods=["GET"])
@jwt_required()
def protected():
    return jsonify(foo="bar")


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

Making a request with a refresh token looks just like making a request with an access token. Here is an example using HTTPie.

$ http POST :5000/refresh Authorization:"Bearer $REFRESH_TOKEN"

Token Freshness Pattern

The token freshness pattern is a very simple idea. Every time a user authenticates by providing a username and password, they receive a fresh access token that can access any route. But after some time, that token should no longer be considered fresh, and some critical or dangerous routes will be blocked until the user verifies their password again. All other routes will still work normally for the user even though their token is no longer fresh. As an example, we might not allow users to change their email address unless they have a fresh token, but we do allow them use the rest of our Flask application normally.

The token freshness pattern is built into this extension, and works seamlessly with both token refreshing strategies discussed above. Lets take a look at this with the explicit refresh example (it will look basically same in the implicit refresh example).

from datetime import timedelta

from flask import Flask
from flask import jsonify
from flask import request

from flask_jwt_extended import create_access_token
from flask_jwt_extended import create_refresh_token
from flask_jwt_extended import get_jwt_identity
from flask_jwt_extended import jwt_required
from flask_jwt_extended import JWTManager

app = Flask(__name__)

app.config["JWT_SECRET_KEY"] = "super-secret"  # Change this!
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)
app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=30)
jwt = JWTManager(app)


# We verify the users password here, so we are returning a fresh access token
@app.route("/login", methods=["POST"])
def login():
    username = request.json.get("username", None)
    password = request.json.get("password", None)
    if username != "test" or password != "test":
        return jsonify({"msg": "Bad username or password"}), 401

    access_token = create_access_token(identity="example_user", fresh=True)
    refresh_token = create_refresh_token(identity="example_user")
    return jsonify(access_token=access_token, refresh_token=refresh_token)


# If we are refreshing a token here we have not verified the users password in
# a while, so mark the newly created access token as not fresh
@app.route("/refresh", methods=["POST"])
@jwt_required(refresh=True)
def refresh():
    identity = get_jwt_identity()
    access_token = create_access_token(identity=identity, fresh=False)
    return jsonify(access_token=access_token)


# Only allow fresh JWTs to access this route with the `fresh=True` arguement.
@app.route("/protected", methods=["GET"])
@jwt_required(fresh=True)
def protected():
    return jsonify(foo="bar")


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

We also support marking a token as fresh for a given amount of time after it is created. You can do this by passing a datetime.timedelta to the fresh option when creating JWTs:

create_access_token(identity, fresh=datetime.timedelta(minutes=15))