Account Lockout in ASP.NET Core Identity

Account Lockout in ASP.NET Core Identity

In this article, I will discuss how to implement Account Lockout in ASP.NET Core Identity. Please read our previous article discussing Add Password to Local Account Linked to External Login.

Account Lockout in ASP.NET Core Identity

Account lockout in ASP.NET Core Identity is a feature that helps protect user accounts from too many failed login attempts. When a user enters the wrong password multiple times, the account gets locked for a short period. This makes it harder for attackers to guess passwords and improves the security of your application.

What is Account Lockout?

Account Lockout is a security feature commonly used in web applications that temporarily disables user access to their account after a certain number of failed login attempts within a specified time frame. This is designed to prevent BRUTE FORCE ATTACKS, where an attacker tries to guess a user’s password by repeatedly submitting different combinations.

If we take the example of bank or internet banking, the bank locks the account after five failed attempts. After how many failed attempts, should the account be locked out? It depends on the company’s lockout policy. The number of failed attempts after which an account should be locked is configurable in ASP.NET Core Identity.

How Account Lockout Works in Web Applications?

The Account Lockout process typically involves the following steps:

  • Tracking Login Attempts: The application tracks the number of consecutive unsuccessful login attempts made on a user’s account. Each time a user attempts to log in with incorrect credentials, the system counts the failure.
  • Lockout Threshold: If a user exceeds a predefined number of failed attempts, their account is locked for a specified period. For example, an account might be locked after five unsuccessful login attempts.
  • Lockout Duration: Once the threshold is reached, the account is locked for a specified duration. This can range from a few minutes to hours or even days, depending on the application’s security policy. The user cannot log in during this period, even with the correct password.
  • Notification (Optional): Upon lockout, the user may be notified via email or other means about the account lockout. This notification typically includes information about the reason for the lockout, the duration of the lockout, and any steps that can be taken to unlock the account.
  • Automatic Unlock or Manual Intervention: After the lockout period expires, the account may be automatically unlocked, or it may require manual intervention from the user or an administrator (such as resetting the password or contacting support).

The primary goal of ACCOUNT LOCKOUT is to prevent attackers from successfully guessing passwords. However, it’s also a double-edged sword in terms of user experience. Attackers can potentially use it to lock legitimate users out of their accounts (a situation known as a DENIAL-OF-SERVICE attack).

Therefore, it’s essential to strike a balance between security and usability when implementing this feature. Considerations might include setting reasonable thresholds, providing user-friendly unlock processes, and implementing additional security measures such as two-factor authentication.

Example to Understand Account Lockout in ASP.NET Core Identity

Let’s say we lock the account for 15 minutes after five failed login attempts have occurred. After 15 minutes, the user will receive an additional five login attempts. After five failed attempts, again, the account will be locked for another 15 minutes. This means it will take many years for an attacker to crack the password successfully. Let us proceed and see how we can implement this in our application using ASP.NET Core Identity:

Account Lockout Flow:

On our login page, when a user provides an invalid password and clicks the Login button, an “Invalid Login Attempt” error message is displayed. At the same time, it will also display the number of remaining login attempts before the account is locked, as shown in the image below:

Example to Understand Account Lockout in ASP.NET Core Identity

Now, if you verify the Users database table, you will see that the AccessFailedCount value has been incremented by 1Here, the LockouEnabled column specifies whether the user account can lock out or not. If the LockouEnabled value is 1, then the Account Lockout feature is enabled for the user; otherwise, it is disabled. The LockoutEnd column stores the future date when the lockout ends. If it is NULL or any date that is equal to or less than the current date, it means the account is unlocked. If it is greater than the current date and time, it means the account is locked.

How Account Lockout Works in Web Applications?

What Happens When the User reaches the Maximum Failed Attempts?

We have configured the maximum number of failed attempts to 5. So, after five failed attempts with an invalid password, the account will be locked, and you will see the following Account Lockout message. Please enter your password four more times with an Invalid Password to proceed to the following page.

What Happens When the User reaches the Maximum Failed Attempts?

Account Lockout Notification Email:

Once the account is locked, we also need to send a Notification Email to the registered and Confirmed Email ID notifying them that the account is locked, as shown in the image below:

