AZ-556 collapses every /login rejection (unknown email, wrong password,
disabled account, lockout, per-account rate limit) to a single opaque
InvalidCredentials (70) → 401 response. Timing equalised by a new
Security.VerifyDummy using the same Argon2id parameters. Audit log keeps
the rejection category internally (login_failed_unknown_email,
login_failed_disabled).
AZ-557 wires /login/mfa into the existing per-account lockout +
rate-limit pipeline. MFA failures now feed UserService's shared failure
accounting (RegisterMfaFailedLogin → RegisterFailedLoginCore) and
CountRecentFailedLogins aggregates both login_failed and
mfa_login_failed rows. Successful TOTP / recovery resets the counter.
Deprecated five legacy ExceptionEnum members (NoEmailFound,
WrongPassword, UserDisabled, AccountLocked, LoginRateLimited) — kept
defined for cross-workspace verifier compatibility during the
deprecation window.
E2E coverage updated: AuthTests (byte-identical body assertion +
disabled-account audit row), LoginRateLimitTests, PasswordHashingTests,
SecurityTests, plus four new MfaLoginTests (AC1, AC2, AC5, AC7).
Code review verdict: PASS_WITH_WARNINGS (batch_06_cycle2_review.md).
Co-authored-by: Cursor <cursoragent@cursor.com>
Mid-Step-10 session handoff for the cycle-2 hotfix sprint. Records
deferred Jira transitions for AZ-552..AZ-555 (batch 5 commits landed
locally; tracker writes batched against the next /autodev step-0 replay)
and updates _autodev_state.md sub_step to point at batch 6 (AZ-556 +
AZ-557, 5 pts). No code changes.
Co-authored-by: Cursor <cursoragent@cursor.com>