{"record":{"id":"4fc6e41e0a41e71a","repo":"fullstackhero/dotnet-starter-kit","slug":"account-is-temporarily-locked-due-to-too-many-failed-login","errorCode":null,"errorMessage":"Account is temporarily locked due to too many failed login attempts. Try again later.","messagePattern":"Account is temporarily locked due to too many failed login attempts\\. Try again later\\.","errorType":"exception","errorClass":"CustomException","httpStatus":423,"severity":"warning","filePath":"src/Modules/Identity/Modules.Identity/Services/IdentityService.cs","lineNumber":199,"sourceCode":"\n        return tenant;\n    }\n\n    private async Task<FshUser> FindAndValidateUserByCredentialsAsync(string email, string password)\n    {\n        var user = await _userManager.FindByEmailAsync(email.Trim().Normalize());\n        if (user is null)\n        {\n            // Generic 401 — never confirm or deny account existence from this path.\n            throw new UnauthorizedException();\n        }\n\n        // Lockout check runs BEFORE password check so an attacker can't tell a locked\n        // account from a wrong-password one on every request.\n        if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user))\n        {\n            _logger.LogWarning(\"Login attempted for locked account {UserId}\", user.Id);\n            throw new CustomException(\n                \"Account is temporarily locked due to too many failed login attempts. Try again later.\",\n                errors: null,\n                HttpStatusCode.Locked);\n        }\n\n        if (!await _userManager.CheckPasswordAsync(user, password))\n        {\n            if (_userManager.SupportsUserLockout)\n            {\n                await _userManager.AccessFailedAsync(user);\n                if (await _userManager.IsLockedOutAsync(user))\n                {\n                    _logger.LogWarning(\n                        \"Account {UserId} locked out after exceeding failed login threshold.\",\n                        user.Id);\n                }\n            }\n            throw new UnauthorizedException();","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Services/IdentityService.cs#L181-L217","documentation":"When the account is currently locked out (too many prior failed password attempts, per ASP.NET Identity lockout), the service throws CustomException with HTTP 423 Locked and this message instead of a generic 401. The lockout check intentionally runs before the password check.","triggerScenarios":"POSTing valid-or-invalid credentials for a user whose LockoutEnd is in the future after exceeding MaxFailedAccessAttempts; repeated automated login retries against the same account.","commonSituations":"CI scripts with stale credentials hammering login; a user mistyped the password several times; misconfigured password in a deployed environment; brute-force protection triggered by a shared NAT IP with retries across accounts of one user.","solutions":["Wait for the lockout window (default 5 minutes / configured LockoutOptions) to expire and retry with the correct password.","An admin can unlock immediately via the identity module's unlock endpoint or by clearing LockoutEnd in the user store.","Fix the credential source (secrets/env config) that caused the repeated failures, or add retry backoff in the calling client."],"exampleFix":"// before: retry loop with no backoff\nwhile (!ok) { ok = await LoginAsync(user, wrongPassword); }\n// after: honor 423 and stop\nif (response.StatusCode == HttpStatusCode.Locked) throw new ApiException(\"Account locked; wait before retrying\");","handlingStrategy":"try-catch","validationCode":"// client-side: track consecutive failures and back off before hitting the threshold (default 5)\nif (++failedAttempts >= 4) { showWarning('One more failed attempt locks the account.'); }","typeGuard":null,"tryCatchPattern":"catch (ApiError e) when (e.StatusCode === 423) {\n  const retryAt = parseRetryAfter(e) ?? Date.now() + 5 * 60_000;\n  scheduleRetry(retryAt); // do not hammer\n}","preventionTips":["Never retry login in a tight loop; use exponential backoff.","Keep credentials in a secret manager, not inline config, to avoid stale-password storms.","Monitor 423 responses in logs — bursts indicate a misconfigured automation job."],"tags":["auth","lockout","http-423","brute-force-protection"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}