ASP.NET Core Identity Testing Sample - Windows ASP.NET Core Hosting 2024 | Review and ComparisonWindows ASP.NET Core Hosting 2024 | Review and Comparison

Introduction

A first encounter with ASP.NET Core Identity seems intimidating. Add claims and third party login providers to the picture and most developers will opt for custom implementations for authentication and authorization.

By categorizing ASP.NET Core Identity functionality into local users, third party login providers and application roles, we reduce this complexity making ASP.NET Core Identity easier to work with.

In this article, we step create a project using ASP.NET Core Identity for authenticatin local users. We also extend the profile data for the application users without making any changes to the database table schema used by ASP.NET Core Identity.

Authentication – Prove You Are Who you Claim To Be

Authentication is the process of creating and letting users log in to an application based on their known credentials. These credentials could be their stored usernames and password or access tokens. Authentication does not determine what tasks the user can perform or what resources the user can see or access; it merely identifies and verifies who the user is. Passwords are the most common authentication method. If a user enters the correct user name and password, the system assumes the identity is valid and grants access to the application. A user, the user may be an individual or another application.

What Is ASP.NET Core Identity

ASP.NET Core Identity is a user management system that provides various authentication and authorization services for ASP.NET Core applications. Some of these services include but not limited to the following:

  • Inbuilt code for generation of database schemas for tables used for storing users, roles, claims and access tokens.
  • Includes UI templates for authentication and authorizations processes
  • Management of application authentication process for users
  • Strong user password hashing functionality
  • Generation of templates for password reset and email verification.
  • Managing two factor authentication (2FA).
  • Allows customization of any generated code
  • Opt in for external login providers like Facebook, Twitter, Google, Microsoft and more

By default, ASP.NET Core Identity uses Entity Framework (EF) Core to store user data in the database. It is however possible to swap out the EF Core and replace it with your own database integration libraries like Dapper.

Claims – Extending User Profile Data

Properties associated with an application user are referred to as claims. These claims consist of a type and a value. A claim may contain multiple values and an identity can contain multiple claims of the same type.

Aside from user name, email and password, an application may require storing of other user claims such as whether the user is active, the role of a user within an organization, display name for a user, date of last login and many more.You can extend the profile application users by appending more claims to the application users.

Show me the Code

In the article we create and work with a project named RazorAuthSample using Visual Studio Code.

Run dotnet new razor --auth Individual -o RazorAuthSample command from a terminal window to create the project. When the project has been created, change directory to RazorAuthSample folder cd RazorAuthSample and run code . to open the project using Visual Studio Code.

After the project has been created, Startup.cs file will be updated with the following changes:

  • Additional configuration will be introduced within the ConfigureServices method to add services that Identity requires. AddDefaultIdentity<IdentityUser> will add the Identity system and configure the user type. AddEntityFrameworkStores<ApplicationDbContext>() will configure Identity to store its data in Entity Framework Core.
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlite(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();
}
  • app.UseAuthentication(); middleware will be added within Configure method just before authorization. This middleware will allow authentication of incoming requests. Placement of the UseAuthentication middleware is early within the middlewares pipeline to prevent non-authenticated users from accessing content requiring authentication.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  ...

  app.UseRouting();

  app.UseAuthentication();
  app.UseAuthorization();

  app.UseEndpoints(endpoints =>
  {
      endpoints.MapRazorPages();
  });
}

The 7 ASP.NET Core Identity Tables

When using SQLite, the data migrations are applied upon creation of the project. If using a different database, you need to run dotnet ef database update to update the database with the migrations.