Account Lockout in ASP.NET Core Identity

Once your account is locked, if you try to log in with the correct password, you will receive the ‘account locked out’ message.

Verify the Lockout in Database:

Now, if you check the Users database table, you will see the LockoutEnd column value to be future date which indicates when the lockout will be expired and it also reset the AccessFailedCount value to 0 as shown in the below image.

What is Account Lockout?

Now, you have two options:

  • You can wait until your account lockout time period expires, allowing you to log in again.
  • You can reset your password by using the Forgot Password option.

With Forgot Password, you will reset the password, and at the same time, you need to update the LockoutEnd column value to the current date and time or set it to null.

How to Configure Account Lockout Options in ASP.NET Core Identity?

Configuring Account Lockout Options in ASP.NET Core Identity involves setting various parameters that define the behaviour of the lockout mechanism. We need to do this within the Program class of our ASP.NET Core application using the following LockoutOptions class.

How to Configure Account Lockout Options in ASP.NET Core Identity?

Here,

  • AllowedForNewUsers: Gets or sets a flag indicating whether a new user can be locked out. Defaults to true. It returns true if a newly created user can be locked out; otherwise, it returns false.
  • MaxFailedAccessAttempts: Gets or sets the number of failed access attempts allowed before a user is locked out, assuming lockout is enabled—defaults to 5.
  • DefaultLockoutTimeSpan: Gets or sets the TimeSpan a user is locked out for when a lockout occurs—defaults to 5 minutes.

This is done in the AddIdentity method. You can set options like the number of failed login attempts and the lockout duration. So, modify the AddIdentity method within the Program class as follows:

// Register ASP.NET Core Identity Services using AddIdentity
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>(
        options =>
        {
            // Password settings
            options.Password.RequireDigit = true;               // Must include digits
            options.Password.RequiredLength = 8;                // Minimum length 8
            options.Password.RequireNonAlphanumeric = true;     // Must include special characters
            options.Password.RequireUppercase = true;           // Must include uppercase letters
            options.Password.RequireLowercase = true;           // Must include lowercase letters
            options.Password.RequiredUniqueChars = 4;           // At least 4 unique characters

            // Lockout Settings
            // Lockout new users
            options.Lockout.AllowedForNewUsers = builder.Configuration.GetValue<bool>("Identity:Lockout:AllowedForNewUsers", true);

            // Number of failed attempts allowed
            options.Lockout.MaxFailedAccessAttempts = builder.Configuration.GetValue<int>("Identity:Lockout:MaxFailedAccessAttempts", 5);

            // Lockout duration
            options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(
                builder.Configuration.GetValue<int>("Identity:Lockout:DefaultLockoutMinutes", 15));
        })
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();
Adding Lockout Keys in AppSettings.json file:

Please add the following to the appsettings.json file.

"Identity": {
  "Lockout": {
    "AllowedForNewUsers": true,
    "MaxFailedAccessAttempts": 5,
    "DefaultLockoutMinutes": 15
  }
}
Extend IEmailService Interface

Add a new method for lockout notification. So, modify the IEmailService interface as follows.

namespace ASPNETCoreIdentityDemo.Services
{
    public interface IEmailService
    {
        Task SendRegistrationConfirmationEmailAsync(string toEmail, string firstName, string confirmationLink);
        Task SendAccountCreatedEmailAsync(string toEmail, string firstName, string loginLink);
        Task SendResendConfirmationEmailAsync(string toEmail, string firstName, string confirmationLink);
        Task SendPasswordResetEmailAsync(string toEmail, string firstName, string resetLink);
        Task SendPasswordChangeNotificationAsync(string email, string userName, DateTime changeTimeUtc, string location, string device, string ipAddress);

        //New Method to send Account Lockout Notification
        Task SendAccountLockoutEmailAsync(string toEmail, string firstName, DateTimeOffset? lockoutEnd);
    }
}
EmailService Implementation

When a user account is locked due to multiple invalid login attempts, we need to inform the user by sending an email. So, please add the following method to the Email Service class.

