Bootstrapping ASP.NET Core - Week 4

Adding ASP.NET Core Identity to an Existing Project

Most of the existing documentation for ASP.NET Identity start with selecting “individual user accounts” when creating a new ASP.NET Core project. We already have a project so we need to look at adding ASP.NET Core to an existing project.

Identity Overview

What is ASP.NET Core Identity?

ASP.NET Core Identity is the membership system for building ASP.NET Core web applications, including membership, login, and user data. - msft

There are four main components that are used to make up the ASP.NET Core Identity system that will help us understand Identity at more than an introductory level.

components

Identity

At the top level are the ASP.NET Core Identity library and services. This is the main entry point into the library and where most of the classes we will be working with directly reside. Specifically, this level contains the services we will use to manage users and perform login operations. It also contains the middleware and service collection extensions methods that we will use when wiring up Identity in our Startup class.

Entity Framework

Underneath the top level libraries, we will configure how we want to persist our data. In our case, we are going to store all our user data in SQL Server and use Entity Framework Core to work with the data. In fact, we will use the same data context class for Identity that we are using for our other domain data.

A quick look at the NuGet feed for “AspNetCore.Identity” reveals that there are several choices for persisting our Identity data in other databases like MongoDB or DocumentDb. We could also create our own if the one we wanted does not already exist.

Authentication

The third component of Identity that we are going to look at is the authentication provider. The authentication provider determines the strategy for storing and retrieving the authentication status of the request coming into our application. We are going to use cookie-based authentication but there are providers available to use other methods like JwtBearer, which would be useful if we were to open up our API to external developers.

Tokens

The last component we will look at is the cryptography provider used to generate tokens and provide data security for our application. This is the level I have the least desire to swap out for another provider anytime soon. When it comes to cryptography I like to leave it to people smarter than myself. Although, it is good to know that the library exists because it may come in useful if we need to generate our own tokens outside of identity at some point in the future.

There are also other providers that we could add to our identity model to take advantage of things like external social logins from Facebook or Google and to enable two-factor authentication but those are topics for another post.

The NuGet Packages

Now that we have a high-level idea of what makes up our ASP.NET Core Identity system we can return to our Fido solution and begin adding the NuGet packages needed to enable Identity in our project. In the package manager console, we will install the following two packages.

PM> Package-Install Microsoft.AspNetCore.Identity
PM> Package-Install Microsoft.AspNetCore.Identity.EntityFrameworkCore
Because the Entity Framework package has a dependency on AspNetCore Identity we could have gotten away with just adding the Entity Framework Core package reference.

I like being more explicit in case I get bored a couple of months from now and decide I want to move my user data store to DocuemntDB.

Application User

Before we can wire up and begin using Identity we need to define a class to represent users in our application. We will do that by creating an ApplilcationUser class in the root of our Models folder and making sure that it inherits from the IdentityUser base class provided by ASP.NET Core Identity.

  public class ApplicationUser: IdentityUser
  {
  }

The IdentityUser base class contains all the properties and methods necessary for our class to work as the user class for ASP.NET Core Identity. We could choose to not create an ApplicationUser class specific to our application and only use the provided IdentityUser but that would limit our ability to add new properties and extend our user class as our application grows. So while we don’t have any additional properties to add to our ApplicationUser class right now it is a good idea to have a place to place them down the road when we need them.

With the inheritance in place, we can walk up the inheritance hierarchy of IdentityUser to the base class that actually contains the user properties. Right off the bat we see that IdentityUser inherits from IdentityUser<String>. The generic string argument on this class indicates the data type for the user id of our user class.

I prefer to use a GUID for the user id. To support a Guid id we can change our class declaration in ApplicationUser to inherit from the generic class and pass Guid as the type argument.

    public class ApplicationUser: IdentityUser<Guid> 
    {
    }

