Skip to content

Password change and reset actually work; old sessions end - #38

Merged
alikahwaji merged 1 commit into
developfrom
fix/password-endpoints
Oct 23, 2026
Merged

alikahwaji merged 1 commit into
developfrom
fix/password-endpoints

Conversation

@alikahwaji

Copy link
Copy Markdown
Contributor

Found while preparing SSO. The password endpoints were stubs:

  • Settings → Change password showed "Your password has been changed successfully" without changing anything; neither the page nor the API stored it.
  • Forgot / reset password claimed a link had been sent, or the password reset, while doing nothing.

A user who forgot their password had no way back in: admins can't reset passwords, and re-inviting fails because the email already exists.

Now

  • Change password:
    • checks the current password and stores the new one (bcrypt)
    • refuses a wrong current password, the same password, or one too short
    • demo accounts are refused
    • the response carries new tokens, so the current session continues
  • Forgot password: emails a reset link using the SMTP settings from Phase 2: scheduled supplier risk digest by email #37.
    • The link works once, for 30 minutes, and only the newest one works.
    • The answer is the same whether or not the account exists, and the email is sent in the background, so neither the answer nor its timing reveals which emails have accounts.
    • Without SMTP the page says that reset by email isn't set up.
  • Reset password: consumes the link once (a SECURITY DEFINER function, in one statement) and sets the password.
  • Other sessions end: users.password_changed_at means refresh tokens issued before a change or reset are refused. Access tokens already issued last at most 60 minutes.
  • Migration 022:
    • the password_changed_at column and the password_resets table (token hash only)
    • scirm_user_by_id recreated to return the new column
    • two reset functions, granted to the app role by db_setup.py, which the migrate job runs
  • Frontend: the Settings form, the forgot-password page and a new /reset-password page. The token stays in the URL fragment, like invitation links.
  • Docs: docs/passwords.md.

Testing

  • New tests/security/test_passwords.py:
    • a change really changes the password; the old refresh token is refused and the new one works
    • wrong current, same or short passwords are refused
    • demo accounts are refused
    • without SMTP the forgot-password endpoint returns 503
    • the same answer for known and unknown emails, and only the known one gets mail
    • a link works once and signs out old sessions
    • a newer link invalidates the older one
    • an expired link is refused
  • tests/db: the reset functions work with a non-superuser table owner (managed PostgreSQL), and a token works exactly once. Migrations still round-trip.
  • Suites: 545 in-memory; security + db + evidence on PostgreSQL: 343. Frontend: 35, tsc clean, vite build OK. Strict docs build OK.
  • Live on the gate stack (migrated 021 → 022 on existing data):
    • forgot password → the email arrived in Mailpit with a localhost:3000/reset-password#… link → set a new password in the browser
    • the old password got 401, the new one 200, and reusing the link got 400
    • auth.password_reset_requested and auth.password_reset appear in the audit log
    • Settings → Change password refused a wrong current password with a clear message, then changed it and kept the session

Deploy note

Migration 022 recreates scirm_user_by_id. The migrate job re-grants it. Restart the gateway after migrating, as usual.

🤖 Generated with Claude Code

The password endpoints were stubs: "Change password" said the password was
changed without changing it, and forgot/reset password claimed success
while doing nothing. A locked-out user had no way back in.

- Change password checks the current password and stores the new one;
  the response carries new tokens so the current session continues.
- Forgot password emails a single-use reset link (30 minutes, newest only)
  over the SMTP settings from scheduled reports. Same answer whether or not
  the account exists; the email is sent in the background. Without SMTP it
  says reset by email is not set up.
- Reset password consumes the link once (SECURITY DEFINER function, one
  statement) and sets the password.
- Migration 022: users.password_changed_at (refresh tokens issued earlier
  are refused, so a change or reset signs out other sessions),
  password_resets (token hash only), scirm_user_by_id returns the new
  column, and the two reset functions (granted by db_setup).
- Frontend: Settings calls the API; the forgot-password page calls it and
  shows its answer; new /reset-password page (token in the fragment).
- docs/passwords.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alikahwaji
alikahwaji merged commit b69b9b6 into develop Oct 23, 2026
21 checks passed
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.

1 participant