The user lockout feature is a way to improve application security by locking out a user who 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 navigate through the entire series, visit the ASP.NET Core Identity series page.
VIDEO: Increase the Security of Your Web APIs With User Lockout Functionality .
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:
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:
Now, if we try the same credentials two more times:
We can see the account locked out and we can confirm that in the database:
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:
And if we try to use the same username and password:
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.