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
+10
View File
@@ -363,6 +363,16 @@ pub trait SessionStoragePort: Send + Sync + 'static {
/// Revokes all sessions of a user
async fn revoke_all_user_sessions(&self, user_id: Uuid) -> Result<u64, DomainError>;
/// Revokes every session of a user EXCEPT `keep_session_id`.
/// Classic "password change" pattern: kills OTHER devices' sessions
/// while keeping the caller's current session alive so the SPA can
/// complete follow-up work without a session-death race.
async fn revoke_other_user_sessions(
&self,
user_id: Uuid,
keep_session_id: Uuid,
) -> Result<u64, DomainError>;
/// Revokes all sessions in a token family (used when replay of a revoked token is detected)
async fn revoke_session_family(&self, family_id: Uuid) -> Result<u64, DomainError>;
+20
View File
@@ -122,6 +122,26 @@ pub trait OpaqueRepositoryPort: Send + Sync + 'static {
/// (that's the point of the admin call).
async fn clear_registration(&self, user_id: Uuid) -> Result<()>;
/// Invalidate the OPAQUE envelope for `user_id` WITHOUT touching
/// `force_password_change_at_next_login`. Used by the self-service
/// `change_password` path: the user just proved and rotated their
/// legacy password, so the OLD envelope (bound to the OLD
/// passphrase) MUST go, but no forced-change prompt is needed on
/// the next login (the user did just change it themselves).
///
/// Distinct from [`clear_registration`], which co-flips
/// `force_password_change` because that path represents an admin
/// override — the user did NOT choose the new value, so they must
/// pick their own on next login. Change-password is the inverse:
/// user chose the value, no re-choice needed.
///
/// Also used by `oxicloud-cli opaque reset --user X` for KSF
/// rotation recovery — same "envelope stale, don't touch other
/// state" semantics.
///
/// Idempotent: nulling already-null columns is a no-op.
async fn clear_envelope_only(&self, user_id: Uuid) -> Result<()>;
/// Stamp `opaque_migrated_at` on `user_id` if it isn't set yet.
/// Called by the login-KE3 handler after a successful OPAQUE
/// handshake — the presence of this timestamp is the Phase 3+