feat(opaque): improve password change

- rebuild the opaque envoloppe
- revoke all other useer's sessions
- send a security email to user
This commit is contained in:
Edouard Vanbelle
2026-08-05 23:35:05 +02:00
parent 60cf9d976b
commit 21607e3e7f
29 changed files with 611 additions and 33 deletions
+13 -6
View File
@@ -68,12 +68,19 @@ export async function changePassword(currentPw: string, newPw: string): Promise<
body: JSON.stringify({ current_password: currentPw, new_password: newPw })
});
if (!res.ok) throw new Error(`password change failed: ${res.status}`);
// Re-mint the OPAQUE envelope under the new passphrase — session
// stays valid across change-password (backend doesn't invalidate),
// so the session-authenticated register endpoints are reachable
// straight away. Non-fatal on failure: silent migration on next
// legacy login recovers the envelope. See
// `$lib/api/endpoints/opaque.ts::syncOpaqueEnvelope`.
// Re-mint the OPAQUE envelope under the new passphrase — SAME
// session is still valid after change_password (the backend now
// preserves the caller's session via `revoke_other_user_sessions`;
// only OTHER devices are logged out). That means the session-
// authenticated register endpoints are reachable straight away,
// no 401 race like the earlier `revoke_all_user_sessions` shape.
//
// This is the PRIMARY migration path: the envelope transitions
// straight from OLD-password bound to NEW-password bound with no
// null intermediate. `opaque_migrated_at` stays intact, admin
// dashboards don't see a spurious "unmigrated" blip. Non-fatal
// on failure — silent-migration on next legacy login (post
// `oxicloud-cli opaque reset` recovery) is the fallback.
//
// Dynamic import keeps the ~200 KiB `@serenity-kit/opaque` WASM
// bundle out of the profile route's initial chunk — the module
+11
View File
@@ -227,6 +227,17 @@ export interface User {
* missing → `false`.
*/
force_password_change?: boolean;
/**
* TRUE when the account has a local Argon2id `password_hash` on
* file. Distinct from `auth_provider`: an SSO-linked account can
* ALSO carry a local password (hybrid posture — SSO for daily
* login, local password as fallback). The profile page's
* change-password card gates on this flag rather than on
* `auth_provider === 'local'` so hybrid users can rotate their
* local credential. Optional on the wire for older-backend
* compatibility; missing → `false` (safe default: hide the card).
*/
has_password?: boolean;
}
/** Fields rendered by the paginated admin table. Full account details remain