auth: reject inactive users at login + invalidate existing sessions

Two-pronged enforcement so deactivation has effect both for fresh
logins and already-issued sessions:

  - load_current_user(): treat User with active=False as logged-out
    (sets g.user=None). Existing sessions stop working immediately.
  - login(): include `not user.active` in the existing 401 condition,
    so deactivated accounts get the same "invalid credentials"
    response as wrong-password / unknown-user — no timing oracle for
    deactivation status.

Tests still green (12/12 in test_auth.py).
This commit is contained in:
mwiegand 2026-05-10 21:13:31 +02:00
parent 726acfa4ff
commit 3490be5fb7
No known key found for this signature in database
2 changed files with 7 additions and 2 deletions

View file

@ -27,7 +27,10 @@ def load_current_user() -> None:
g.user = None g.user = None
return return
with session_scope() as db: with session_scope() as db:
g.user = db.scalar(select(User).where(User.id == int(user_id))) user = db.scalar(select(User).where(User.id == int(user_id)))
# Treat deactivated users as logged-out so existing sessions stop
# working as soon as an admin flips active=False.
g.user = user if user is not None and user.active else None
def current_user() -> User | None: def current_user() -> User | None:

View file

@ -48,7 +48,9 @@ def login() -> Response:
user = db.scalar(select(User).where(User.username == username)) user = db.scalar(select(User).where(User.username == username))
digest = user.password_digest if user is not None else _TIMING_DUMMY_DIGEST digest = user.password_digest if user is not None else _TIMING_DUMMY_DIGEST
password_ok = verify_password(password, digest) password_ok = verify_password(password, digest)
if user is None or not password_ok: if user is None or not password_ok or not user.active:
# Same generic response for missing user, wrong password, or
# deactivated account — no timing oracle for deactivation status.
return Response("invalid credentials", status=401) return Response("invalid credentials", status=401)
login_user(user.id) login_user(user.id)
LOGIN_ATTEMPTS_BY_IP.pop(remote_addr, None) LOGIN_ATTEMPTS_BY_IP.pop(remote_addr, None)