fix(auth): rendre effectives l'expiration de session et la desactivation de compte

Deux protections etaient configurees sans avoir d'effet.

Expiration de session
  app.py:73 definit PERMANENT_SESSION_LIFETIME = 3600, mais Flask
  n'applique cette duree qu'aux sessions marquees permanentes. Aucune
  occurrence de session.permanent n'existait dans app/. Le cookie emis
  etait donc un cookie de session navigateur, sans expiration, et le
  serveur ne verifiait aucune anciennete. Ajout de session.permanent
  juste avant login_user, apres la rotation anti-fixation.

Desactivation de compte
  is_active_account n'etait consulte qu'au moment du login (auth.py:141).
  User n'ayant pas surcharge is_active, UserMixin renvoyait True en
  permanence. Desactiver un compte empechait donc la reconnexion mais
  laissait vivre la session en cours.

  La propriete is_active seule ne suffit pas : Flask-Login ne la consulte
  qu'a l'appel de login_user, jamais lors de la restauration d'une session
  depuis le cookie. Le verrou effectif est donc dans load_user, qui renvoie
  desormais None pour un compte desactive. La propriete est ajoutee malgre
  tout pour que login_user soit coherent avec le chargeur.

load_user passe au passage de Query.get() (API heritee, avertie en
SQLAlchemy 2.0) a db.session.get(), et tolere un identifiant non entier
sans lever.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
GGThed
2026-08-07 19:16:37 -04:00
co-authored by Claude Opus 5
parent 56aee7f3f3
commit 2c37c05c8f
3 changed files with 32 additions and 1 deletions
+16 -1
View File
@@ -9,6 +9,21 @@ def load_user(user_id):
Returns the correct polymorphic subclass (Admin, Coach, Player, etc.) Returns the correct polymorphic subclass (Admin, Coach, Player, etc.)
automatically because SQLAlchemy resolves the identity column. automatically because SQLAlchemy resolves the identity column.
Returns None for deactivated accounts so that disabling a user also
invalidates the sessions they already hold. Flask-Login only consults
is_active when login_user() is called, never when restoring a session
from the cookie, so the check has to happen here.
""" """
from app.extensions import db
from app.models.user_model.user import User from app.models.user_model.user import User
return User.query.get(int(user_id))
try:
pk = int(user_id)
except (TypeError, ValueError):
return None
user = db.session.get(User, pk)
if user is None or not user.is_active_account:
return None
return user
+11
View File
@@ -51,6 +51,17 @@ class User(UserMixin, db.Model):
'TeamMember', foreign_keys='TeamMember.player_id', 'TeamMember', foreign_keys='TeamMember.player_id',
backref='player_ref', lazy='dynamic') backref='player_ref', lazy='dynamic')
# --- Flask-Login integration -------------------------------------------
@property
def is_active(self):
"""Whether Flask-Login should accept this account.
UserMixin returns True unconditionally, which meant a deactivated
account kept any session it already held. Binding this to
is_active_account makes deactivation take effect on the next request.
"""
return bool(self.is_active_account)
# --- shared helper methods --------------------------------------------- # --- shared helper methods ---------------------------------------------
def get_games_list(self): def get_games_list(self):
"""Return the user's games as a list.""" """Return the user's games as a list."""
+5
View File
@@ -154,6 +154,11 @@ def login():
if _csrf_token: if _csrf_token:
session['csrf_token'] = _csrf_token session['csrf_token'] = _csrf_token
# Mark the session permanent so PERMANENT_SESSION_LIFETIME applies.
# Without this, Flask emits a browser-session cookie with no expiry
# and the configured lifetime is silently ignored.
session.permanent = True
login_user(user) login_user(user)
# Validate redirect URL to prevent open redirect vulnerability # Validate redirect URL to prevent open redirect vulnerability