fix(auth): stop discarding a legitimate password on admin approval (E9.5 S1)

SEC-10 revoked any password set while an account's email was unverified, on both
the Google-adoption and the admin-approval path. On the approval path that was
redundant: password_login refuses an unverified account before it ever reads
is_active, so approval hands a pre-emptor's password nothing it could use. All the
revoke achieved there was destroying the password a legitimate registrant had just
chosen -- and since the admin normally decides before the user reads their mail,
'your password was cleared for security' was the DEFAULT experience, not an edge
case. The guard stays on the Google-adoption path, where the real owner does attest
ownership.

password_reset_required and the ok_reset banner stay: accounts stripped on prod
before this change have no other way back in.

Two existing tests pinned the old rule, so they are rewritten rather than deleted,
and the SEC-10 recovery pack now builds the revoked state through the surviving
producer.
This commit is contained in:
2026-09-03 23:32:34 +02:00
parent 7339a0b84b
commit 243869401a
6 changed files with 128 additions and 55 deletions
+4 -3
View File
@@ -339,9 +339,10 @@ function AuthCard() {
verifyFired.current = true;
api
.verifyEmail(verifyToken)
// "ok_reset": verified, but SEC-10 had dropped the password this account registered with (an
// admin approved it before it was verified). Sign-in would answer with the uniform credentials
// error, so point the user at the reset flow instead. Same value the legacy ?verify= redirect uses.
// "ok_reset": verified, but SEC-10 had dropped the password this account registered with. Sign-in
// would answer with the uniform credentials error, so point the user at the reset flow instead.
// Same value the legacy ?verify= redirect uses. Rare since E9.5 S1 (admin approval no longer
// revokes) — it still covers accounts stripped before that and the Google-adoption path.
.then((r) => setVerify(r.password_reset_required ? "ok_reset" : "ok"))
.catch(() => setVerify("invalid"));
}, [verifyToken]);
+4 -3
View File
@@ -37,9 +37,10 @@ export const authApi = {
{ quiet: true },
),
// Confirm an email-verification token the SPA read from the URL fragment (SB3).
// `password_reset_required` = SEC-10 dropped the password this account registered with (an admin
// approved it before it was verified), so signing in would fail with the uniform credentials error.
// The card sends the user to the reset flow instead of leaving them to guess.
// `password_reset_required` = SEC-10 dropped the password this account registered with, so signing
// in would fail with the uniform credentials error. The card sends the user to the reset flow instead
// of leaving them to guess. Rare since E9.5 S1: only accounts stripped before that change, and the
// Google-adoption path, still reach it.
verifyEmail: (token: string): Promise<{ ok: boolean; password_reset_required?: boolean }> =>
req("/auth/verify", { method: "POST", body: JSON.stringify({ token }) }, { quiet: true }),
// Re-issue a verification link (the first one expired, or was lost). Uniform response — never