The user lockout feature is the way to improve application security by locking out a user that enters a password incorrectly several times. This technique can help us in protecting against brute force attacks, where an attacker repeatedly tries to guess a password.

In this article, we are going to learn how to implement the user lockout functionality in our application and how to implement a custom password validator that extends default password policies.

To download the source code for this project, visit the User Lockout with ASP.NET Core Identity repository.

To navigate through the entire series, visit the ASP.NET Core Identity series page.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

User Lockout Configuration

The default configuration for the lockout functionality is already in place, but if we want, we can apply our configuration. To do that, we have to modify the AddIndentity method in the ConfigureService method for .NET 5 or previous versions:

services.AddIdentity<User, IdentityRole>(opt =>
{
    //previous code removed for clarity reasons

    opt.Lockout.AllowedForNewUsers = true;
    opt.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(2);
    opt.Lockout.MaxFailedAccessAttempts = 3;
})

In .NET 6, we have to modify the Program class:

builder.Services.AddIdentity<User, IdentityRole>(opt =>
{
    //previous code removed for clarity reasons
    opt.Lockout.AllowedForNewUsers = true;
    opt.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(2);
    opt.Lockout.MaxFailedAccessAttempts = 3;
})

The user lockout feature is enabled by default, but we state that here explicitly by setting the AllowedForNewUsers property to true. Additionally, we configure a lockout time span to two minutes (default is five) and maximum failed login attempts to three (default is five). Of course, the time span is set to two minutes just for the sake of this example, that value should be a bit higher in production environments.

So, this is the way to configure the user lockout functionality in our application by using IdentityOptions.

Implementing User Lockout in the Login Action

If we check the Login action, we are going to see this code:

var result = await _signInManager.PasswordSignInAsync(userModel.Email, 
    userModel.Password, userModel.RememberMe, false);

The last parameter from the PasswordSignInAsync method stands for enabling or disabling the lockout feature. For now, it’s disabled and we have to enable it by setting it to true. Furthermore, this method returns a SignInResult with the IsLockedOut property we can use to check whether the account is locked out or not.

With that said, let’s modify the Login action:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IactionResult> Login(UserLoginModel userModel, string returnUrl = null)
{
    if (!ModelState.IsValid)
    {
        return View(userModel);
    }

    var result = await _signInManager.PasswordSignInAsync(userModel.Email, userModel.Password, userModel.RememberMe, lockoutOnFailure: true);
    if (result.Succeeded)
    {
        return RedirectToLocal(returnUrl);
    }

    if (result.IsLockedOut)
    {
        var forgotPassLink = Url.Action(nameof(ForgotPassword),"Account", new { }, Request.Scheme);
        var content = string.Format("Your account is locked out, to reset your password, please click this link: {0}", forgotPassLink);

        var message = new Message(new string[] { userModel.Email }, "Locked out account information", content, null);
        await _emailSender.SendEmailAsync(message);

        ModelState.AddModelError("", "The account is locked out");
        return View();
    }
    else
    {
        ModelState.AddModelError("", "Invalid Login Attempt");
        return View();
    }
}

By setting the lockoutOnFailure parameter to true, we enable the lockout functionality, thus enable  modification of the AccessFailedCount and LockoutEnd columns in the AspNetUsers table:

User Lockout columns in aspnetusers table

The AccessFailedCount column will increase for every failed login attempt and reset once the account is locked out. Additionally, the LockoutEnd column will have a DateTime value to represent the period until this account is locked out.

As we can see in the code, we check the IsLockedOut property, and if it is true, we send an email with the forgot password link and appropriate message, and return information about the locked out account, to the user.

About an Email Message

Sending an email message to inform a user about a locked-out account is a good practice. By doing that, we encourage the user to act proactively. That user can reset the password, or report that something is strange because they didn’t try to log in, which means that someone is trying to hack the account, etc.

Testing Time

Okay, let’s test this implementation.

If we try to log in with the wrong credentials, we will get the Invalid Login Attempt error and the AccessFailedCount column will increase:

Lockout AccessFailedCount increased

Now, if we try the same credentials two more times:

Account locked out

We can see the account locked out and we can confirm that in the database:

Account locked out database - User Lockout

Also, we can see the AccessFailedCount is reset.

You can check your email, to find the link to the forgot password action. From there, everything is familiar because we talked about it in a previous article.

Custom Password Validation

As we can see from the previous example, the user gets an email with the link to reset the password. But, even though IdentityOptions already has different password configuration properties, we can add custom validations as well. For example, we don’t want a password to be the same as a username or we don’t want a password to contain the word password in it, etc.

You get the point.

Well, let’s see how to do that.

First, we are going to create a new folder CustomValdiators with a single class inside:

public class CustomPasswordValidator<TUser> : IPasswordValidator<TUser> where TUser : class
{
    public async Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user, string password)
    {
        var username = await manager.GetUserNameAsync(user);
        if (username.ToLower().Equals(password.ToLower()))
            return IdentityResult.Failed(new IdentityError { Description = "Username and Password can't be the same.", Code = "SameUserPass" });

        if (password.ToLower().Contains("password"))
            return IdentityResult.Failed(new IdentityError { Description = "The word password is not allowed for the Password.", Code = "PasswordContainsPassword" });

        return IdentityResult.Success;
    }
}

We have to inherit from the IPasswordValidator<TUser> interface and implement the ValidateAsync method. Inside, we extract a username from a current user and then execute the required validations. If these validations check out, we return the Failed identity result, otherwise, we return success.

Now, we have to register this custom validator:

services.AddIdentity<User, IdentityRole>(opt =>
{
    //code removed for clarity reasons
})
 .AddEntityFrameworkStores<ApplicationContext>()
 .AddDefaultTokenProviders()
 .AddDefaultTokenProviders()                
 .AddTokenProvider<EmailConfirmationTokenProvider<User>>("emailconfirmation")
 .AddPasswordValidator<CustomPasswordValidator<User>>();

And that’s it. With the help of the AddPasswordValidator method, we can register our custom validator class. So, the final step is to test this feature.

If we try to use the password with the „password“ word in it:

User Lockout - Password not allowed in Password

And if we try to use the same username and password:

username and password can't be the same

Excellent. This also works for the Registration process.

Conclusion

In this article, we’ve learned:

  • How to create a custom Lockout configuration
  • The way to implement User Lockout functionality
  • Why is a good practice to send an email message for lockout
  • How to implement custom password validation

In the next article, we are going to talk about two-way authentication in ASP.NET Core Identity.

So, stay with us.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!