(C# ASP.NET Core) Account Lockout Functionality in Identity

You must have seen on various websites that a user account gets locked if the user makes about three unsuccessful login attempts. The exact same functionality is supported by the Identity API also - and it is possible to configure the number of failed attempts before an account gets locked. If a lockout timespan is specified, then the account remains locked for that specific duration. The default value is 5 minutes, but it can be set to any value. The number of failed attempts and the clock are reset when a user performs a successful login.
(Rev. 19-Mar-2024)

Categories | About |     |  

Parveen,

Table of Contents (top down ↓)

Step 1 - Configure the Lockout Options

Let us now visit the program.cs file to have a look at the lockout options.

Open the solution explorer and locate the program.cs file. Double click to open it. We have used this same file in our tutorials so far. First we have the namespaces. Then we have the builder and DbContext.

// program.cs file 

// namespaces 
using Microsoft.AspNetCore.Identity;

using MyRazorApp.Data;

using MyRazorApp.Utility;

using System.Security.Claims;

var builder = WebApplication.CreateBuilder();

builder.Services.AddDbContext<MyAuthContext>();

// identity 
builder.Services
.AddIdentity<IdentityUser, IdentityRole>(
  options => options.SignIn.RequireConfirmedAccount = true)
.AddTokenProvider<DataProtectorTokenProvider<IdentityUser>>
    (TokenOptions.DefaultProvider)
.AddEntityFrameworkStores<MyAuthContext>();

// for claims based auth 
builder.Services.AddAuthorization(options =>
{

  options.AddPolicy("MyPolicy", policy =>
  {

    policy.RequireClaim(ClaimTypes.Role, "Admin");

  });

});

// claims identity 
builder
.Services
.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>,
    AdditionalUserClaimsPrincipalFactory>();

// authentication 
builder.Services
.ConfigureApplicationCookie(options =>
{

  options.LoginPath = "/Auth/Login";

  options.AccessDeniedPath = "/Auth/AccessDenied";

  // Cookie settings 
  // prevent cookie from being accessed 
  // through javascript on the client side 
  options.Cookie.HttpOnly = true;

  options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

  options.SlidingExpiration = true;

}

);

builder.Services.Configure<IdentityOptions>(options =>
{

  // Password settings. 
  options.Password.RequireDigit = true;

  options.Password.RequireLowercase = true;

  options.Password.RequireNonAlphanumeric = true;

  options.Password.RequireUppercase = true;

  options.Password.RequiredLength = 6;

  options.Password.RequiredUniqueChars = 1;

  // Lockout settings. 
  options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);

  options.Lockout.MaxFailedAccessAttempts = 5;

  options.Lockout.AllowedForNewUsers = true;

  // User settings. 
  options.User.AllowedUserNameCharacters =
  "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";

  options.User.RequireUniqueEmail = false;

});

builder.Services.AddRazorPages();

var app = builder.Build();

// create the database 
// these calls can be removed in 
// a production scenario 
// the recommended method is through migrations 
// as explained in the first tutorial of this chapter 
using (var scope = app.Services.CreateScope())

{

  var services = scope.ServiceProvider;

  var ctx = services.GetRequiredService<MyAuthContext>();

  ctx.Database.EnsureCreated();

}

app.UseAuthentication();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

Then the services for Identity are configured. After that we have added services for claims based authorization. And, then we have the cookie options. After the cookie options, we have password settings. And, just next to them we have the lockout options.

The DefaultLockoutTimeSpan is the duration till which the account remains locked out. The account gets automatically unlocked after the expiry of this timespan. You can set a very long duration to lock a user's account so that he is forced to go through the email verification process again. MaxFailedAccessAttempts can also be configured as per the needs.

The rest of the file has already been discussed in the earlier tutorial.

Video Explanation (see it happen!)

Please watch the following youtube video:

Step 2 - Add a Lockout File

The next step is to add a file that displays a message to the user that his account is locked. We can even add links for customer service, or links for resetting the password.

