Skip to Content

Members can sign in with security questions when their 2FA email bounces

Shipped 2026-09-14

TUN-867 told a member when their passcode email was rejected and offered SMS where they had a verified phone. A member with no verified phone was told plainly there was no second channel and pointed at support — honest, but not a way in.

They can now answer their security questions to sign in. It reuses the username and password they just entered, and on a correct set of answers issues a session exactly as a passcode or authenticator success does — no password reset. That distinction is the whole point: the member’s password was never the problem, only their second factor, so the old “recover your password” route looped them back to the same failing email step.

Why this is not a weakening of 2FA

The endpoint sits behind the password, so it is a second factor on top of the password, not a password-free way in — no weaker than the answer-based path password recovery already permitted. Two controls keep it a genuine fallback rather than a standing alternative:

  • Only after a real bounce. When a passcode email actually fails, the member’s passcode row is stamped with bounce_at (15 minute window). Both fallback endpoints require it, and it is cleared on first successful use — and again whenever a new code is issued, so the marker only ever reflects the most recent send. Outside that window the endpoints refuse, so answers can never substitute for the emailed code except in the exact case the panel exists for.
  • Admins excluded. Admins authenticate with an authenticator app, never an emailed passcode, so the fallback does not apply to them; letting a knowledge factor stand in for their TOTP would have defeated the mandatory-authenticator control.

Sequenced after TUN-869 : the legacy plaintext security answers are hashed first, so the factor this leans on is protected before it becomes a way in.

Under the hood

Answer verification moved out of the password-recovery flow into a shared security-answers feature now that a second consumer (the login fallback) exists. The move also fixed a bug the migration rehearsal surfaced: the old verifier wrote each legacy answer’s new hash inside the compare loop and returned on the first mismatch, so a failed attempt persisted hashes for the answers that matched first. The shared version returns the migrations and applies them only after a full match, so a failed attempt mutates nothing.

Last updated on