Table of Contents (top down ↓)
Configure the Program.cs File
First, we have to make a few additional configuration to the program.cs file (see the code below, the completed visual studio 2022 project is in the downloads). So let's begin by opening the program.cs file.
Recall from the first tutorial of this series that we have already added services for identity, and also notice that we set a RequireConfirmedAccount
option to true - which means that we will also have to integrate an email or sms based confirmation loop to a newly registered user - similar to what you see on various websites these days.
Also recall that we added options for the authentication cookie - when we specified the login and access denied paths.
Now we shall add IdentityOptions for password, lockout and user settings.
Password settings are used to specify various restrictions on a password. Lockout settings are used to set various lockout parameters, and as we can see, the user settings are used to configure various restrictions on user id or names.
// completed program.cs file // visual studio 2022 and later using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Identity; using MyRazorApp.Data; var builder = WebApplication.CreateBuilder(); builder.Services.AddDbContext<MyAuthContext>(); // add identity builder.Services .AddDefaultIdentity<IdentityUser>( options => options.SignIn.RequireConfirmedAccount = true ) .AddEntityFrameworkStores<MyAuthContext>(); // authentication builder.Services .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(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 options.Password.RequireUppercase = true; options.Password.RequireDigit = true; options.Password.RequireLowercase = true; options.Password.RequireNonAlphanumeric = true; options.Password.RequiredUniqueChars = 1; options.Password.RequiredLength = 6; // Lockout options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); options.Lockout.AllowedForNewUsers = true; // User options.User.AllowedUserNameCharacters = "abcde...xyzABC...XYZ012...789-._@+"; 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();
Video Explanation (see it happen!)
Please watch the following youtube video:
Add the Register Razor Page
Next, we have to add a register page.
Open the solution explorer, and locate the folder for Auth area, and right click the Pages folder to add a razor page called Register.
Let us examine the Register.cshtml markup file.
First we have the various directives for page, model and addtagHelper
Then we have a tag for presenting a validation summary of various model errors.
A table tag has been used to create the form. The first tr is for the user id. This element is connected to a property called UserID
.
The next tr is for the phone number. A phone number could be used for 2-step authentication later on. It could even have been omitted altogether. It is bound to a property called UserPhone
.
Then we have a tr for password, which is connected to a property called UserPassword
.
The next tr is for the confirm password. Notice that we haven't used any client side validations because they can always be added by the web designer teams.
Lastly, we have the submit button. The form shows the bare minimum html required for creating a registration form. It can be styled with any CSS - but that is out of the scope of this tutorial.
// Area->Auth->Pages->Register.cshtml @page @model MyRazorApp.Areas.Auth.Pages.RegisterModel @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers <h1>Please Register</h1> <div asp-validation-summary="All"></div> <form method="post"> <table> <tr> <td> <label asp-for="UserID"></label> </td> <td> <input asp-for="UserID" /> </td> <td> <span asp-validation-for="UserID"></span> </td> </tr> <tr> <td> <label asp-for="UserPhone"></label> </td> <td> +ISD-<input asp-for="UserPhone" /> </td> <td> <span asp-validation-for="UserPhone"></span> </td> </tr> <tr> <td> <label asp-for="UserPassword"></label> </td> <td> <input asp-for="UserPassword" /> </td> <td> <span asp-validation-for="UserPassword"></span> </td> </tr> <tr> <td> <label asp-for="ConfirmPassword"></label> </td> <td> <input asp-for="ConfirmPassword" /> </td> <td> <span asp-validation-for="ConfirmPassword"></span> </td> </tr> <tr> <td colspan="2" style="text-align:right"> <input type="submit" value="Create!" /> </td> </tr> </table> </form>
Now let us examine the programming side. Open the backing class for this markup page. This is the Register.cshtml.cs and it contains the RegisterModel class.
First we have the various namespace directives. The class has been marked as AllowAnonymous
so that the page is publically available for anyone to register on our website.
First we have a BindProperty
for UserID
, then we have a property for the phone number. We have used a regular expression for a phone number of exactly ten digits.
// Area->Auth->Pages->Register.cshtml.cs // namespaces 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 RegisterModel : PageModel { [BindProperty] [Required] [DataType(DataType.EmailAddress)] public String? UserID { get; set; } // Phone (can be optional, remove Required) [BindProperty] [Required] [DataType(DataType.PhoneNumber)] [RegularExpression(@"^\d{10}$", ErrorMessage = "10 digits")] public String? UserPhone { get; set; } // Password [BindProperty] [Required] [DataType(DataType.Password)] public String? UserPassword { get; set; } // confirm password [BindProperty] [Required] [Compare("UserPassword", ErrorMessage = "Passwords do not match")] [DataType(DataType.Password)] public string? ConfirmPassword { get; set; } // UserManager service private readonly UserManager<IdentityUser> _um; public RegisterModel(UserManager<IdentityUser> um) { _um = um; } public async Task<IActionResult> OnPostAsync() { if (!ModelState.IsValid) { return Page(); } var user = new IdentityUser() { UserName = UserID, Email = UserID, PhoneNumber = UserPhone }; var result = await _um.CreateAsync(user, UserPassword); if (!result.Succeeded) { foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } } else { String message = "SUCCESS: Registration done! In the next tutorial " + "we add code for email confirmation because in our program.cs " + "file we have options.SignIn.RequireConfirmedAccount = true"; // just using this for temporarily displaying // the above message ModelState.AddModelError(string.Empty, message); } return Page(); } } }
Similarly, we have the properties for password and confirm password.
We have used constructor based dependency injection to obtain the UserManager
service - which is one of the services that the Identity API provide us for performing user related actions like registration and login. The instance has been cached as a readonly member _um.
The OnPostAsync
method executes when the user submits and posts the form.
An IdentityUser
has been created on the basis of data entered by the user. The user is registered with a call to the CreateAsync
method of the UserManager
instance. Notice that the entire work has been done by this single call. It handles all the database communication for us.
The return value can be examined for success.
If the method doesn't succeed, the errors are added to the validation summary. These errors are such as - a user already exists.
If the method succeeds we need to add a further code for email confirmation because we have configured RequireConfirmedAccount
in the program.cs file. We shall do that in the next tutorial; but for now we have temporarily added a success message to the validation summary. This is against the rules - but we have done it so that we know that we succeeded, and this will be removed in the next tutorial.
Run the Project
The completed source course as visual studio 2022 project is given in your downloads.Run the project to open the home page of our website.
Click the link to attempt a visit to the restricted page. We are redirected to the login page. Click the link to register.
Enter any data. Enter a simple password such as 123 and click submit.
As expected, this password fails validation because we configured strict requirments in our program.cs file.
Next enter a complex password and hit the create button. The registration succeeds!
Verify the Creation
Finally, we should verify the creation by examining the users table in our SQL Server database.
Open SQL Server Object Explorer and locate the database myauthdb
Locate the table AspNetUsers and right click to view the data.
As you can see, we are able to verify that the user has been created!
In the next tutorial we shall add code for email based verification of the user. Thanks!
This Blog Post/Article "(C# ASP.NET Core) Adding a Registration Page to an Identity based Authentication Project" by Parveen is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.