public async Task SendAccountLockoutEmailAsync(string toEmail, string firstName, DateTimeOffset? lockoutEnd)
{
    string until = lockoutEnd.HasValue
        ? lockoutEnd.Value.UtcDateTime.ToString("dd MMM yyyy, HH:mm 'UTC'")
        : "a short period";

    string htmlContent = $@"
            <html><body style='font-family: Arial, sans-serif; background:#f4f6f8; margin:0; padding:20px;'>
              <div style='max-width:700px; margin:auto; background:#fff; padding:30px; border-radius:8px;'>
                <h2 style='color:#333;'>Account Locked</h2>
                <p style='font-size:16px; color:#555;'>Hello {firstName},</p>
                <p style='font-size:16px; color:#555;'>
                  Your account has been temporarily locked due to multiple unsuccessful sign-in attempts.
                  For your security, you will not be able to sign in until <strong>{until}</strong>.
                </p>
                <p style='font-size:16px; color:#555;'>
                  If this wasn't you, please reset your password immediately.
                </p>
                <p style='text-align:center;'>
                  <a href='{_configuration["AppSettings:BaseUrl"]}/Account/ForgotPassword' 
                     style='background:#0d6efd; color:#fff; padding:12px 24px; border-radius:6px; text-decoration:none; font-weight:bold;'>
                    Reset Password?
                  </a>
                </p>
                <p style='font-size:12px; color:#999; margin-top:30px;'>&copy; {DateTime.UtcNow.Year} Dot Net Tutorials</p>
              </div>
            </body></html>";

    await SendEmailAsync(toEmail, "Your Account Has Been Temporarily Locked - Dot Net Tutorials", htmlContent, true);
}
Update the LoginUserAsync Method in AccountService.cs

Inside the LoginUserAsync method, we need to check if the user account is locked out. If so, then we need to send the ‘account locked’ email notification to the user and redirect them to the account locked out page. We also need to display the number of Failed Attempts and the remaining attempts before the account is locked out. For this, we need to modify the LoginUserAsync method signature.

IAccountService Interface:

So, please modify the IAccountService interface as follows:

using ASPNETCoreIdentityDemo.ViewModels;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using System.Security.Claims;

namespace ASPNETCoreIdentityDemo.Services
{
    public interface IAccountService
    {
        Task<IdentityResult> RegisterUserAsync(RegisterViewModel model);
        Task<IdentityResult> ConfirmEmailAsync(Guid userId, string token);

        //Updating the signature
        Task<(SignInResult Result, int FailedAttempts, int RemainingAttempts, DateTimeOffset? LockoutEndUtc, int MaxAttempts)> LoginUserAsync(LoginViewModel model);
        Task LogoutUserAsync();
        Task SendEmailConfirmationAsync(string email);
        Task<ProfileViewModel> GetUserProfileByEmailAsync(string email);
        AuthenticationProperties ConfigureExternalLogin(string provider, string? redirectUrl);
        Task<ExternalLoginInfo?> GetExternalLoginInfoAsync();
        Task<SignInResult> ExternalLoginSignInAsync(string loginProvider, string providerKey, bool isPersistent);
        Task<IdentityResult> CreateExternalUserAsync(ExternalLoginInfo info);
        Task<bool> SendPasswordResetLinkAsync(string email);
        Task<IdentityResult> ResetPasswordAsync(ResetPasswordViewModel model);
        Task<IdentityResult> ChangePasswordAsync(ChangePasswordViewModel model, HttpContext httpContext);
        Task<bool> HasPasswordAsync(ClaimsPrincipal principal);
        Task<IdentityResult> SetPasswordAsync(SetPasswordViewModel model, ClaimsPrincipal principal);
    }
}
Modifying the LoginUserAsync Method in the Account Service

Now, please update the LoginUserAsync method in the AccountService class with the following code. The following code is self-explanatory; please refer to the comment lines for a better understanding.

