Back to: ASP.NET Core Identity Tutorials
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 Two-Factor Authentication in ASP.NET Core Identity.
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 depends on the lockout policy of the company. 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 the user exceeds a predefined number of failed attempts, the account gets locked for a certain 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 be a few minutes, 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 often includes information about the reason for the lockout, how long the account will be locked, and what steps can be taken to unlock it.
- Automatic Unlock or Manual Intervention: After the lockout period expires, the account may be automatically unlocked or 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 important to balance 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 5 failed login attempts. After 15 minutes, the user will get another 5 attempts to log in. After 5 failed attempts, the account will be locked again for another 15 minutes. So, 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:
When the user provides an invalid password and clicks on the Login button, an Invalid Login Attempt error message will appear on our login page. At the same time, it also displays the number of remaining login attempts before the account is locked, as shown in the image below:
Now, if you verify the AspNetUsers database table, you will see the AccessFailedCount value increment by 1. Here, the LockouEnabled column specifies whether the user account can lockout. 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 indicating when the lockout ends. If it is NULL or any date that is less than or equal to the current date, it means the account is unlocked. If it is greater than the current datetime, it means the account is locked.
What Happens When the User Reached the Maximum Failed Attempts?
We have configured the maximum number of failed attempts as 5. So, after 5 failed attempts with an invalid password, the account will be locked, and you will see the following Account Lockout message. So, please 4 more times with an Invalid Password to show the following page.
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 that the account is locked, as shown in the below image:
Once your account is locked, if you try with the correct password, you will get the following message.
Verify the Lockout in Database:
Now, if you check the AspNetUsers database table, you will see the LockoutEnd column value to be the future date, which indicates when the lockout will expire, and it also reset the AccessFailedCount value to 0, as shown in the below image.
Now, you have two options:
- You can wait until your account lockout period expires so that you can 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 time.
How Do We 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 behavior of the lockout mechanism. We need to do this within the Program.cs of our ASP.NET Core application using the following LockoutOptions class.
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 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. It returns the number of failed access attempts allowed before a user is locked out, if lockout is enabled.
- 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:
//Configuration Identity Services builder.Services.AddIdentity<ApplicationUser, ApplicationRole>( options => { // Password settings options.Password.RequireDigit = true; options.Password.RequiredLength = 8; options.Password.RequireNonAlphanumeric = true; options.Password.RequireUppercase = true; options.Password.RequireLowercase = true; options.Password.RequiredUniqueChars = 4; // Lockout settings options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); // Lockout duration options.Lockout.MaxFailedAccessAttempts = 5; // Number of failed attempts allowed options.Lockout.AllowedForNewUsers = true; // Lockout new users }) .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); //Register the Token Generation Provider
Method to Send Account Locked Email to User
When the user account is locked due to multiple invalid login attempts, we must inform the user by email. For this purpose, we are going to use the following private method.
private async Task SendAccountLockedEmail(ApplicationUser user) { string subject = "Your Account Has Been Locked"; var ForgetPasswordLink = Url.Action("ForgotPassword", "Account", null, protocol: HttpContext.Request.Scheme); // Encode the link to prevent XSS attacks var safeLink = HtmlEncoder.Default.Encode(ForgetPasswordLink); string message = $@" <p>Dear {user.FirstName} {user.LastName},</p> <p>We have detected multiple unsuccessful login attempts to your account, and as a security measure, your account has been temporarily locked.</p> <p>The lockout will expire on: <strong>{user.LockoutEnd?.UtcDateTime.ToLocalTime():f}</strong></p> <p>If you initiated these attempts, please ensure you are using the correct login credentials. If you have forgotten your password, you can reset it using the following link:</p> <p><a href={safeLink}>Reset Password</a></p> <p>If you did not attempt to access your account, it is possible that someone else is trying to gain unauthorized access. We recommend that you reset your password immediately and contact our support team.</p> <p>For further assistance, please reach out to our support team at <a href='mailto:support@dotnettutorials.net'>support@dotnettutorials.net</a>.</p> <br /> <p>Thank you for your attention to this matter.</p> <p>Best regards,</p> <p>Dot Net Tutorials Team</p>"; await emailSender.SendEmailAsync(user.Email, subject, message, IsBodyHtml: true); }
Enable Account Lockout in Login Logic
We need to check if the user account is locked out in the Login Post action method. If so, we need to send the account-locked email notification to the user and redirect the user to the account-locked-out page. So, modify the Login Post Action method of the Account Controller as follows:
[HttpPost] public async Task<IActionResult> Login(LoginViewModel model) { if (ModelState.IsValid) { // Find user by email var user = await userManager.FindByEmailAsync(model.Email); if (user == null) { // Generic error message to avoid disclosing details ModelState.AddModelError(string.Empty, "Invalid login attempt."); model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); return View(model); } // Check if the user is locked out if (await userManager.IsLockedOutAsync(user)) { // Inform the user that their account is locked ModelState.AddModelError(string.Empty, "Your account is locked due to multiple unsuccessful login attempts. Please try again later or contact support."); model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); return View(model); } // Check Password first to avoid leaking information about Email Confirmation var passwordCheck = await userManager.CheckPasswordAsync(user, model.Password); if (!passwordCheck) { // Increment access failed count await userManager.AccessFailedAsync(user); // Calculate remaining attempts var accessFailedCount = await userManager.GetAccessFailedCountAsync(user); var maxFailedAccessAttempts = userManager.Options.Lockout.MaxFailedAccessAttempts; var attemptsLeft = maxFailedAccessAttempts - accessFailedCount; // Provide feedback to the user if (accessFailedCount != 0) { ModelState.AddModelError(string.Empty, $"Invalid login attempt. You have {attemptsLeft} more {(attemptsLeft == 1 ? "attempt" : "attempts")} before your account gets locked."); } else { // Handle lockout scenario await SendAccountLockedEmail(user); return RedirectToAction("AccountLocked", "Account"); } model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); return View(model); } // Password is valid, now check email confirmation if (!user.EmailConfirmed) { ModelState.AddModelError(string.Empty, "Your email address is not confirmed. Please confirm your email to log in."); model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); return View(model); } // SignIn the User // The last boolean parameter lockoutOnFailure indicates if the account should be locked on failed login attempt. // On every failed login attempt AccessFailedCount column value in AspNetUsers table is incremented by 1. // When the AccessFailedCount reaches the configured MaxFailedAccessAttempts which in our case is 5, // the account will be locked and LockoutEnd column will be populated. var result = await signInManager.PasswordSignInAsync(user, model.Password, model.RememberMe, lockoutOnFailure: true); if (result.Succeeded) { // Redirect user after successful sign-in if (!string.IsNullOrEmpty(model.ReturnUrl) && Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); } return RedirectToAction(nameof(HomeController.Index), "Home"); } else if (result.RequiresTwoFactor) { // Handle two-factor authentication // Generate a 2FA token either using DefaultPhoneProvider or DefaultEmailProvider // Which provider we use here, same we need to use while doing the verification var TwoFactorAuthenticationToken = await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider); //var TwoFactorAuthenticationToken3 = await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider); // Send SMS if the phone number is confirmed if (user.PhoneNumberConfirmed && !string.IsNullOrEmpty(user.PhoneNumber)) { var smsMessage = $"Your Two-Factor Authentication code is: {TwoFactorAuthenticationToken}. Please use this code to log in."; await smsSender.SendSmsAsync(user.PhoneNumber, smsMessage); } // Send Email if the email is confirmed if (user.EmailConfirmed && !string.IsNullOrEmpty(user.Email)) { var emailSubject = "Two-Factor Authentication Code"; var emailBody = $@" <p>Hello {user.FirstName} {user.LastName},</p> <p>Your Two-Factor Authentication code is: <strong>{TwoFactorAuthenticationToken}</strong></p> <p>If you did not request this code, please contact our support team immediately.</p> <p>Thank you,<br/>Your Dot Net Tutorials Team</p>"; await emailSender.SendEmailAsync(user.Email, emailSubject, emailBody, IsBodyHtml: true); } // Redirect to Two-Factor Authentication verification page with data return RedirectToAction("VerifyTwoFactorToken", "Account", new { model.Email, model.ReturnUrl, model.RememberMe }); } else if (result.IsLockedOut) { // Handle lockout scenario // It's important to inform users when their account is locked. await SendAccountLockedEmail(user); return RedirectToAction("AccountLocked", "Account"); } else { // Generic error message for invalid credentials. Also displaying the number of attempts left var attemptsLeft = userManager.Options.Lockout.MaxFailedAccessAttempts - await userManager.GetAccessFailedCountAsync(user); ModelState.AddModelError(string.Empty, $"Invalid login attempt. You have {attemptsLeft} more {(attemptsLeft == 1 ? "attempt" : "attempts")} before your account gets locked."); return View(model); } } // If we got this far, something failed; redisplay the form model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); return View(model); }
AccountLocked Action Method:
Next, please add the following AccountLocked GET Action method within the Account Controller. This action method will render a view showing that the account is locked.
[HttpGet] public IActionResult AccountLocked() { return View(); }
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 copy and paste the following code.
@{ ViewData["Title"] = "Account Locked"; } <div class="container mt-5"> <div class="row justify-content-center"> <div class="col-lg-6 col-md-8"> <div class="card shadow-sm border-0"> <div class="card-header bg-danger text-white text-center"> <h3 class="mb-0">Account Temporarily Locked</h3> </div> <div class="card-body"> <div class="text-center mb-4"> <i class="fas fa-lock fa-3x text-danger"></i> </div> <h5 class="card-title text-center">Security Notice</h5> <p class="card-text"> For your security, your account has been temporarily locked due to multiple unsuccessful login attempts. </p> <p class="card-text"> Please follow the steps below to regain access to your account: </p> <ul class="list-group list-group-flush mb-4"> <li class="list-group-item"> <strong>Reset Your Password:</strong> If you've forgotten your password, you can <a href="@Url.Action("ForgotPassword", "Account")" class="text-primary">reset it here</a>. </li> <li class="list-group-item"> <strong>Wait and Retry:</strong> If you believe this lockout was a mistake, please wait for the lockout period to expire (usually 15 minutes) before attempting to log in again. </li> <li class="list-group-item"> <strong>Contact Support:</strong> If you need further assistance, our support team is here to help. Reach out to us at <a href="mailto:support@dotnettutorials.net" class="text-primary">support@dotnettutorials.net</a>. </li> </ul> <div class="text-center"> <a href="@Url.Action("Support", "Home")" class="btn btn-outline-primary">Visit Support Center</a> <a href="@Url.Action("Login", "Account")" class="btn btn-primary">Return to Login</a> </div> </div> <div class="card-footer text-muted text-center"> Thank you for your understanding and patience. </div> </div> </div> </div> </div>
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 using the password reset option.
If the user is locked out, a password reset can be requested. Upon successful password reset, we need to set the account lockout end date to the current UTC DateTime so the user can log in with the new password. So, modify the ResetPassword Post Action method of the Account Controller as follows:
[HttpPost] [AllowAnonymous] public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model) { // Check if the incoming model passes all validation rules specified in the data annotations. if (ModelState.IsValid) { // Find the user in the database using the provided email address. var user = await userManager.FindByEmailAsync(model.Email); // Proceed only if the user exists in the database. if (user != null) { // Attempt to reset the user's password using the token and the new password provided in the model. var result = await userManager.ResetPasswordAsync(user, model.Token, model.Password); //Remove the Token from the database await userManager.RemoveAuthenticationTokenAsync(user, "ResetPassword", "ResetPasswordToken"); // If the password reset operation is successful, redirect the user to the Reset Password Confirmation page. if (result.Succeeded) { // Upon successful password reset and if the account is lockedout, // set the account lockout end date to current UTC date time, // so, the user can login with the new password if (await userManager.IsLockedOutAsync(user)) { await userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow); } return RedirectToAction("ResetPasswordConfirmation", "Account"); } // If the password reset fails, loop through the list of errors returned by Identity // and add them to the ModelState to display on the view. foreach (var error in result.Errors) { ModelState.AddModelError("", error.Description); // Add each error description to ModelState. } // Return the model back to the view so the user can fix any validation errors. return View(model); } // If the user is not found, redirect to the Reset Password Confirmation page to avoid // revealing whether an account exists for the provided email. // This approach prevents account enumeration attacks. return RedirectToAction("ResetPasswordConfirmation", "Account"); } // If the model state is invalid (e.g., missing or incorrect data), return the same view // and display validation errors to the user. return View(model); }
With the above changes in place, run the application and test it, and it should work as expected.
Account lockout is a crucial security feature in ASP.NET Core Identity that protects against brute force and other unauthorized login attempts. By configuring the lockout settings properly and using ASP.NET Core Identity’s built-in mechanisms, we can ensure our web application is more protected against malicious login attempts.
In the next article, I will discuss the Password Expiration 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 For New Online Training
Enhance Your Professional Journey with Our Upcoming Live Session. For complete information on Registration, Course Details, Syllabus, and to get the Zoom Credentials to attend the free live Demo Sessions, please click on the below links.