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.