public async Task<(SignInResult Result, int FailedAttempts, int RemainingAttempts, DateTimeOffset? LockoutEndUtc, int MaxAttempts)> LoginUserAsync(LoginViewModel model)
{
    var maxAttempts = _userManager.Options.Lockout.MaxFailedAccessAttempts;

    // Find the user by Email
    var user = await _userManager.FindByEmailAsync(model.Email);

    // Basic validations
    if (user == null)
        return (SignInResult.Failed, 0, maxAttempts, null, maxAttempts);

    if (!await _userManager.IsEmailConfirmedAsync(user))
        return (SignInResult.NotAllowed, 0, maxAttempts, null, maxAttempts);

    // Check if user is already locked out
    if (await _userManager.IsLockedOutAsync(user))
    {
        var lockoutEnd = await _userManager.GetLockoutEndDateAsync(user);

        // Send lockout notification email (idempotent)
        if (lockoutEnd.HasValue)
        {
            await _emailService.SendAccountLockoutEmailAsync(
                user.Email!, user.FirstName!, lockoutEnd.Value.UtcDateTime);
        }

        var failedCount = await _userManager.GetAccessFailedCountAsync(user);
        return (SignInResult.LockedOut, failedCount, 0, lockoutEnd, maxAttempts);
    }

    // Try login with lockout enabled
    var result = await _signInManager.PasswordSignInAsync(
        user.UserName!,
        model.Password,
        model.RememberMe,
        lockoutOnFailure: true // increments AccessFailedCount on invalid password
    );

    if (result.Succeeded)
    {
        // On success, Identity resets AccessFailedCount automatically; return clean counters
        user.LastLogin = DateTime.UtcNow;
        await _userManager.UpdateAsync(user);
        return (result, 0, maxAttempts, null, maxAttempts);
    }

    // Failed attempt (maybe became locked)
    int failedAttempts = await _userManager.GetAccessFailedCountAsync(user);
    int remainingAttempts = Math.Max(0, maxAttempts - failedAttempts);
    DateTimeOffset? lockoutEndUtc = null;

    // If just locked out, send email
    if (result.IsLockedOut)
    {
        var lockoutEnd = await _userManager.GetLockoutEndDateAsync(user);
        lockoutEndUtc = lockoutEnd;
        remainingAttempts = 0;

        if (lockoutEnd.HasValue)
        {
            await _emailService.SendAccountLockoutEmailAsync(
                user.Email!, user.FirstName!, lockoutEnd.Value.UtcDateTime);
        }
    }

    return (result, failedAttempts, remainingAttempts, lockoutEndUtc, maxAttempts);
}
Modify AccountController Login POST action method

Now, please update the Login POST action method as follows:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model)
{
    try
    {
        if (!ModelState.IsValid)
            return View(model);

        var (result, failedAttempts, remainingAttempts, lockoutEndUtc, maxAttempts) = await _accountService.LoginUserAsync(model);

        if (result.Succeeded)
        {
            // Redirect back to original page if ReturnUrl exists and is local
            if (!string.IsNullOrEmpty(model.ReturnURL) && Url.IsLocalUrl(model.ReturnURL))
                return Redirect(model.ReturnURL);

            // Otherwise, redirect to a default page (like user profile)
            return RedirectToAction("Profile", "Account");
        }

        // Handle login failure (e.g., account locked out, invalid credentials or unconfirmed email)
        if (result.IsLockedOut)
        {
            ViewBag.FailedCount = failedAttempts;
            ViewBag.MaxAttempts = maxAttempts;
            ViewBag.LockoutEndUtc = lockoutEndUtc;                    // DateTimeOffset? (UTC)
            ViewBag.SupportEmail = "support@dotnettutorials.net";     // optional config value

            // Redirect to friendly lockout page
            return View("AccountLocked");
        }

        if (result.IsNotAllowed)
        {
            ModelState.AddModelError("", "Email is not confirmed yet.");
        }
        else
        {
            // Show attempts info
            ModelState.AddModelError("", $"Invalid login attempt. " +
                $"Failed Attempts: {failedAttempts}. Remaining Attempts: {remainingAttempts}.");
        }

        return View(model);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error during login for email: {Email}", model.Email);
        ModelState.AddModelError("", "An unexpected error occurred. Please try again later.");
        return View(model);
    }
}
Account Locked View

If the user is locked out, you can inform them about their lockout status, typically through a view or a notification on your login page. So, create a view named AccountLocked.cshtml within the Views/Account folder and then copy and paste the following code.

