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
- User Authentication With Flask-Login—password hashing concepts
- Responses and JSON API
pip install Flask-JWT-Extended
When to Use JWT vs Sessions
| Approach | Best for | Trade-off |
|---|---|---|
| Session cookie (Flask-Login) | Same-origin web apps | Server stores session state (or signed cookie) |
| JWT access token | SPAs, mobile, microservices | Stateless verify; revocation needs extra design |
Compare with Spring Security and JWT—same ideas, different stack.
Install and Configure
pip install Flask-JWT-Extendedapp/extensions.py:
from flask_jwt_extended import JWTManager
jwt = JWTManager()app/config.py:
class Config:
JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY", "change-me-jwt")
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)app/__init__.py:
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:
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:
identityis embedded in JWTsubclaim—usually user ID as stringadditional_claimsadds custom fields readable after decode
Protect Routes
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:
TOKEN="eyJ..."
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:5000/api/v1/postsCode explanation:
@jwt_required()returns 401 if token missing, expired, or invalidget_jwt_identity()returnsidentityfromcreate_access_token
Refresh Tokens (Concept)
Short access token + long refresh token improves security:
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:
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 NoneRequires Redis or DB to store revoked jti values—acceptable for APIs needing forced logout.
JWT + CORS
SPAs on another port need CORS chapter:
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.