Skip to content

Commit 523fc48

Browse files
fix: revoke sessions on password reset (#215)
Password reset is the standard response to a suspected compromise, but reset_password was leaving existing refresh tokens valid. Revoke all refresh tokens before issuing the new auto-login session, matching recover_account and promote_email.
1 parent 4fb0091 commit 523fc48

2 files changed

Lines changed: 42 additions & 0 deletions

File tree

routers/core/account.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,8 @@ async def reset_password(
734734
session.commit()
735735
session.refresh(authorized_account)
736736

737+
revoke_all_refresh_tokens(authorized_account.id, session)
738+
737739
# Auto-login: issue new auth cookies so the user doesn't have to re-enter credentials
738740
access_token = create_access_token(
739741
data={"sub": authorized_account.email, "fresh": True}

tests/routers/core/test_account.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,46 @@ def test_password_reset_auto_logs_in_and_shows_flash(
706706
assert any("flash_message=" in c for c in cookie_headers)
707707

708708

709+
def test_password_reset_revokes_existing_sessions(
710+
unauth_client: TestClient, session: Session, test_account: Account
711+
):
712+
"""Password reset revokes all existing refresh tokens before auto-login."""
713+
attacker_session = RefreshToken(
714+
account_id=test_account.id,
715+
expires_at=datetime.now(UTC) + timedelta(days=30),
716+
)
717+
session.add(attacker_session)
718+
session.commit()
719+
session.refresh(attacker_session)
720+
721+
reset_token = PasswordResetToken(account_id=test_account.id)
722+
session.add(reset_token)
723+
session.commit()
724+
session.refresh(reset_token)
725+
726+
response = unauth_client.post(
727+
app.url_path_for("reset_password"),
728+
data={
729+
"email": test_account.email,
730+
"token": reset_token.token,
731+
"password": "NewPass123!@#",
732+
"confirm_password": "NewPass123!@#",
733+
},
734+
)
735+
assert response.status_code == 303
736+
737+
session.refresh(attacker_session)
738+
assert attacker_session.revoked is True
739+
740+
active_tokens = session.exec(
741+
select(RefreshToken).where(
742+
RefreshToken.account_id == test_account.id,
743+
RefreshToken.revoked == False, # noqa: E712
744+
)
745+
).all()
746+
assert len(active_tokens) == 1
747+
748+
709749
def test_password_reset_after_recovery_auto_logs_in(
710750
unauth_client: TestClient, session: Session
711751
):

0 commit comments

Comments
 (0)