If we peek at the class definition of IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin> we can see that the following properties will be available on our ApplicationUser class.

        //
        // Summary:
        //     Navigation property for the roles this user belongs to.
        public virtual ICollection<TUserRole> Roles { get; }
        //
        // Summary:
        //     Gets or sets the number of failed login attempts for the current user.
        public virtual int AccessFailedCount { get; set; }
        //
        // Summary:
        //     Gets or sets a flag indicating if the user could be locked out.
        public virtual bool LockoutEnabled { get; set; }
        //
        // Summary:
        //     Gets or sets the date and time, in UTC, when any user lockout ends.
        //
        // Remarks:
        //     A value in the past means the user is not locked out.
        public virtual DateTimeOffset? LockoutEnd { get; set; }
        //
        // Summary:
        //     Gets or sets a flag indicating if two factor authentication is enabled for this
        //     user.
        public virtual bool TwoFactorEnabled { get; set; }
        //
        // Summary:
        //     Gets or sets a flag indicating if a user has confirmed their telephone address.
        public virtual bool PhoneNumberConfirmed { get; set; }
        //
        // Summary:
        //     Gets or sets a telephone number for the user.
        public virtual string PhoneNumber { get; set; }
        //
        // Summary:
        //     A random value that must change whenever a user is persisted to the store
        public virtual string ConcurrencyStamp { get; set; }
        //
        // Summary:
        //     A random value that must change whenever a users credentials change (password
        //     changed, login removed)
        public virtual string SecurityStamp { get; set; }
        //
        // Summary:
        //     Gets or sets a salted and hashed representation of the password for this user.
        public virtual string PasswordHash { get; set; }
        //
        // Summary:
        //     Gets or sets a flag indicating if a user has confirmed their email address.
        public virtual bool EmailConfirmed { get; set; }
        //
        // Summary:
        //     Gets or sets the normalized email address for this user.
        public virtual string NormalizedEmail { get; set; }
        //
        // Summary:
        //     Gets or sets the email address for this user.
        public virtual string Email { get; set; }
        //
        // Summary:
        //     Gets or sets the normalized user name for this user.
        public virtual string NormalizedUserName { get; set; }
        //
        // Summary:
        //     Gets or sets the user name for this user.
        public virtual string UserName { get; set; }
        //
        // Summary:
        //     Gets or sets the primary key for this user.
        public virtual TKey Id { get; set; }
        //
        // Summary:
        //     Navigation property for the claims this user possesses.
        public virtual ICollection<TUserClaim> Claims { get; }
        //
        // Summary:
        //     Navigation property for this users login accounts.
        public virtual ICollection<TUserLogin> Logins { get; }

Apart from the generic Id the class is pretty much as we would expect it to be. The Roles, Claims and Logins properties will be more interesting when we take a deeper look at authorization and external social logins in a subsequent post.

Setup

After the packages are added and the ApplicationUser class is created we can register the identity services and wire up the identity middleware in our Startup class.

Hopefully this process is starting to sound familiar at this point.

You will notice that we are passing in our ApplicationUser class to the service registration as well as indicating that we would like to use a Guid as the datatype for our id’s.

public void ConfigureServices(IServiceCollection services)
{
    …

    services.AddIdentity<ApplicationUser, IdentityRole<Guid>>()
        .AddEntityFrameworkStores<ApplicationDataContext, Guid>()
        .AddDefaultTokenProviders();
}

We are making 3 calls when registering Identity services; AddIdentity, AddEntityFrameworkServices and AddDefaultTokenProviders. Let’s take a closer look at what each call is doing.

.AddIdentity

First is the call to AddIdentity with two generic type parameters. This defines the classes that we will be using to represent users and roles in our application. We also indicate that we want to use our custom ApplicationUser class and the provided IdentityRole class but with a Guid datatype for the id property.

.AddEntityFrameworkStores

The second call is to register Entity Framework as our data store with ASP.NET Core Identity. We want to use our existing ApplicationDataContext that we have already wired up but with some changes from what we currently have.

In the same way our ApplicationUser inherits from IdentityUser we want to have our data context inherit from IdentityDbContext so that we can take advantage of the data and behavior added by ASP.NET Core Identity. Once we have done that our data context will look like the following.

public class ApplicationDataContext: IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
{
    public ApplicationDataContext(DbContextOptions<ApplicationDataContext> options)
        : base(options)
    { }

    public DbSet<Attachment> Attachments { get; set; }

    public DbSet<Customer> Customers { get; set; }

    public DbSet<Payment> Payments { get; set; }

    public DbSet<Pet> Pets { get; set; }

    public DbSet<Walk> Walks { get; set; }
}

Notice that we had to once again provide data type information about our decision to use GUID ids instead of strings.

If we peek at the definition of IdentityDbContext and then walk up the inheritance chain we discover that the following additional collections have been added to our data context.

        //
        // Summary:
        //     Gets or sets the Microsoft.EntityFrameworkCore.DbSet`1 of Users.
        public DbSet<TUser> Users { get; set; }
        //
        // Summary:
        //     Gets or sets the Microsoft.EntityFrameworkCore.DbSet`1 of User claims.
        public DbSet<TUserClaim> UserClaims { get; set; }
        //
        // Summary:
        //     Gets or sets the Microsoft.EntityFrameworkCore.DbSet`1 of User logins.
        public DbSet<TUserLogin> UserLogins { get; set; }
        //
        // Summary:
        //     Gets or sets the Microsoft.EntityFrameworkCore.DbSet`1 of User roles.
        public DbSet<TUserRole> UserRoles { get; set; }
        //
        // Summary:
        //     Gets or sets the Microsoft.EntityFrameworkCore.DbSet`1 of User tokens.
        public DbSet<TUserToken> UserTokens { get; set; }
        //
        // Summary:
        //     Gets or sets the Microsoft.EntityFrameworkCore.DbSet`1 of roles.
        public DbSet<TRole> Roles { get; set; }
        //
        // Summary:
        //     Gets or sets the Microsoft.EntityFrameworkCore.DbSet`1 of role claims.
        public DbSet<TRoleClaim> RoleClaims { get; set; }

