mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 17:31:10 +00:00
[AZ-556] [AZ-557] Unify login errors + share MFA lockout pipeline
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>
This commit is contained in:
@@ -23,6 +23,24 @@ public static class Security
|
||||
|
||||
public sealed record VerifyResult(bool Valid, bool NeedsRehash);
|
||||
|
||||
// AZ-556 — timing equalizer for unknown-email and disabled-account branches of
|
||||
// `UserService.ValidateUser`. Pre-computed once with the same Argon2id parameters
|
||||
// as a real hash so a `VerifyDummy(plaintext)` call costs ~the same wall-clock as
|
||||
// a real `VerifyPassword(plaintext, user.PasswordHash)`. The result is always
|
||||
// discarded — this is a side-channel mitigation, not a control-flow path.
|
||||
private static readonly string DummyHashForTiming = HashPassword(
|
||||
"az-556-timing-equalizer-dummy-do-not-store-in-db");
|
||||
|
||||
/// <summary>
|
||||
/// AZ-556 — run the same Argon2id work a real verify would do, then discard the
|
||||
/// result. Used to keep the unknown-email and disabled-account login branches
|
||||
/// timing-indistinguishable from a wrong-password branch.
|
||||
/// </summary>
|
||||
public static void VerifyDummy(string plaintext)
|
||||
{
|
||||
_ = VerifyPassword(plaintext, DummyHashForTiming);
|
||||
}
|
||||
|
||||
public static string HashPassword(string plaintext)
|
||||
{
|
||||
if (plaintext == null) throw new ArgumentNullException(nameof(plaintext));
|
||||
|
||||
Reference in New Issue
Block a user