mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 18:01:10 +00:00
4bf2e689cb
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>
6.5 KiB
6.5 KiB
Code Review Report
Batch: 6 (cycle 2, batch 6 of 6) Tasks: AZ-556 (unify_login_error_codes), AZ-557 (mfa_brute_force_lockout) Date: 2026-05-14 Verdict: PASS_WITH_WARNINGS
Findings
| # | Severity | Category | File:Line | Title |
|---|---|---|---|---|
| 1 | Medium | Spec-Gap | e2e/Azaion.E2E/Tests/PasswordHashingTests.cs | AZ-556 AC-5 — no dedicated paired-latency timing test |
| 2 | Medium | Spec-Gap | e2e/Azaion.E2E/Tests/MfaLoginTests.cs | AZ-557 AC-3 — CountRecentFailedLogins 2+3 mix covered only behaviourally |
| 3 | Low | Spec-Gap | e2e/Azaion.E2E/Tests/MfaLoginTests.cs | AZ-557 AC-4 — /login/mfa per-IP burst test deliberately omitted (matches AZ-537 stub) |
| 4 | Low | Maintainability | Azaion.Common/BusinessException.cs | Five deprecated ExceptionEnum members + two BusinessExceptionHandler mappings are dead in the login path |
Finding Details
F1: AZ-556 AC-5 — no dedicated paired-latency timing test (Medium / Spec-Gap)
- Location:
e2e/Azaion.E2E/Tests/PasswordHashingTests.cs(test suite scope) - Description: AC-5 calls for 1000 paired "unknown email" vs "known + wrong password" requests with p50/p95 within 5%. We have
Login_timing_is_independent_of_password_length_ac5(per-length timing), but not the unknown-vs-wrong paired comparison. - Suggestion: Structural mitigation already in place —
Security.VerifyDummyis constructed fromHashPassword(...)so it uses the same Argon2id parameters as the real verify. Adding 1000 paired E2E samples would add ~3 minutes to every CI run and Argon2id work-factor noise dominates the 5% ceiling anyway. Recommendation: accept structural argument; tracker follow-up if the deploy gate insists on the live measurement. - Task: AZ-556
F2: AZ-557 AC-3 — CountRecentFailedLogins 2+3 mix covered only behaviourally (Medium / Spec-Gap)
- Location:
e2e/Azaion.E2E/Tests/MfaLoginTests.cs - Description: AC-3 expects a direct assertion that
CountRecentFailedLoginsreturns 5 given 2login_failed+ 3mfa_login_failedrows. We test the contract end-to-end (AZ557_AC1, AZ557_AC2) — a wrong MFA crosses the threshold seeded by aFailedLoginCount = 9row, which only works if the counter aggregates both event types — but we do not exerciseAuditLog.CountRecentFailedLoginsdirectly with the exact 2+3 mix. - Suggestion: Acceptable today (behavioral coverage proves the contract). A direct unit test would require introducing a unit-test project for Azaion.Services. Recommendation: defer to the test-decompose pass.
- Task: AZ-557
F3: AZ-557 AC-4 — /login/mfa per-IP burst test deliberately omitted (Low / Spec-Gap)
- Location:
e2e/Azaion.E2E/Tests/MfaLoginTests.cs - Description: AC-4 expects HTTP 429 on a single-IP burst at
/login/mfa. The endpoint correctly carries.RequireRateLimiting(LoginPerIpPolicy)(Azaion.AdminApi/Program.cs:374). The behavioral test is intentionally not added — the same policy is exercised at/loginand the correspondingLoginRateLimitTests.AC1_Per_ip_rate_limit_returns_429is stubbed (Task.CompletedTask) because tripping the per-IP limiter from inside the suite destabilises every subsequent test that runs from the same client. - Suggestion: Accept the stub pattern from AZ-537 — code-level evidence (single policy object, single attachment line) covers the AC.
- Task: AZ-557
F4: Deprecated ExceptionEnum members + handler mappings are dead in the login path (Low / Maintainability)
- Location:
Azaion.Common/BusinessException.cs,Azaion.AdminApi/BusinessExceptionHandler.cs:55-56 - Description:
NoEmailFound,WrongPassword,UserDisabled,AccountLocked,LoginRateLimitedare no longer thrown byUserService.ValidateUser/MfaService.VerifyForLogin.NoEmailFound+WrongPasswordare still thrown by admin-side MFA Enroll/Confirm/Disable (lines 75, 81, 138, 166, 173 ofMfaService.cs), so they remain live — butUserDisabled,AccountLocked,LoginRateLimitedhave no remaining production throws. - Suggestion: Intentional. The AZ-556 task spec calls for a deprecation window so cross-workspace verifiers (gps-denied, satellite-provider, ui) that pattern-match on the old codes don't break. The deprecation notes in
BusinessException.csalready point to a future removal ticket. - Task: AZ-556
Phase Summary
| Phase | Result |
|---|---|
| 1 — Context loading | Task specs + dependencies table read |
| 2 — Spec compliance | AZ-556 ACs 1/2/3/6/7 covered; AC-4 covered structurally via Security.VerifyDummy + audit-row test; AC-5 documented gap (F1). AZ-557 ACs 1/2/5/6/7 covered; AC-3 covered behaviourally (F2); AC-4 by code-attachment + stub-parity (F3). |
| 3 — Code quality | SRP: RegisterFailedLoginCore + FailureKind enum keep both factors on one accounting path. DRY: shared lockout logic deduplicated. No swallowed errors. |
| 4 — Security quick-scan | Net security improvement (closes F-AUTH-1, F-AUTH-3, F-AUTH-MFA). No new injection surfaces. DummyHashForTiming plaintext is a labelled side-channel artefact, not a credential. |
| 5 — Performance scan | Security.VerifyDummy adds an Argon2id call to the unknown-email + disabled paths (required by threat model, bounded by per-IP limiter). CountRecentFailedLogins gained a second predicate on the existing composite index — no plan change. |
| 6 — Cross-task consistency | AZ-557 cleanly consumes AZ-556 primitives (InvalidCredentials, audit recorders, shared accounting). No conflicting patterns. |
| 7 — Architecture compliance | Azaion.Services → Azaion.Common (for AuthConfig) is the established layer direction. No new cross-component internal imports. No new cyclic deps. |
Verdict Logic
No Critical or High findings. Two Medium and two Low → PASS_WITH_WARNINGS.
Auto-Fix Gate Disposition
| # | Severity | Category | Eligible? | Disposition |
|---|---|---|---|---|
| 1 | Medium | Spec-Gap | Escalate | Documented structural mitigation; tracker follow-up if needed |
| 2 | Medium | Spec-Gap | Escalate | Behavioral coverage accepted; defer unit-test scaffolding |
| 3 | Low | Spec-Gap | Auto-fix-eligible by severity, but accepted as parity with AZ-537 stub | No change |
| 4 | Low | Maintainability | Auto-fix-eligible by severity, but intentional (deprecation window) | No change |
No findings require code changes in this batch. Verdict stays PASS_WITH_WARNINGS — the implement skill auto-fix gate proceeds.