Flask-Login sessions with Flask-SQLAlchemy users and werkzeug password hashing: working versions (tested 2026-10-15)
Login, login_required redirects, logout and a user_loader built on db.session.get, with the User model in SQLite memory and tests run through the Flask test client.
passed6 passed · 0 failed2026-10-15 13:22 UTC- Status
- passed
- Last passed
- 2026-10-15 13:22 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38741369695
The code
Exactly the files that passed. Your agent can pull them with get_recipe("flask-login-sqlalchemy-sessions") (install), or see them on GitHub.
flask==3.1.3 flask-login==0.6.3 flask-sqlalchemy==3.1.1 pytest==9.1.1
from flask import Blueprint, Flask, redirect, request, url_for
from flask_login import LoginManager, UserMixin, current_user, login_required, login_user, logout_user
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import check_password_hash, generate_password_hash
db = SQLAlchemy()
login_manager = LoginManager()
login_manager.login_view = "auth.login"
auth = Blueprint("auth", __name__)
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
def set_password(self, password: str) -> None:
self.password_hash = generate_password_hash(password)
def check_password(self, password: str) -> bool:
return check_password_hash(self.password_hash, password)
@login_manager.user_loader
def load_user(user_id: str) -> User | None:
# Flask-Login passes the id as a string. A deleted user returns None (anonymous), not an error.
return db.session.get(User, int(user_id))
@auth.route("/login", methods=["GET", "POST"])
def login():
if request.method == "GET":
return "login form"
username = request.form.get("username", "")
user = db.session.scalar(db.select(User).filter_by(username=username))
if user is None or not user.check_password(request.form.get("password", "")):
return "bad credentials", 401
login_user(user)
nxt = request.args.get("next", "")
# Only local paths. Reject absolute URLs and "//host" to block open redirects.
if not nxt.startswith("/") or nxt.startswith("//"):
nxt = url_for("auth.secret")
return redirect(nxt)
@auth.get("/secret")
@login_required
def secret():
return f"secret for {current_user.username}"
@auth.post("/logout")
def logout():
# POST only, so a GET link or image tag cannot log the user out.
logout_user()
return "bye"
def create_app(config: dict | None = None) -> Flask:
app = Flask(__name__)
app.config.update(SECRET_KEY="dev-only", SQLALCHEMY_DATABASE_URI="sqlite:///:memory:")
app.config.update(config or {})
db.init_app(app)
login_manager.init_app(app)
app.register_blueprint(auth)
return app
import pytest
from app import User, create_app, db
@pytest.fixture
def app():
app = create_app({"TESTING": True})
with app.app_context():
db.create_all()
user = User(username="ada")
user.set_password("pw")
db.session.add(user)
db.session.commit()
yield app
with app.app_context():
db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()
def post_login(client, password="pw", next_url="/secret"):
return client.post(
"/login",
query_string={"next": next_url},
data={"username": "ada", "password": password},
)
def test_login_then_secret(client):
r = post_login(client)
assert r.status_code == 302
assert r.headers["Location"] == "/secret"
assert client.get("/secret").data == b"secret for ada"
def test_anonymous_redirects_to_login(client):
r = client.get("/secret")
assert r.status_code == 302
assert "/login" in r.headers["Location"]
def test_open_redirect_is_blocked(client):
r = post_login(client, next_url="https://evil.example")
assert r.headers["Location"] == "/secret"
def test_bad_password_is_rejected(client):
assert post_login(client, password="wrong").status_code == 401
assert client.get("/secret").status_code == 302
def test_logout_is_post_only(client):
post_login(client)
assert client.get("/logout").status_code == 405
client.post("/logout")
assert client.get("/secret").status_code == 302
def test_deleted_user_session_becomes_anonymous(client, app):
post_login(client)
with app.app_context():
user = db.session.scalar(db.select(User).filter_by(username="ada"))
db.session.delete(user)
db.session.commit()
r = client.get("/secret")
assert r.status_code == 302
assert "/login" in r.headers["Location"]
[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.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "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 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-SQLAlchemy models with Flask-Migrate upgrades on sqlite in tests
Define Flask-SQLAlchemy models, version the schema with Flask-Migrate, and test upgrade, downgrade and model/migration drift offline on a temporary sqlite file.
Flask 3 + Flask-JWT-Extended access and refresh tokens
Login, protected routes, refresh tokens and custom claims in Flask 3 using Flask-JWT-Extended, with an app factory.
All Flask recipesAll flask-login recipesAll Flask-SQLAlchemy recipesAll werkzeug recipes