JWT API Authentication (Optional)

Introduction

Flask-Login fits cookie sessions in browser apps. JSON APIs and mobile clients often use JWT (JSON Web Tokens)—stateless bearer tokens sent in the Authorization header. Flask-JWT-Extended adds token creation, validation, and protected routes. This chapter is optional; skip it if you only build server-rendered sites.

Prerequisites

When to Use JWT vs Sessions

ApproachBest forTrade-off
Session cookie (Flask-Login)Same-origin web appsServer stores session state (or signed cookie)
JWT access tokenSPAs, mobile, microservicesStateless verify; revocation needs extra design

Compare with Spring Security and JWT—same ideas, different stack.

Install and Configure

bash
pip install Flask-JWT-Extended

app/extensions.py:

python
from flask_jwt_extended import JWTManager
 
jwt = JWTManager()

app/config.py:

python
class Config:
    JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY", "change-me-jwt")
    JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)

app/__init__.py:

python
from app.extensions import jwt
 
def create_app(...):
    ...
    jwt.init_app(app)
    ...

Use a separate secret from SECRET_KEY or derive both from env in production.

Warning

Protect JWT_SECRET_KEY

Anyone with the secret can forge tokens—store in environment variables only.

Login Returns Access Token

app/api/routes.py:

Test:

bash
curl -s -X POST http://127.0.0.1:5000/api/v1/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"secret"}'

Code explanation:

  • identity is embedded in JWT sub claim—usually user ID as string
  • additional_claims adds custom fields readable after decode

Protect Routes

python
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
 
@api_bp.route("/posts", methods=["GET"])
@jwt_required()
def list_posts():
    user_id = get_jwt_identity()
    claims = get_jwt()
    role = claims.get("role")
    return jsonify({"user_id": user_id, "role": role, "posts": []})

Authenticated request:

bash
TOKEN="eyJ..."
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:5000/api/v1/posts

Code explanation:

  • @jwt_required() returns 401 if token missing, expired, or invalid
  • get_jwt_identity() returns identity from create_access_token

Refresh Tokens (Concept)

Short access token + long refresh token improves security:

python
from flask_jwt_extended import create_refresh_token, jwt_required
 
refresh = create_refresh_token(identity=str(user.id))
 
@api_bp.route("/refresh", methods=["POST"])
@jwt_required(refresh=True)
def refresh():
    user_id = get_jwt_identity()
    return jsonify({"access_token": create_access_token(identity=user_id)})

Store refresh tokens securely (HTTP-only cookie or secure mobile storage)—not in localStorage if XSS is a concern.

Error Handlers

Revocation and Blocklist (Concept)

JWTs are stateless—logout often means blocklist until expiry:

python
app.config["JWT_BLOCKLIST_ENABLED"] = True
 
@jwt.token_in_blocklist_loader
def check_if_token_revoked(jwt_header, jwt_payload):
    jti = jwt_payload["jti"]
    return redis_client.get(jti) is not None

Requires Redis or DB to store revoked jti values—acceptable for APIs needing forced logout.

JWT + CORS

SPAs on another port need CORS chapter:

python
from flask_cors import CORS
CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}})

Send token in Authorization header from frontend fetch.

FAQ

422 Unprocessable Entity on protected route?

Missing Authorization: Bearer <token> header or wrong format.

identity int vs str?

Flask-JWT-Extended expects string identity in recent versions—use str(user.id).

Same app Flask-Login and JWT?

Possible—browser uses cookies, /api/* uses JWT; keep concerns separated in blueprints.

Token in URL query string?

Avoid—logs and referrer headers leak tokens.

HS256 vs RS256?

HS256 with shared secret is simpler; RS256 with key pair suits multi-service verification.

Skip this chapter?

Yes—Todo blog project can use Flask-Login only; REST project may add JWT in project chapter.