ProvenPin Install

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
pinned versions4
flask@3.1.3flask-login@0.6.3flask-sqlalchemy@3.1.1pytest@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 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.

requirements.txt
flask==3.1.3
flask-login==0.6.3
flask-sqlalchemy==3.1.1
pytest==9.1.1
app.py
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
tests/test_app.py
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.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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.