@{
ViewData["Title"] = "Account Locked";
var failed = (int?)ViewBag.MaxAttempts ?? 5;                // show "max reached"
var max = (int?)ViewBag.MaxAttempts ?? 5;
var remaining = 0;
DateTimeOffset? lockoutEndUtc = ViewBag.LockoutEndUtc as DateTimeOffset?;
string supportEmail = (string?)ViewBag.SupportEmail ?? "support@dotnettutorials.net";
}
<div class="container-xxl pt-1 pb-4">
<div class="row justify-content-center">
<div class="col-12 col-lg-11 col-xl-10 col-xxl-9">
<div class="card border-0 shadow-lg rounded-4 overflow-hidden">
<!-- Header -->
<div class="card-header bg-danger bg-gradient text-white text-center">
<div class="d-flex align-items-center justify-content-center gap-2 py-2">
<i class="bi bi-shield-lock-fill fs-3"></i>
<h3 class="mb-0 fw-semibold">Account Temporarily Locked</h3>
</div>
</div>
<!-- Body -->
<div class="card-body p-4 p-md-4 bg-body-tertiary">
<p class="lead text-center mb-3">
Multiple unsuccessful sign-in attempts triggered a temporary lock to keep your account safe.
</p>
<!-- Warning -->
<div class="alert alert-danger mb-3" role="alert">
<div class="d-flex align-items-start">
<i class="bi bi-exclamation-triangle-fill me-2 fs-5"></i>
<div>
<strong>Security notice:</strong>
Reset your password to unlock immediately, or wait for the lockout period to expire.
</div>
</div>
</div>
<!-- Metrics -->
<div class="row g-3 mb-3">
<div class="col-12 col-md-4">
<div class="border rounded-3 p-3 bg-body text-center h-70">
<div class="text-danger fw-bold fs-4">@failed</div>
<div class="text-muted small">Failed attempts (max reached)</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="border rounded-3 p-3 bg-body text-center h-70">
<div class="fw-bold fs-4">@max</div>
<div class="text-muted small">Allowed attempts</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="border rounded-3 p-3 bg-body text-center h-70">
<div class="fw-bold fs-4">@remaining</div>
<div class="text-muted small">Remaining this window</div>
</div>
</div>
</div>
<!-- Lockout end / countdown -->
@if (lockoutEndUtc.HasValue)
{
<div class="alert alert-warning mb-3" role="alert">
<div class="d-flex align-items-start">
<i class="bi bi-clock-history me-2 fs-5"></i>
<div>
<div class="fw-semibold">
Locked until:
<span id="lockout-end-local"
data-utc="@lockoutEndUtc.Value.UtcDateTime.ToString("o")">
@lockoutEndUtc.Value.ToLocalTime().ToString("dd MMM yyyy, hh:mm tt")
</span>
<span class="badge text-bg-light ms-2" id="countdown" aria-live="polite"></span>
</div>
<div class="text-muted small">You can try signing in again after this time.</div>
</div>
</div>
</div>
}
<!-- Next steps -->
<div class="card border-0 bg-body mb-3">
<div class="card-body">
<ol class="mb-0 ps-3">
<li class="mb-2">
<strong>Reset your password:</strong>
<a class="link-primary" href="@Url.Action("ForgotPassword", "Account")">Start reset</a> to unlock immediately.
</li>
<li class="mb-2">
<strong>Wait & retry:</strong>
You can sign in again after the lockout window ends.
</li>
<li class="mb-0">
<strong>Contact support:</strong>
Email <a class="link-primary" href="mailto:@supportEmail">@supportEmail</a>.
</li>
</ol>
</div>
</div>
<!-- CTAs -->
<div class="d-flex flex-wrap justify-content-center gap-2">
<a href="@Url.Action("ForgotPassword", "Account")" class="btn btn-success px-4 rounded-pill">
<i class="bi bi-unlock-fill me-1"></i> Reset password
</a>
<a href="@Url.Action("Login", "Account")" class="btn btn-primary px-4 rounded-pill">
<i class="bi bi-box-arrow-in-right me-1"></i> Return to login
</a>
<a href="@Url.Action("Support", "Home")" class="btn btn-info px-4 rounded-pill">
<i class="bi bi-life-preserver me-1"></i> Support center
</a>
</div>
</div>
<!-- Footer -->
<div class="card-footer bg-body text-center text-muted small">
Thank you for your understanding—your security is our priority.
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@* Live countdown until unlock (if LockoutEndUtc provided) *@
<script>
(function () {
var el = document.getElementById('lockout-end-local');
var cd = document.getElementById('countdown');
if (!el || !cd) return;
var utcIso = el.getAttribute('data-utc');
if (!utcIso) return;
var end = new Date(utcIso); // UTC ISO 8601
function pad(n) { return n.toString().padStart(2, '0'); }
function tick() {
var now = new Date();
var diff = end - now;
if (diff <= 0) { cd.textContent = 'Unlocked'; return; }
var s = Math.floor(diff / 1000);
var h = Math.floor(s / 3600); s -= h * 3600;
var m = Math.floor(s / 60);   s -= m * 60;
cd.textContent = '(' + pad(h) + ':' + pad(m) + ':' + pad(s) + ' remaining)';
requestAnimationFrame(function () { setTimeout(tick, 500); });
}
tick();
})();
</script>
}
Set Lockout End Date on Successful Password Reset