.AddDefaultTokenProviders

The last call that we make during our service registration is to add the default token providers. If we return to our diagram we can see that this is the call to define the cryptography strategy for any tokens we need in our application. I’m always fascinated about cryptography and curious about what is being added during this call. Since all of the libraries we are working with are Open Source we can jump over to GitHub and view the source of the AddDefaultTokenProviders to see the following calls within this method.

public virtual IdentityBuilder AddDefaultTokenProviders()
        {
            var dataProtectionProviderType = typeof(DataProtectorTokenProvider<>).MakeGenericType(UserType);
            var phoneNumberProviderType = typeof(PhoneNumberTokenProvider<>).MakeGenericType(UserType);
            var emailTokenProviderType = typeof(EmailTokenProvider<>).MakeGenericType(UserType);
            var authenticatorProviderType = typeof(AuthenticatorTokenProvider<>).MakeGenericType(UserType);

            return AddTokenProvider(TokenOptions.DefaultProvider, dataProtectionProviderType)
                .AddTokenProvider(TokenOptions.DefaultEmailProvider, emailTokenProviderType)
                .AddTokenProvider(TokenOptions.DefaultPhoneProvider, phoneNumberProviderType)
                .AddTokenProvider(TokenOptions.DefaultAuthenticatorProvider, authenticatorProviderType);
        }

The default method adds token providers for email, phone, and authentication (cookies in our case).

Middleware

The last step in our Setup class is, of course, to wire up the middleware in our Configure method.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    …     
       
    app.UseStaticFiles();
   
    app.UseIdentity();

    app.UseMvc();

    ….

}

Nothing really exciting going on here.

Create Identity Migrations

Everything is wired up and we are ready to begin using Identity in our application but there are no tables yet to support any user data in our database. We manage our database schema by creating migrations so we need to create a new migration to add our Identity tables.

If we remember back to our work with Entity Framework Core Migrations we will remember that we can use the Add-Migration command in the NuGet Package Manager Console to create a new database migration anytime we modify the ApplicationDataContext. Our situation here is no different. We added several new collections to our data context when we added the inheritance from IdentityDbContext. None of these collections were available when the last model snapshot was created so they will be picked up next time we run the Add-Migration command.

All we have to do is add and apply a new migration and all the Identity tables will be created for us.

PM> Add-Migration Identity

We can take a quick look at the migration that was generated and then update our database so we can continue working on authentication.

PM> Update-Database

The identity tables

identity_tables

Adding Authentication

We finally have Identity all setup and the database tables created but we have not created any logins yet. That makes it a good time to test out our unauthenticated responses. Just like in previous versions of ASP.NET we can add an Authorize attribute to our controllers to ensure only authenticated users can access them. We will add an Authorize attribute to our customers controller and see what happens.

    [Authorize]
    public class CustomersController: BaseApiController
    {
    …
    }

Now when we open this request in our browser we see that we are redirected the default login page url, which we haven’t implemented yet so it is handled by our catch-all route.


unauthorized_request

There are two issues here. First I’m not a big fan of the /Account/Login URL path. I’d rather have something like /login. I’m sure we can change that somewhere in the Identity configuration.

The second and more onerous issue is that when we access an endpoint in our REST API I expect to receive an HTTP 401 response if I am not authenticated and an HTTP 403 if I am authenticated but not authorized to access the endpoint. The behavior we are seeing by default is to redirect the request to the login page which will then return an HTTP Status Code of 200. That is not what we want.

Login Page URL

Let’s address the login path URL first. We should be able to figure that out pretty easily and solving that issue will likely lead us in the direction of addressing the issue with the status code.

