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 How to Add a Password to a Local Account Linked to an External Login in ASP.NET Core Identity.

What is Account Lockout in ASP.NET Core Identity?

Account lockout is a security feature commonly used in web applications to protect user accounts from unauthorized access, particularly in cases of repeated failed login attempts. This feature is part of an overall strategy to prevent brute force attacks, where an attacker tries numerous combinations of usernames and passwords to gain access to user accounts.

If we take the example of bank or internet banking, the bank locks the account after 5 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 Does the Account Lockout Work?

Here’s a breakdown of how account lockout typically works:

  • Threshold for Failed Attempts: The system sets a threshold for the number of consecutive failed login attempts. For example, an account might be locked after 5 unsuccessful login attempts.
  • Lockout Duration: The account is locked for a specified duration once the threshold is reached. Depending on the application’s security policy, this can be a few minutes, hours, or even days.
  • User Notification: When an account is locked, the user is usually notified on the login screen or via email. This notification often includes information on 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).
  • Prevention of Enumeration Attacks: Care must be taken to ensure that the lockout feature does not provide clues to attackers. For example, messages like “User not found” or “Account locked” can reveal whether a username is valid to an attacker.

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 login attempts. After 5 failed attempts, the account will be locked for another 15 minutes. So, it will take many years for an attacker to crack the password successfully.

An organization may also have a password change policy, meaning the password must be changed every 3 or 6 months. So, an account lockout policy combined with a password change policy makes it extremely difficult for an attacker to brute-force (i.e., guess) passwords and gain access. So, when we provide an invalid password and click on the Login button, then it will display Invalid Login Attempt and also display the number of remaining login attempts the account is locked, as shown in the below image:

Example to Understand Account Lockout in ASP.NET Core Identity

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 lock out. The LockoutEnd column stores the future date when the lockout ends. If it is less than or equal to the current DateTime, it means the account is unlocked.

How to Implement Account Lockout in ASP.NET Core Identity

We have configured the maximum number of failed attempts to 5 before locking the user account. We have made one failed attempt. Now try four more failed attempts with an invalid password, and then you will see the following Account Lockout message:

How Does the Account Lockout Work?

If you check the AspNetUsers database table, you should see the LockoutEnd column value as the future date, which indicates when the lockout has ended. It also reset the AccessFailedCount value to 0. Now, you have two options: You can wait until your account lockout period expires or reset your password 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 you 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. This setup is typically done in the Program.cs of your ASP.NET Core application. Using the following LockoutOptions class, we can configure the Lockout Options in ASP.NET Core Identity.

How do you 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 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. It defaults to 5 minutes.

This is done in the AddIdentity or AddDefaultIdentity 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:

