Skip to content

fix: profile password change now works and is verified (#99)#106

Open
christianromeni wants to merge 2 commits into
mainfrom
fix/profile-password-change
Open

fix: profile password change now works and is verified (#99)#106
christianromeni wants to merge 2 commits into
mainfrom
fix/profile-password-change

Conversation

@christianromeni

Copy link
Copy Markdown
Contributor

Fixes #99

Problem

The profile page sent {current_password, new_password} to PATCH /users/:id, whose body struct only declares password. Go's JSON decoder silently drops the unknown fields, so req.Password == nil: the handler ran no password update and returned 200, the UI showed "password changed", and the stored hash never changed. The generic update path also never verified the current password.

Fix

Dedicated self-service endpoint POST /me/password:

  • Verifies current_password with bcrypt before changing anything; wrong current → 400 "current password is incorrect".
  • Changes only the authenticated user's password (user ID from the token, never from body/path).
  • Rejects SSO / password-less accounts; enforces 8..128 length.
  • On success, revokes the user's other sessions (RevokeUserSessionsExcept) while keeping the current session valid in both the DB and the cache - so the user stays logged in on the current device and any stolen session elsewhere is killed.
  • Burns a bcrypt comparison on the SSO/no-password paths so response timing does not reveal the account type.
  • Writes an audit event (metadata only, never the password).

The profile UI calls the new endpoint and surfaces the real backend error instead of always reporting success.

Known limitation

There is no per-attempt throttle on the current-password check yet (the caller already holds a valid session token). This will be wired through the login-throttle infrastructure from #104 once that lands, rather than adding a second throttle here.

Tests

  • Happy path regression: after change, login with the new password succeeds and the old password fails (would fail on the old code, which reported success without changing anything).
  • Wrong current → 400 + hash unchanged; short/long new → 400; SSO account → 400; no token → 401; only-own-password; session invalidation (other session 401, current survives in DB).
  • go test -race ./internal/api/admin/ ./internal/db/ and tsc --noEmit clean.

The profile page sent current_password/new_password to PATCH
/users/:id, whose body struct only knows "password", so the fields were
silently dropped: the handler returned 200 while the stored hash never
changed. The generic update path also performed no current-password
check.

Add a dedicated self-service endpoint POST /me/password that verifies
the current password with bcrypt before changing it, rejects SSO/
password-less accounts, and enforces the 8..128 length bounds. On
success it revokes the user's other sessions (RevokeUserSessionsExcept)
while keeping the current one valid in both the DB and the cache, and
writes an audit event. SSO/no-password paths burn a bcrypt comparison
so response timing does not reveal the account type. The profile UI
calls the new endpoint and surfaces real errors instead of always
reporting success.

Fixes #99
@snyk-io

snyk-io Bot commented Jun 10, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 55.20833% with 43 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/api/admin/auth.go 43.07% 29 Missing and 8 partials ⚠️
internal/db/api_keys.go 70.00% 3 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

bcrypt rejects inputs over 72 bytes, so a 73-128 char new_password passed
validation and then 500ed. Cap at 72 bytes and treat ErrPasswordTooLong
as 400.

Session revocation was best-effort: if it failed the handler still
returned 200, leaving other sessions live in the DB to re-enter the cache
on refresh. Password update and other-session revocation now run in one
transaction (ChangePasswordAndRevokeOtherSessions); on failure nothing
commits and the handler returns 500 instead of a false success.
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.

[Bug] Cant change password for generated admin user

1 participant