ASP.NET Core Identity has a total of seven tables prefixed with AspNet. The tables can be broken down into the following three groups:

  1. User and User Claims management. In this group are two (2) tables; AspNetUsers and AspNetUserClaimsAspNetUsers table stores profile data for the user while AspNetUserClaims stores claims associated with an application user. If your application is managing users and their passwords without using any external third party identity providers (like Facebook, Twitter, Google, Microsoft, etc ), these are the only tables you may require for identity management.
  2. Third Party Identity Provider Tables. If you want to delegate authentication for your application to third party providers (for example Facebook, Twitter, Google, Microsoft), then AspNetUserLogins and AspNetUserTokens are the tables you may be interested in. By using a third party identity provider, you shift responsibility and any risk of storing users’ personal data onto the third party. You can combine third party authentication with application based authentication as a fallback plan just in case the external login provider may become unavailable.
  3. Role Based Identity Tables. In role based authentication, a user must be assigned to a role and any application permissions assigned to an application role. If your application uses this model for authentication, then the following three (3) tables will be necessary; AspNetUserRolesAspNetRoles, and AspNetRoleClaims. The role-based permission model is considered to be legacy and use of claims-based permission model is encouraged.

You are not restricted to the group of tables your application should adopt for the identity model. it is possible to use one or more of the above groups for identity management. For this article, we will work only with only users and user claims.

The schema of some tables contain columns with names prefixed with Normalized keyword. Any data in these normalized columns is all set in uppercase. For example NormalizedEmail column will contain similar data to Email column but in uppercase.

Install Entity Framework (EF) Core Tool and Shared Components

Verify whether Entity Framework Core tool installed by running dotnet tool list -g. If the output of the command does not contain an entry for dotnet-ef, then install EF Core tool by running dotnet tool install --global dotnet-ef.

Install design-time shared components for Entity Framework Core tools by running dotnet add package Microsoft.EntityFrameworkCore.Design. These components are necessary for using commands like dotnet ef

Configure password, lockout and user settings .

This step is optional if you want to use the default settings. You have however to be aware that even after successfully registering a new user, the new user cannot login until the user’s email has been verified. You can disable this requirement by setting RequireConfirmedAccount to false services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = false) within the ConfigureServices method in Startup.cs.

Apart from RequireConfirmedAccount, other password default settings that you may require to modify are that passwords contain an uppercase character, lowercase character, a digit, and a non-alphanumeric character. The password length is set by default to be at least six characters long. You can modify this value to either increase or reduce the length.

You can also configure lockout, user and cookie settings to fit your environment.

public void ConfigureServices(IServiceCollection services)
{
    ...
        
    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;
    });

    services.ConfigureApplicationCookie(options =>
    {
        // Cookie settings
        options.Cookie.HttpOnly = true;
        options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

        options.LoginPath = "/Identity/Account/Login";
        options.AccessDeniedPath = "/Identity/Account/AccessDenied";
        options.SlidingExpiration = true;
    });
    
    ...
}

Scaffold Identity register, login and logout UI pages.

Scaffolding will allow you to generate code used for the identity features you may require. In this article we scaffold code for registration, login and logout features. After the code has been generated, you can then modify the code to fit your needs.

Before scaffolding, verify that ASP.NET Core scaffolder tool is installed by running dotnet tool list -g. If the output from the command does not contain an entry for dotnet-aspnet-codegenerator, then install EF Core tool by running dotnet tool install -g dotnet-aspnet-codegenerator command.

Next, add Microsoft.VisualStudio.Web.CodeGeneration.Design NuGet package to the project by running dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design command.

Even though we are using SQLite database in our project, at the time of this writing, SQLite requires Microsoft SQL Server database provider EntityFrameworkCore package to be installed. To install Microsoft.EntityFrameworkCore.SqlServer package, run dotnet add package Microsoft.EntityFrameworkCore.SqlServer command.

Scaffold register, login and logout code by running dotnet aspnet-codegenerator identity -dc RazorAuthSample.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout" command. -dc option refers to the application DbContext to use. Replace RazorAuthSample.Data.ApplicationDbContext with the location of your application’s ApplicationDbContext file. Omitting –files generator option will generate all available Identity UI pages.

Extend User Profile Data

