Flask 3 + Flask-JWT-Extended access and refresh tokens: working versions (tested 2026-10-15)
Login, protected routes, refresh tokens and custom claims in Flask 3 using Flask-JWT-Extended, with an app factory.
passed3 passed · 0 failed2026-10-15 11:35 UTC- Status
- passed
- Last passed
- 2026-10-15 11:35 UTC
- Tests
- 3 / 3
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions3
flask@3.1.3flask-jwt-extended@4.7.4pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38728226240
The code
Exactly the files that passed. Your agent can pull them with get_recipe("flask-jwt-extended-auth") (install), or see them on GitHub.
requirements.txt
flask==3.1.3 flask-jwt-extended==4.7.4 pytest==9.1.1
app.py
from flask import Flask, jsonify, request
from flask_jwt_extended import (JWTManager, create_access_token, create_refresh_token,
get_jwt, get_jwt_identity, jwt_required)
USERS = {"ada": {"password": "lovelace", "role": "admin"}} # swap for a real store
def create_app(secret="change-me"):
app = Flask(__name__)
app.config["JWT_SECRET_KEY"] = secret
JWTManager(app)
@app.post("/login")
def login():
body = request.get_json(silent=True) or {}
user = USERS.get(body.get("username"))
if not user or user["password"] != body.get("password"):
return jsonify(error="invalid_credentials"), 401
claims = {"role": user["role"]}
return jsonify(access=create_access_token(identity=body["username"], additional_claims=claims),
refresh=create_refresh_token(identity=body["username"], additional_claims=claims))
@app.post("/refresh")
@jwt_required(refresh=True)
def refresh():
return jsonify(access=create_access_token(identity=get_jwt_identity(), additional_claims={"role": get_jwt()["role"]}))
@app.get("/admin")
@jwt_required()
def admin():
if get_jwt().get("role") != "admin":
return jsonify(error="forbidden"), 403
return jsonify(user=get_jwt_identity())
return app
tests/test_auth.py
from app import create_app
def client():
return create_app("a-long-enough-test-secret-key-123").test_client()
def test_login_and_access():
c = client()
tokens = c.post("/login", json={"username": "ada", "password": "lovelace"}).get_json()
r = c.get("/admin", headers={"Authorization": f"Bearer {tokens['access']}"})
assert r.status_code == 200 and r.get_json() == {"user": "ada"}
def test_refresh_flow():
c = client()
tokens = c.post("/login", json={"username": "ada", "password": "lovelace"}).get_json()
new = c.post("/refresh", headers={"Authorization": f"Bearer {tokens['refresh']}"}).get_json()["access"]
assert c.get("/admin", headers={"Authorization": f"Bearer {new}"}).status_code == 200
# an access token cannot be used to refresh
assert c.post("/refresh", headers={"Authorization": f"Bearer {new}"}).status_code == 422
def test_rejections():
c = client()
assert c.post("/login", json={"username": "ada", "password": "x"}).status_code == 401
assert c.get("/admin").status_code == 401
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Flask 3 app factory + Flask-SQLAlchemy
Flask application factory with Flask-SQLAlchemy 3, a JSON API and pytest fixtures using an in-memory database.
Flask · Flask-SQLAlchemy · SQLAlchemy
Flask API with Flask-SQLAlchemy and marshmallow-sqlalchemy, validation errors as 422
A Flask JSON API that loads and dumps SQLAlchemy models through SQLAlchemyAutoSchema, returns validation errors as 422, and is tested offline with the Flask test client on in-memory sqlite.
Flask · Flask-SQLAlchemy · marshmallow-sqlalchemy · marshmallow · SQLAlchemy