fix(auth): the password alone calls off an account's reset waiting

A sign-in with the password alone, once a reset of the authenticators
took effect, left a reset of the account waiting: whoever read the
owner's mail could ask for one while the owner waited, and carry it out
after the owner was back (the second reading's M2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-20 20:53:02 +02:00
commit 38bb090575
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -733,7 +733,8 @@ Future<SignInChallenge> takeChallenge(
/// either, only a reset of the authenticators that took effect signs in: it
/// is carried out then (the authenticators and recovery codes go), and the
/// account must set one up again; until one is confirmed, the password
/// alone signs in. Refused ([PasswordRefusedException]): an account without
/// alone signs in, and calls off a reset of the account waiting (its owner
/// is back). Refused ([PasswordRefusedException]): an account without
/// a confirmed authenticator (and no reset), [PasswordRefusal.setupIncomplete];
/// no code while a reset waits, [PasswordRefusal.notDue]; anything else,
/// [PasswordRefusal.wrongCredentials].
@ -760,6 +761,7 @@ Future<bool> secondFactor(
// Carried out before (or after the account's reset): nothing to delete,
// and an authenticator enrolled since stays to be confirmed.
if (resetTookEffect) {
await _callOffAccountReset(session, account);
return true;
}
throw refusedFor(PasswordRefusal.setupIncomplete);
@ -776,6 +778,7 @@ Future<bool> secondFactor(
}
} else if (resetTookEffect) {
await _carryOutAuthenticatorReset(session, account);
await _callOffAccountReset(session, account);
return true;
} else if (reset != null) {
throw PasswordRefusedException(
@ -888,6 +891,19 @@ Future<void> _carryOutAuthenticatorReset(
);
});
/// Calls off the reset of [account] that a code by mail asked for: its
/// owner signed in with the password alone, once a reset of the
/// authenticators took effect. That reset's row stays: until an
/// authenticator is confirmed, the password alone signs in.
Future<void> _callOffAccountReset(Session session, UuidValue account) =>
// DELETE FROM "account_reset" WHERE "authUserId" = $1 AND "kind" = $2
AccountReset.db.deleteWhere(
session,
where: (t) =>
t.authUserId.equals(account) &
t.kind.equals(AccountResetKind.account),
);
/// Calls off every reset of [account] waiting: its owner signed in.
Future<void> _callOffResets(Session session, UuidValue account) =>
// DELETE FROM "account_reset" WHERE "authUserId" = $1