Flask-JWT-Extended is a fantastic library for adding JSON Web Token (JWT) authentication to your Flask applications, but it’s easy to get lost in the setup and miss some critical security nuances.

Let’s see it in action. Imagine a simple Flask app with a login endpoint and a protected endpoint.

from flask import Flask, request, jsonify
from flask_jwt_extended import create_access_token, jwt_required, JWTManager

app = Flask(__name__)
# Change this in production!
app.config["JWT_SECRET_KEY"] = "super-secret-change-me"
jwt = JWTManager(app)

@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=username)
    return jsonify(access_token=access_token)

@app.route("/profile", methods=["GET"])
@jwt_required()
def profile():
    return jsonify({"message": "Welcome to your profile!"})

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

When you POST {"username": "test", "password": "test"} to /login, you get back a JWT like {"access_token": "eyJ..."}. You then use this token in the Authorization: Bearer <token> header for subsequent requests to /profile.

The core problem Flask-JWT-Extended solves is stateless authentication. Instead of a server-side session that needs to be stored and looked up for every request, the JWT itself contains the user’s identity and claims. The server only needs to verify the token’s signature and expiration. This makes your API scalable and easier to deploy across multiple instances.

Internally, Flask-JWT-Extended uses the PyJWT library to encode and decode tokens. When you call create_access_token(identity=username), it generates a JWT. This token is a string composed of three parts separated by dots: header, payload, and signature. The header typically specifies the token type (JWT) and the signing algorithm (e.g., HS256). The payload contains registered claims (like exp for expiration, iat for issued at) and custom claims (like identity). The signature is generated by signing the encoded header and payload with your JWT_SECRET_KEY using the specified algorithm. On subsequent requests, jwt_required() intercepts the request, extracts the token, decodes it using the secret key, and verifies its signature and expiration. If valid, it allows the request to proceed, making the identity available via get_jwt_identity().

The most surprising thing about Flask-JWT-Extended is how easily a seemingly robust authentication system can be compromised by a weak JWT_SECRET_KEY. If your secret key is short, predictable, or leaked, an attacker can forge valid JWTs for any user, granting them full access to your protected resources without ever needing valid credentials.

You control the security and behavior of your JWTs through Flask configuration variables. JWT_SECRET_KEY is paramount; it must be long, random, and kept secret. JWT_ACCESS_TOKEN_EXPIRES controls how long tokens are valid (e.g., timedelta(minutes=15)). You can also configure token locations (JWT_TOKEN_LOCATION), cookie settings, and enable CSRF protection (JWT_COOKIE_CSRF_PROTECT).

Many developers overlook the importance of token revocation. While JWTs are stateless and designed to be short-lived, if a token is compromised before it expires, an attacker has a window of opportunity. Flask-JWT-Extended offers ways to manage this, like blacklisting tokens, but it adds state back into the system. A common approach is to maintain a set of revoked token identifiers (often stored in Redis or a database) and check against this set within a custom @jwt.token_in_blocklist_loader. This allows you to invalidate a token immediately after, say, a user logs out or their password changes, even if the token hasn’t expired yet.

The next logical step after securing your API with JWTs is implementing refresh tokens for a better user experience without compromising security.

Want structured learning?

Take the full Flask course →