builder.Services.AddIdentity<ApplicationUser, ApplicationRole>(
                options =>
                {
                    // Rest of the Code
                    
                    // 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();
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(string? email)
{
    //Send the Confirmation Email to the User Email Id
    await emailSender.SendEmailAsync(email, "Account Locked", "Your Account is Locked Due to Multiple Invalid Attempts.", false);
}
Enable Lockout in Login Logic

When implementing the login logic, you must check if the user account is locked. So, modify the Login Post Action method of the Account Controller as follows:

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginViewModel model, string? ReturnUrl)
{
    //If Model Login Failed, we also need to show the External Login Providers 
    model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();

    if (ModelState.IsValid)
    {
        //First Fetch the User Details by Email Id
        var user = await userManager.FindByEmailAsync(model.Email);

        //Then Check if User Exists, EmailConfirmed and Password Is Valid
        //CheckPasswordAsync: Returns a flag indicating whether the given password is valid for the specified user.
        if (user != null && !user.EmailConfirmed &&
                    (await userManager.CheckPasswordAsync(user, model.Password)))
        {
            ModelState.AddModelError(string.Empty, "Email not confirmed yet");
            return View(model);
        }

        // 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 is populated.
        // After the account is lockedout, even if we provide the correct username and password,
        // PasswordSignInAsync() method returns Lockedout result and
        // the login will not be allowed for the duration the account is locked.
        var result = await signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);

        if (result.Succeeded)
        {
            // Handle successful login
            // Check if the ReturnUrl is not null and is a local URL
            if (!string.IsNullOrEmpty(ReturnUrl) && Url.IsLocalUrl(ReturnUrl))
            {
                return Redirect(ReturnUrl);
            }
            else
            {
                // Redirect to default page
                return RedirectToAction("Index", "Home");
            }
        }
        if (result.RequiresTwoFactor)
        {
            // Handle two-factor authentication case
        }
        if (result.IsLockedOut)
        {
            //It's important to inform users when their account is locked.
            //This can be done through the UI or by sending an email notification.
            await SendAccountLockedEmail(model.Email);
            return View("AccountLocked");
        }
        else
        {
            // Handle failure
            // Get the number of attempts left
            var attemptsLeft = userManager.Options.Lockout.MaxFailedAccessAttempts - await userManager.GetAccessFailedCountAsync(user);

            ModelState.AddModelError(string.Empty, $"Invalid Login Attempt. Remaining Attempts : {attemptsLeft}");
            return View(model);
        }
    }

    // If we got this far, something failed, redisplay form
    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. After the account is locked, there are 2 options. Wait for the account lockout time to expire, which in our case is 15 minutes, and then try again or request a password reset if you have forgotten the password. So, create a view named AccountLocked.cshtml within the Views/Shared folder and then copy and paste the following code.

@{
    ViewData["Title"] = "Account Locked";
}

<h3 class="text-danger">
    Your account is locked, please try again after sometime or you may
    <a asp-action="ForgotPassword" asp-controller="Account">
        reset your password by clicking here
    </a>
</h3>
Set lockout end date on successful password reset.

When an account is locked out, you can either wait for the lockout duration to expire or manually unlock the account using the password reset option. This can also be done through an admin panel or similar interface. Here, we are unlocking the user on a successful password reset.

If the user is locked out, a password reset can be requested. Upon successful password reset, set the account lockout end date to the current UTC datetime so the user can log in with the new password. Use the SetLockoutEndDateAsync() method of the UserManager service for this. So, modify the ResetPassword Post Action method of the Account Controller as follows:

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
{
    if (ModelState.IsValid)
    {
        // Find the user by email
        var user = await userManager.FindByEmailAsync(model.Email);

        if (user != null)
        {
            // reset the user password
            var result = await userManager.ResetPasswordAsync(user, model.Token, model.Password);

            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);
                }

                //Once the Password is Reset, remove the token from the database if you are storing the token
                await userManager.RemoveAuthenticationTokenAsync(user, "ResetPassword", "ResetPasswordToken");
                return RedirectToAction("ResetPasswordConfirmation", "Account");
            }

            // Display validation errors. For example, password reset token already
            // used to change the password or password complexity rules not met
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError("", error.Description);
            }
            return View(model);
        }

        // To avoid account enumeration and brute force attacks, don't
        // reveal that the user does not exist
        return RedirectToAction("ResetPasswordConfirmation", "Account");
    }
    // Display validation errors if model state is not valid
    return View(model);
}

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

Why Should We Lock Accounts in ASP.NET Core Identity?

Locking accounts in ASP.NET Core Identity is an important security measure, primarily designed to protect user accounts from unauthorized access, particularly from brute-force and password-guessing attacks. Here are the key reasons for implementing account lockout:

Prevention of Brute-Force Attacks
  • Limiting Guess Attempts: By locking an account after a certain number of failed login attempts, you effectively limit the number of guesses an attacker can make. This makes it significantly harder for attackers to guess passwords through repeated attempts.
Enhancing Overall Security
  • Deterrent to Attackers: Knowing that accounts will be locked after a few failed attempts can deter potential attackers.
  • Protecting User Data: User accounts often access sensitive personal or business data. Account lockout helps safeguard this data from unauthorized access.
Compliance with Security Standards
  • Regulatory Requirements: Many industries have regulations that require certain security measures, including account lockouts, to be in place.
  • Best Practices: Following security best practices, including account lockouts, is essential for maintaining the integrity and trustworthiness of your application.
User Notification
  • Alerting Users to Suspicious Activity: If an account gets locked out due to multiple failed login attempts, it can alert the user and system administrators about a possible unauthorized access attempt.

In the next article, I will discuss How to Send SMS in ASP.NET Core. 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.

Leave a Reply

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