When an account is locked, the user can either wait for the lockout duration to expire or manually unlock the account by resetting the password using the Forgot Password flow.

If the user is locked out, a password reset can be requested using the Forgot Password flow. Upon successful password reset, we need to set the account lockout end date to the current UTC date and time, or set it to null, so that the user can log in with the new password. So, please modify the ResetPasswordAsync method of the Account Service class as follows:

public async Task<IdentityResult> ResetPasswordAsync(ResetPasswordViewModel model)
{
// Find the user associated with the provided email
var user = await _userManager.FindByEmailAsync(model.Email);
// If user not found, return a generic failure (no details leaked for security)
if (user == null)
return IdentityResult.Failed(new IdentityError { Description = "Invalid request." });
// Decode the token that was passed in from the reset link
var decodedBytes = WebEncoders.Base64UrlDecode(model.Token);
var decodedToken = Encoding.UTF8.GetString(decodedBytes);
// Get stored token from AspNetUserTokens
var storedToken = await _userManager.GetAuthenticationTokenAsync(user, "ResetPassword", "PasswordResetToken");
if (string.IsNullOrEmpty(storedToken) || storedToken != decodedToken)
return IdentityResult.Failed(new IdentityError { Description = "Invalid or expired token." });
// Attempt to reset the user's password with the new one
var result = await _userManager.ResetPasswordAsync(user, decodedToken, model.Password);
// If successful, update the Security Stamp to invalidate any active sessions or tokens
if (result.Succeeded)
{
// Clear lockout so the user can log in immediately with the new password
// Setting null removes any lockout end; alternatively you could set UtcNow.
await _userManager.SetLockoutEndDateAsync(user, null);
// Reset failed access count so they start fresh
await _userManager.ResetAccessFailedCountAsync(user);
// Invalidate any active sessions / auth cookies
await _userManager.UpdateSecurityStampAsync(user);
// Delete token after successful use
await _userManager.RemoveAuthenticationTokenAsync(user, "ResetPassword", "PasswordResetToken");
}
return result;
}

With the above changes in place, run the application and test it; it should work as expected.

Account lockout is a simple yet effective way to enhance the security of your application. By locking accounts after repeated failed attempts, you reduce the risk of attacks while still allowing users to reset their passwords or attempt to log in again later.

In the next article, I will discuss the Password Change Policy in ASP.NET Core Identity. In this article, I explain how to implement Account Lockout in ASP.NET Core Identity. I hope you enjoy this article, How to Implement Account Lockout in ASP.NET Core Identity.

Registration Open – Full-Stack .NET with Angular & ASP.NET Core

New Batch Starts: 15th September, 2025
Session Time: 8:30 PM – 10:00 PM IST

Advance your career with our expert-led, hands-on live training program. Get complete course details, the syllabus, and Zoom credentials for demo sessions via the links below.

Contact: +91 70218 01173 (Call / WhatsApp)

Leave a Reply

Your email address will not be published. Required fields are marked *