Session invalidation on credential change

Add session_version to users table. Sessions now carry session_version
alongside user_id; @login_required rejects any session where the version
doesn't match the DB value.

- migration 0024: session_version INTEGER NOT NULL DEFAULT 1
- models/user.py: session_version Mapped[int] column
- auth.py: version mismatch → session.clear() + 401
- services/auth.py: change_password and reset_password_with_token both
  increment session_version, evicting all other live sessions
- routes/auth.py: login/register/oauth_callback store session_version;
  update_password rehydrates current session with new version so the
  user who changed their password stays logged in

Existing sessions will require one re-login after the migration runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 16:30:14 -05:00
parent 16ecd6bbeb
commit b33aa25fb7
6 changed files with 51 additions and 9 deletions
+2 -1
View File
@@ -1,4 +1,4 @@
from sqlalchemy import Index, Text
from sqlalchemy import Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base
@@ -14,6 +14,7 @@ class User(Base, CreatedAtMixin):
password_hash: Mapped[str | None] = mapped_column(Text, nullable=True)
oauth_sub: Mapped[str | None] = mapped_column(Text, unique=True, nullable=True)
role: Mapped[str] = mapped_column(Text, nullable=False, default="user")
session_version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
__table_args__ = (
Index("ix_users_username", "username"),