After a quick journey through the ASP.NET Core Identity documentation, we discover that the login path is part of the Cookie Authorization provider and that we have access to the options for that provider through an overload of the the AddIdentity registration method. We can easily change the paths to the routes that we prefer and will be implementing later

 services.AddIdentity<ApplicationUser, IdentityRole<Guid>>(options =>
            {
                options.Cookies.ApplicationCookie.AccessDeniedPath = "/accessdenied";
                options.Cookies.ApplicationCookie.LoginPath = "/login";
                options.Cookies.ApplicationCookie.LoginPath = "/logout";
            })
        .AddEntityFrameworkStores<ApplicationDataContext, Guid>()
        .AddDefaultTokenProviders();

401 and 403 Status Code

Generating the 401 and 403 HTTP Status codes only for calls to our API controllers is a little more involved but is also accomplished by setting options on the Cookie Authentication provider. In this case the cookie provider exposes an events object with handlers for __ OnRedirectToAccessDenied__ and __ OnRedirectToLogin__.

Within each of those handlers we can inspect the path and if it starts with “/api” then we can return only the status code otherwise we can apply the default redirect.

services.AddIdentity<ApplicationUser, IdentityRole<Guid>>(options =>
        {
            options.Cookies.ApplicationCookie.AccessDeniedPath = "/accessdenied";
            options.Cookies.ApplicationCookie.LoginPath = "/login";
            options.Cookies.ApplicationCookie.LogoutPath = "/logApplicationCookie";

            options.Cookies.ApplicationCookie.Events = new CookieAuthenticationEvents() {
                OnRedirectToAccessDenied = context => {
                    if (context.Request.Path.StartsWithSegments("/api"))
                    {
                        context.Response.StatusCode = 403;
                        return Task.FromResult(0);
                    }

                    context.Response.Redirect(context.RedirectUri);
                    return Task.FromResult(0);

                },
                OnRedirectToLogin = context =>
                {
                    if (context.Request.Path.StartsWithSegments("/api"))
                    {
                        context.Response.StatusCode = 401;
                        return Task.FromResult(0);
                    }

                    context.Response.Redirect(context.RedirectUri);
                    return Task.FromResult(0);
                }
            };
        })
For now, we’ve implemented the handlers as inline code in the Startup class. This works but makes the Startup class more verbose than I would like. To keep things simple we will keep it here for now but will likely refactor to something cleaner down the road.

With the event handlers added we can again try to access the customers api and relieve the desired HTTP 401 response.

postman_image

Identity Services

Now that the setup is complete, the database is migrated and we are returning the responses that we want we for unauthenticated requests we can look at how to interact with ASP.NET Core Identity. We can access all the data through the new collections added to our ApplicationDataContext and while there may be times where we want to do that we would miss out on all the authentication, encryption and validation logic that Identity provides.

ASP.NET Core Identity exposes two key services that we will use to do most of our work with the Identity system. These two services are the __UserManager<ApplicationUser> and the SignInManager<ApplicationUser>.

UserManager

The user manager exists to allow interaction with the users in our database. UserManager supports activities like creating a user, adding roles, settings passwords and generating verifications tokens we can use to validate emails or phone numbers.

SignInManager

The SignInManager is the service that we will interact with when we want to log the user into the system using a username and password or even an external login provider. It takes care of setting all the necessary authentication cookies so that we don’t have to.

The Account Controller

To make use of these services we will need to create a controller to handle our login requests. For our project that means we will create a new AccountController class in our controllers folder and inject an instance of our UserManager and SignInManager services into the constructor for the controller.

public class AccountController: Controller
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;

    public AccountController( UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
    {
        _userManager = userManager;
        _signInManager = signInManager;
    }

}

Once the controller is created we will need to create two login actions. The first to handle the GET request to render the login page and the second to handle the POST to authentication and sign in the user. We will also create a LoginModel class to represent the data we need to process a user login.

LoginModel

public class LoginModel
{
    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [DataType(DataType.Password)]
    public string Password { get; set; }
}

Updated Controller


[Route("/login")]
public async Task<IActionResult> Login()
{
    return View();
}

[HttpPost("/login")]
public async Task<IActionResult> Login(LoginModel model)
{
    if (ModelState.IsValid)
    {
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, false, false);
        if (result.Succeeded)
        {
            return RedirectToAction("Index", "Home");
        }
        else
        { 
            throw new Exception("Invalid login attempt");
                    
        }
    }
    return View(model)
Obviously we do not really want to throw an exception if the login fails but returning a real error would mean I would have to handle ti on the UI and I still do not want to do UI yet.

The Login Page

There are so many front-end decisions and topics to discuss once we start creating user interface components and I still went to stick to our goal of not throwing a bunch of frameworks or topics like TagHelpers, or dependency injection of services into Razor views; both of which are part of the default starter template into the conversation. So, heads up we’re going to continue to have a really low fidelity UI for at least the rest of this post.

