profile: happy-path + cross-session invalidation tests
Verifies that on a successful change the digest rotates, the password_changed_at advances, this session keeps working with the re-stamped marker, and a parallel session forged from the pre-change marker is rejected by load_current_user. profile_password_change now writes a naive password_changed_at so the in-memory marker matches what SQLite returns on next read. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d25fb57f30
commit
47722dbb19
2 changed files with 74 additions and 1 deletions
|
|
@ -83,7 +83,9 @@ def profile_password_change() -> Response:
|
||||||
if user is None or not verify_password(current_password, user.password_digest):
|
if user is None or not verify_password(current_password, user.password_digest):
|
||||||
return _redirect_with_error("wrong_current")
|
return _redirect_with_error("wrong_current")
|
||||||
user.password_digest = hash_password(new_password)
|
user.password_digest = hash_password(new_password)
|
||||||
user.password_changed_at = now_utc()
|
# Strip tz so the marker matches what a subsequent DB read returns
|
||||||
|
# (SQLite DateTime columns don't preserve tzinfo).
|
||||||
|
user.password_changed_at = now_utc().replace(tzinfo=None)
|
||||||
new_marker = user.password_changed_at.isoformat()
|
new_marker = user.password_changed_at.isoformat()
|
||||||
|
|
||||||
session["pw_changed_at"] = new_marker
|
session["pw_changed_at"] = new_marker
|
||||||
|
|
|
||||||
|
|
@ -160,3 +160,74 @@ def test_post_password_wrong_current(app_and_user):
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
assert "/profile?error=wrong_current" in response.headers["Location"]
|
assert "/profile?error=wrong_current" in response.headers["Location"]
|
||||||
assert _digest_of("alice") == before
|
assert _digest_of("alice") == before
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_password_happy_path_rotates_and_keeps_current_session(app_and_user):
|
||||||
|
app, uid, marker = app_and_user
|
||||||
|
client = _logged_in_client(app, uid, marker)
|
||||||
|
before_digest = _digest_of("alice")
|
||||||
|
with session_scope() as db:
|
||||||
|
before_marker = db.scalar(select(User).where(User.id == uid)).password_changed_at
|
||||||
|
|
||||||
|
response = _post_pw(
|
||||||
|
client,
|
||||||
|
current_password="currentpass",
|
||||||
|
new_password="newpass12",
|
||||||
|
confirm_new_password="newpass12",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.headers["Location"].endswith("/profile?success=1")
|
||||||
|
|
||||||
|
after_digest = _digest_of("alice")
|
||||||
|
assert after_digest != before_digest
|
||||||
|
with session_scope() as db:
|
||||||
|
u = db.scalar(select(User).where(User.id == uid))
|
||||||
|
assert u.password_changed_at > before_marker
|
||||||
|
new_marker = u.password_changed_at.isoformat()
|
||||||
|
|
||||||
|
follow = client.get("/dashboard")
|
||||||
|
assert follow.status_code == 200
|
||||||
|
|
||||||
|
with client.session_transaction() as sess:
|
||||||
|
assert sess["pw_changed_at"] == new_marker
|
||||||
|
|
||||||
|
|
||||||
|
def test_other_session_dies_after_password_change(app_and_user):
|
||||||
|
app, uid, marker = app_and_user
|
||||||
|
primary = _logged_in_client(app, uid, marker)
|
||||||
|
other = _logged_in_client(app, uid, marker)
|
||||||
|
|
||||||
|
pre = other.get("/dashboard")
|
||||||
|
assert pre.status_code == 200
|
||||||
|
|
||||||
|
response = _post_pw(
|
||||||
|
primary,
|
||||||
|
current_password="currentpass",
|
||||||
|
new_password="newpass12",
|
||||||
|
confirm_new_password="newpass12",
|
||||||
|
)
|
||||||
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
post = other.get("/dashboard", follow_redirects=False)
|
||||||
|
assert post.status_code == 302
|
||||||
|
assert "/login" in post.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_password_works_for_login(app_and_user):
|
||||||
|
app, uid, marker = app_and_user
|
||||||
|
client = _logged_in_client(app, uid, marker)
|
||||||
|
_post_pw(
|
||||||
|
client,
|
||||||
|
current_password="currentpass",
|
||||||
|
new_password="newpass12",
|
||||||
|
confirm_new_password="newpass12",
|
||||||
|
)
|
||||||
|
|
||||||
|
fresh = app.test_client()
|
||||||
|
response = fresh.post(
|
||||||
|
"/login",
|
||||||
|
data={"username": "alice", "password": "newpass12"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.headers["Location"].endswith("/dashboard")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue