From a02a9df08102b7d794e272a306e51c64b36f82a9 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 20 Apr 2026 17:32:32 +0200 Subject: [PATCH] src/auth/setup.py : init_auth et helper safe_next (#73) Co-Authored-By: Claude Sonnet 4.6 --- src/auth/setup.py | 52 ++++++++++++++++++++++++++++++++++++++++ tests/auth/test_setup.py | 41 +++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 src/auth/setup.py create mode 100644 tests/auth/test_setup.py diff --git a/src/auth/setup.py b/src/auth/setup.py new file mode 100644 index 0000000..adf621b --- /dev/null +++ b/src/auth/setup.py @@ -0,0 +1,52 @@ +import os + +from flask import Flask +from flask_login import LoginManager +from flask_wtf.csrf import CSRFProtect + +from src.auth import db, mailer +from src.auth.models import load_user +from src.utils import DEVELOPMENT, logger + +_csrf: CSRFProtect | None = None +_login_manager: LoginManager | None = None + + +def safe_next(url: str | None, fallback: str = "/") -> str: + if not url or not url.startswith("/") or url.startswith("//"): + return fallback + return url + + +def init_auth(app: Flask) -> None: + global _csrf, _login_manager + + secret = os.getenv("SECRET_KEY") or app.config.get("SECRET_KEY") + if not secret: + raise RuntimeError( + "SECRET_KEY est obligatoire pour l'authentification. " + "Définissez-la dans .env (voir .template.env)." + ) + app.config["SECRET_KEY"] = secret + app.config["SESSION_COOKIE_HTTPONLY"] = True + app.config["SESSION_COOKIE_SAMESITE"] = "Lax" + app.config["SESSION_COOKIE_SECURE"] = not DEVELOPMENT + app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 * 30 # 30 jours + + db.init_schema() + db.purge_expired_tokens() + + mailer.init_mailer(app) + + _login_manager = LoginManager() + _login_manager.login_view = "/connexion" + _login_manager.user_loader(load_user) + _login_manager.init_app(app) + + _csrf = CSRFProtect(app) + + if not os.getenv("SMTP_HOST"): + logger.warning( + "SMTP_HOST non défini : les emails d'auth échoueront. " + "Définissez les variables SMTP_* dans .env pour envoyer des emails." + ) diff --git a/tests/auth/test_setup.py b/tests/auth/test_setup.py new file mode 100644 index 0000000..51fec7a --- /dev/null +++ b/tests/auth/test_setup.py @@ -0,0 +1,41 @@ +import pytest +from flask import Flask +from flask_login import current_user + +from src.auth.setup import init_auth, safe_next + + +def test_safe_next_allows_relative_path(): + assert safe_next("/acheteur?id=1") == "/acheteur?id=1" + + +def test_safe_next_rejects_absolute_urls(): + assert safe_next("https://evil.com/phish") == "/" + + +def test_safe_next_rejects_protocol_relative(): + assert safe_next("//evil.com") == "/" + + +def test_safe_next_rejects_empty(): + assert safe_next("") == "/" + assert safe_next(None) == "/" + + +def test_safe_next_custom_fallback(): + assert safe_next("", fallback="/compte") == "/compte" + + +def test_init_auth_requires_secret_key(monkeypatch, users_db_path): + monkeypatch.delenv("SECRET_KEY", raising=False) + app = Flask(__name__) + with pytest.raises(RuntimeError, match="SECRET_KEY"): + init_auth(app) + + +def test_init_auth_anonymous_user_not_authenticated(users_db_path): + app = Flask(__name__) + app.config["SECRET_KEY"] = "test" + init_auth(app) + with app.test_request_context("/"): + assert current_user.is_authenticated is False