In the Views folder create a new subfolder called Accounts and a new .cshtml paged called Login that contains the following overly simplified login page.

<form  action="/login" method="POST">
    <h2>Login to Fido</h2>
    <div>
        <label for="email">Email</label>
        <input type="email" name="email" required autofocus />]
    </div>
    <div>
        <label for="password">Password</label>
        <input type="password" name="password" required autofocus />
    </div>
    <button type="submit">Sign in</button>
</form>

Seed Data

If we were to run the site and try to login in right now we would always get the error we are throwing because of course, we have not created any users in our database. Most samples or starter templates add a Register action to the account controller that takes in a new user and password and then creates a new user account. That is great and at some point we will definitely want to create the ability for future dog walking customers to signup and create a login on our site. But I do not want to create a whole registration process just to create our first admin user.

Instead we will create some initialization code that will run during the startup process. In the initialization code we will check for at least one admin user account and if no account exists then we will create it.

To accomplish this we will add the following SeedData class to our Data folder. This class will work with an instance of the UserManager and RoleManager services to create our default users and roles.


public class SeedData
    {
    private const string _adminRoleName = "administrator";
    private string _adminEmail = "[email protected]";
    private string _adminPassword = "Hellofido!1234";

    private string[] _defaultRoles = new string[] { _adminRoleName, "customer" };

    private readonly RoleManager<IdentityRole<Guid>> _roleManager;
    private readonly UserManager<ApplicationUser> _userManager;

    public  static async Task Run(IServiceProvider serviceProvider)
    {
        using (var serviceScope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
        {
            var instance = serviceScope.ServiceProvider.GetService<SeedData>();
            await instance.Initialize();
        }
    }

    public SeedData(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole<Guid>> roleManager)
    {
        _roleManager = roleManager;
        _userManager = userManager;
    }

    public async Task Initialize()
    {
        await EnsureRoles();
        await EnsureDefaultUser();
    }

    protected async Task EnsureRoles()
    {
        foreach (var role in _defaultRoles)
        {
            if (!await _roleManager.RoleExistsAsync(role))
            {
                await _roleManager.CreateAsync(new IdentityRole<Guid>(role));
            }
        }
    }

    protected async Task EnsureDefaultUser()
    {
        var adminUsers = await _userManager.GetUsersInRoleAsync(_adminRoleName);

        if (!adminUsers.Any())
        {
            var adminUser = new ApplicationUser()
            {
                Id = Guid.NewGuid(),
                Email = _adminEmail,
                UserName = _adminEmail
            };

            var result = await _userManager.CreateAsync(adminUser, _adminPassword);
            await _userManager.AddToRoleAsync(adminUser, _adminRoleName);
        }
    }

}

We will call SeedData at the end of Configure method in our startup class after everything else has been registered and wired up.


public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
	…
	SeedData.Run(app.ApplicationServices).Wait();
}

There are a couple of key things to notice during this process.

First within the SeedData.Run method we are creating a new ServiceScope for us to use when creating services to use during initialization. This allows us to use dependency injection when we create an instance of our SeedData class. This ensures we are getting the same services we would get in other parts of our application but we are getting instances of those services that are only used for this initialization step.

Second, are the calls to the RoleManager and UserManager services to create default roles and users for our application. The calls are pretty straightforward but they demonstrate how we use these Identity services for interacting without our identity data.

Login

Now we can actually use our amazingly ugly and naive login form. So we will fire up a browser and navigate to /login and enter the login information we created as part of our seed data.

login_form

Once we have logged we will be authenticated and redirected to the home page.

Calling a Protected API

Now that we have logged in we can execute the customers api and actually receive data and not a 401.

customer_api

Displaying the Current User

Now that we have the ability to authenticate a user we can update our home page to display some user information if the user is logged in. We can do all of this by simply modifying the Razor view in the Home folder. This is accomplished by injecting our Identity services directly into our Razor view.

@using Fido.Web.Models
@using Microsoft.AspNetCore.Identity

@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
<h1>Hello from Home Index</h1>

@if (SignInManager.IsSignedIn(User))
{
    <h3>Welcome @UserManager.GetUserName(User)</h3>
}

home_with_user

Next Steps

Authentication and authorization go a lot deeper than what we just covered. We haven’t touched on important topics such as Role or Claims based authorization and policies, password resets, two-factor authentication, phone number, and email verification or using external login providers. Some of those items, like password resets, are mostly boilerplate code and we can return to a starter template to get us started. Others, such as two-factor authentication and Claims, are probably worthy of their own post.

All that being said our next step is to finally begin creating a User Interface for our project which means I have to choose a front-end framework and get working.