The generated identity register user UI page contains only email and password input fields. Likewise, AspNetUsers table schema contains columns for username, email, phone number and hashed password. You can capture more details for the application user by adding these to the collection of user claims.

The process of customizing the new identity user with the new properties can be accomplished using three steps. Let’s demonstrate the steps by extending the user registration page to include first name and a check of whether the user being created is an administrator

1. Start by adding the new properties to the InputModel class. Open Areas/Identity/Pages/Account/Register.cshtml.cs page model and update InputModel class to include FirstName and IsAdmin properties.

public class InputModel
{
   ...

    [Required, Display(Name = "First Name")]
    public string FirstName { get; set; }

    [Display(Name = "Is Admin?")]
    public string IsAdmin { get; set; }
}

2. Next, update Register.cshtml page with the new input fields. Open Areas/Identity/Pages/Account/Register.cshtml page and insert new input fields for the new properties.

...
<form asp-route-returnUrl="@Model.ReturnUrl" method="post">
    ...
    <div class="form-group">
        <label asp-for="Input.FirstName"></label>
        <input asp-for="Input.FirstName" class="form-control" />
        <span asp-validation-for="Input.FirstName" class="text-danger"></span>
    </div>
    <div class="form-group form-check">
        <input type="checkbox" asp-for="Input.IsAdmin" class="form-check-input" />
        <label asp-for="Input.IsAdmin" class="form-check-label"></label>
    </div>
</form>
...

3. Open Register.cshtml.cs and update the code for creating the new user to add FirstName and IsAdmin properties to the user claims collection. Add using System.Security.Claims; namespace to the page model namespaces and insert following code to persist and append FirstName and IsAdmin properties to the user claims collection.

...
using System.Security.Claims;

namespace RazorAuthSample.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class RegisterModel : PageModel
    {
        ...
        
        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            ...
            if (ModelState.IsValid)
            {
                var user = new IdentityUser { UserName = Input.Email, Email = Input.Email };
                var result = await _userManager.CreateAsync(user, Input.Password);
                if (result.Succeeded)
                {
                    var claims = new List<Claim> 
                    {
                        new Claim("FirstName", Input.FirstName),
                        new Claim("IsAdmin", Input.IsAdmin),
                    };
                    await _userManager.AddClaimsAsync(user, claims);

                    // To add a single claim
                    // var claim = new Claim("FirstName", Input.FirstName);
                    // await _userManager.AddClaimAsync(user, claim);
                    ...
                }
            }
            
            // If we got this far, something failed, redisplay form
            return Page();
        }
     }
 }

Testing the Application

Run the application by pressing Control + F5 keys or by selecting Run > Run Without Debugging from the menu. When the application displays on a web browser, click on Register link (top right) to register a new user and type the user data. Click on Register button to persist the user data. On the Registration confirmation page, click on Click here to confirm your account link to confirm the user’s email. After email confirmation, click on Login link (top right). Type email and password used on registration and click on Log in button.

You can display any of the claims you have added anywhere on a razor page by using @User.FindFirstValue method and providing the claim type you are interested in. For example, to display the FirstName claim in place of the email address after successfull registration and login, replace @User.Identity.Name in the _LoginPartial.cshtml with @User.FindFirstValue("FirstName"). Remember to add @using System.Security.Claims namespace in the _LoginPartial.cshtml page namespaces. Alternatively, you can use LINQ to fetch a user claim @User.Claims.FirstOrDefault(x=>x.Type == "FirstName").Value. Using this latter approach does not require importation of the @using System.Security.Claims namespace on a razor page.

Conclusion

In this article, we went through the process of using ASP.NET Core Identity for user authentication by creating a sample application using Visual Studio Code. By grouping tables used with ASP.NET Core Identity, we noted it is possible to simplify the workings of ASP.NET Core Identity Using claims, we observed that we can extend the profile data for a user and utilize that data within our application without making modifications to the default tables provided by ASP.NET Core Identity.