Open the solution explorer and locate the Pages folder of the auth area. Right click this folder and a page called Lockout.cshtml. Double click to open the file. As you can see, this file contains a message for the user. We could add links for customer service, or links for resetting the password through the email verification process.

// Areas -> Auth -> Pages -> Lockout.cshtml 

@page

<h1>Account is locked out. Please retry later.</h1>

  

Step 3 - Modify the Login Page

You must have seen on various high security websites that the Login page displays the number of failed attempts as well as the total attempts available to a user.

We can do the same thing here also.

Open the solution explorer and locate the Login.cshtml.cs file. Double click to open this file so that we can examine the code. Please do recall that we have been using this file all through our tutorials so far, but now it needs an additional modification. But let us discuss it completely - with all the changes applied and updated!

// Areas -> Auth -> Pages -> Login.cshtml.cs 
using Microsoft.AspNetCore.Authorization;

using Microsoft.AspNetCore.Identity;

using Microsoft.AspNetCore.Mvc;

using Microsoft.AspNetCore.Mvc.RazorPages;

using System.ComponentModel.DataAnnotations;

namespace MyRazorApp.Areas.Auth.Pages
{

  [AllowAnonymous]
  public class LoginModel : PageModel
  {

    [BindProperty]
    [Required]
    [DataType(DataType.EmailAddress)]
    public String? UserID { get; set; }

    // userpassword 
    [BindProperty]
    [Required]
    [DataType(DataType.Password)]
    public String? UserPassword { get; set; }

    // remember me 
    [BindProperty]
    public Boolean RememberMe { get; set; }

    // dependency injection 
    private readonly SignInManager<IdentityUser> _sm;

    public LoginModel(SignInManager<IdentityUser> sm)
    {

      _sm = sm;

    }

    public async Task<IActionResult> OnPostAsync(string? returnUrl)
    {

      if (ModelState.IsValid)
      {

        var result = await _sm.PasswordSignInAsync(
            UserID, UserPassword, RememberMe, lockoutOnFailure: true);

        if (result.Succeeded)
        {

          return LocalRedirect(returnUrl ?? "~/");

        }

        else if (result.RequiresTwoFactor)
        {

          // to implement later 
          throw new NotImplementedException();

        }

        else if (result.IsLockedOut)
        {

          return RedirectToPage("Lockout", new { area = "auth" });

        }

        else 
        {

          // display the count of failed attempts 

          // get the user 
          IdentityUser user = await _sm.UserManager.FindByEmailAsync(UserID);

          // failed attempts 
          int fails = await _sm.UserManager.GetAccessFailedCountAsync(user);

          // total attempts 
          int total = _sm.Options.Lockout.MaxFailedAccessAttempts;

          String message =
              $"Login failed. Unsuccessful attempts {fails} of {total}";

          // add to validation summary 
          ModelState.AddModelError(string.Empty, message);

        }

      }

      return Page();

    }

  }

}

First we have the namespaces. Then we have the LoginModel class. Then we have the bind properties for UserID, UserPassword and RememberMe.

Constructor based dependency injection has been used to cache the SignInManager service.

The OnPost method executes when the user posts the login form. The PasswordSignInAsync method is used to sign the user in.

If the sign in succeeds, the user is redirected to the returnUrl.

Next, we have included the case for 2 factor authentication. This will be implemented in the future tutorials.

If the user account is locked out, then he is redirected to the Lockout page. This happens if the user consumed all his login attempts.

The last else block is reached when a login fails. First we query the failed attempts and the number of attempts he has in total. A message is constructed and added to the model error so that it appears on the login page.

Run the Project

Run the project to open the login page. Enter a registered email, but a wrong password. Click the login button - we verify that the login fails and displays the number of failed attempts along with the total number of attempts.

If try for 5 times - the account gets locked. And, after five minutes the account gets unlocked. Thanks!


This Blog Post/Article "(C# ASP.NET Core) Account Lockout Functionality in Identity" by Parveen is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.