Skip to content

flask: Invalidate sessions and JWTs when a user's password changes (1/3 split of #73) - #74

Open
RudraBJoshi wants to merge 2 commits into
Open-Coding-Society:mainfrom
CSA-Admin-OCS:split/flask/f1-jwt-session-invalidation
Open

flask: Invalidate sessions and JWTs when a user's password changes (1/3 split of #73)#74
RudraBJoshi wants to merge 2 commits into
Open-Coding-Society:mainfrom
CSA-Admin-OCS:split/flask/f1-jwt-session-invalidation

Conversation

@RudraBJoshi

Copy link
Copy Markdown

Splitting #73 into smaller, independently-reviewable PRs across spring/flask/pages. This one covers the password reset button side of flask — session/JWT invalidation on password change.

Independent of the other PRs in this stack.

Previously nothing tied an issued JWT or Flask-Login session to a specific password: the JWT had no exp claim at all and carried nothing password-derived, and sessions carried a bare user id. A stolen JWT or session cookie kept working indefinitely, surviving a password reset meant to lock an attacker out.

Adds User.token_version, bumped in set_password() only on an actual hash change. JWTs now carry token_version + exp and are checked against the account's current value; sessions carry it via a composite get_id(), checked in load_user.

Requires a schema change before deploy: ALTER TABLE users ADD COLUMN token_version INTEGER NOT NULL DEFAULT 0;

Original PR: #73

*** REQUIRED BEFORE THIS DEPLOYS: production runs MySQL (see __init__.py --
SQLALCHEMY_DATABASE_URI switches to MySQL whenever DB_ENDPOINT/DB_USERNAME/
DB_PASSWORD are set), a completely separate database this session had no
access to. Only the local dev SQLite DB has been migrated. Someone MUST run
this against production before/with this deploy, or every login there will
error on the missing column:

    ALTER TABLE users ADD COLUMN token_version INTEGER NOT NULL DEFAULT 0;

***

Previously nothing tied an issued JWT or Flask-Login session to a specific
password: the JWT carried no exp claim at all (never expired by JWT
semantics) and no password-derived data, and Flask-Login sessions just
carried a bare user id, re-validated against fresh DB data on every request
but with no check that the underlying credential hadn't changed. A stolen
JWT or session cookie kept working indefinitely, surviving a password reset
that was meant to lock an attacker out.

Adds User.token_version, bumped in set_password() (the single funnel every
password-change path already goes through) only on an actual hash change.
JWTs now carry token_version + exp and are checked against the account's
current value in auth_required. Sessions now carry it via a composite
get_id() ("id:token_version"), checked in load_user (main.py), so a stale
session is rejected before ever reaching a @login_required route instead of
running with outdated auth state.

Verified live: fresh JWT/session -> 200, password reset -> old JWT gets 401
with an explicit "password has changed" message, old session gets redirected
to login, fresh login after the reset works again.
@RudraBJoshi

RudraBJoshi commented Aug 26, 2026

Copy link
Copy Markdown
Author

@jmort1021 accept the pull request pls so Dhyan can add the schema

@jm1021

jm1021 commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

This should be tied to Profile password reset.

@RudraBJoshi

Copy link
Copy Markdown
Author

Addressed on the spring side (split/spring/s2-oauth-verified-reset, spring #170 -- see that PR/spring #169 for the full response). Short version: the profile page's direct password field was removed in a later PR (pages #1371) in favor of routing everyone through the OAuth-verified reset wizard, so 'tied to Profile password reset' now means tied to that wizard's completion. /reset/oauth/complete now force-expires the account's Spring MVC sessions and the frontend explicitly logs out of Spring on success. No flask-side change needed here -- flask's session/token invalidation (this PR) already covers its own auth paths; this was specifically about the Spring MVC session gap.

Every session predating this feature has a plain "id" cookie with no
":token_version" suffix, which load_user() previously invalidated
unconditionally (the split on ":" raised, caught, returned None). Treat
a missing ":" as token_version "0" instead, matching a fresh/unchanged
account -- it still correctly fails once that account's real
token_version has moved past 0 from an actual password change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants