March 19, 2014

ASP.NET Identity Stripped Bare - MVC Part 2

In my previous post I introduced ASP.NET Identity and the minimal code required to set up cookie based authentication.

In this post I’ll replace my questionable authentication logic (hard-coded username and password) to use the new membership features in ASP.NET Identity, validating credentials against information stored in a SQL database.

The code for this blog series can be found on GitHub.

Storing user information in a database

In order to store user information in a database we need to install another nuget package:

Install-Package Microsoft.AspNet.Identity.EntityFramework

As the name might suggest, this library uses Entity Framework to persist user data to SQL Server (in this example I’m using SQL LocalDB).

Create a class to represent your user

ASP.NET Identity makes it easy to store additional information about your users. All you have to do is subclass IdentityUser and add the properties you need. In this example I’m adding a custom property for the user’s country:

public class AppUser : IdentityUser
{
    public string Country { get; set; }
}

Note that the AppUser principal class I created in my previous post has since been renamed to AppUserPrincipal - ‘cause naming stuff is hard.

Create your own DbContext

Whilst you can use the built in IdentityDbContext<TUser>, the guys behind the ASP.NET Identity library recommend you create your own EF DbContext. This allows you to use the same context for other application data which makes it easier to manage things like session-per-request and database migrations.

using Microsoft.AspNet.Identity.EntityFramework;

namespace NakedIdentity.Mvc
{
    public class AppDbContext : IdentityDbContext<AppUser>
    {
        public AppDbContext()
            : base("DefaultConnection")
        {
        }
    }
}

By default, EF will create a database named DefaultConnection in your App_Data directory. If you want to override it you can just add your own connection string in web.config. Here’s mine:

<connectionStrings>
  <add name="DefaultConnection" 
       connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\NakedIdentity-Mvc.mdf;Initial Catalog=NakedIdentity-Mvc;Integrated Security=True" 
       providerName="System.Data.SqlClient" />
</connectionStrings>

Note - You should also make sure you create the App_Data directory in advance, otherwise EF will throw a wobbly.

Configuring UserManager

The ASP.NET Identity UserManager class is used to manage users e.g. registering new users, validating credentials and loading user information. It is not concerned with how user information is stored. For this it relies on a UserStore (which in our case uses Entity Framework). There are also implementations available for Azure Table Storage, RavenDB and MongoDB to name a few.

Below I use the Factory Pattern so that I can create a new instance of UserManager at the start of each request (you could achieve the same thing with the DI tool of your choice). In Startup.cs:

public class Startup
{
    public static Func<UserManager<AppUser>> UserManagerFactory { get; private set; }

    public void Configuration(IAppBuilder app)
    {           
		// this is the same as before
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/auth/login")
        });

        // configure the user manager
        UserManagerFactory = () =>
        {
            var usermanager = new UserManager<AppUser>(
                new UserStore<AppUser>(new AppDbContext()));
            // allow alphanumeric characters in username
            usermanager.UserValidator = new UserValidator<AppUser>(usermanager)
            {
                AllowOnlyAlphanumericUserNames = false
            };

            return usermanager;
        };
    }
}

The cookie based authentication configuration is the same as before. The UserManager class is generic and takes the type of user (in our case AppUser). We’re also configuring the default UserValidator to allow alphanumeric characters in the username because seriously, who has separate usernames and email addresses in 2014?!

Authentication

First we’ll make the UserManager<AppUser> instance accessible from the AuthController:

 public class AuthController : Controller
{
    private readonly UserManager<AppUser> userManager;

    public AuthController()
        : this (Startup.UserManagerFactory.Invoke())
    {
    }

    public AuthController(UserManager<AppUser> userManager)
    {
        this.userManager = userManager;
    }

    // ...

And we want to make sure we dispose the underlying Entity Framework DbContext at the end of the request:

protected override void Dispose(bool disposing)
{
    if (disposing && userManager != null)
    {
        userManager.Dispose();
    }
    base.Dispose(disposing);
}

Now let’s replace the hardcoded authentication logic to use UserManager:

[HttpPost]
public async Task<ActionResult> LogIn(LogInModel model)
{
    if (!ModelState.IsValid)
    {
        return View();
    }

    var user = await userManager.FindAsync(model.Email, model.Password);

    if (user != null)
    {
        var identity = await userManager.CreateIdentityAsync(
            user, DefaultAuthenticationTypes.ApplicationCookie);

        GetAuthenticationManager().SignIn(identity);

        return Redirect(GetRedirectUrl(model.ReturnUrl));
    }

    // user authN failed
    ModelState.AddModelError("", "Invalid email or password");
    return View();
}
  1. First we attempt to find a user with the provided credentials using userManager.FindAsync.
  2. If the user exists we create a claims identity for the user that can be passed to AuthenticationManager. This will include any custom claims that you’ve stored.
  3. Finally we sign in the user using the cookie authentication middleware SignIn(identity).

Registration

Of course being able to log in is of little use if we can’t register. First we’ll create a view model to represent the user registration.

public class RegisterModel
{
    [Required]
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

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

    [Required]
    public string Country { get; set; }
}

Then we’ll add register actions to AuthController:

[HttpGet]
public ActionResult Register()
{
    return View();
}

[HttpPost]
public async Task<ActionResult> Register(RegisterModel model)
{
    if (!ModelState.IsValid)
    {
        return View();
    }

    var user = new AppUser
    {
        UserName = model.Email,
        Country = model.Country
    };

    var result = await userManager.CreateAsync(user, model.Password);

    if (result.Succeeded)
    {
        await SignIn(user);
        return RedirectToAction("index", "home");
    }

    foreach (var error in result.Errors)
    {
        ModelState.AddModelError("", error);
    }

    return View();
}

To create the user we call userManager.CreateAsync passing our AppUser instance and the user password (the ASP.NET Identity library will take care of hashing and storing this securely).

Since we need to log the user in from both the LogIn and Register actions I’ve refactored this into a separate method:

private async Task SignIn(AppUser user)
{
    var identity = await userManager.CreateIdentityAsync(
        user, DefaultAuthenticationTypes.ApplicationCookie);
	da
    GetAuthenticationManager().SignIn(identity);
}

Finally we create a register view:

@model NakedIdentity.Mvc.ViewModels.RegisterModel
@{
    ViewBag.Title = "Register";
}

<h2>Register</h2>

@Html.ValidationSummary(false)

@using (Html.BeginForm())
{
  @Html.EditorForModel()
  <p>
    <button type="submit">Register</button>
  </p>
}

Run the application

After logging in we’ll get an error when redirecting to the home page. If you recall from my previous post, the home page expects a “Country” claim to exist on the current user so it can display a message like:

Hello Ben. How’s the weather in England?

Personally I think the claims implementation in ASP.NET Identity is a little confusing. We can add custom properties to our user class (like we did with AppUser.Country) but these aren’t actually made available as Claims. There’s little guidance on when we should store additional user information as properties and when we should use Claims. Brock Allen covers this in more detail on his blog.

In any case we need to make the Country property available as a claim. One way of doing this would be to just create the claim when we log the user in:

private async Task SignIn(AppUser user)
{
    var identity = await userManager.CreateIdentityAsync(
        user, DefaultAuthenticationTypes.ApplicationCookie);

    identity.AddClaim(new Claim(ClaimTypes.Country, user.Country));

    GetAuthenticationManager().SignIn(identity);
}

A better approach however is to create your own ClaimsIdentityFactory:

public class AppUserClaimsIdentityFactory : ClaimsIdentityFactory<AppUser>
{
    public override async Task<ClaimsIdentity> CreateAsync(
        UserManager<AppUser> manager, 
        AppUser user, 
        string authenticationType)
    {
        var identity = await base.CreateAsync(manager, user, authenticationType);
        identity.AddClaim(new Claim(ClaimTypes.Country, user.Country));

        return identity;
    }
}

To wire this up we just need to update our UserManagerFactory in Startup.cs:

UserManagerFactory = () =>
{
    //...


	// use out custom claims provider
    usermanager.ClaimsIdentityFactory = new AppUserClaimsIdentityFactory();

    return usermanager;
};

Rebuild and everything should work as expected.

Storing additional user information

Suppose we now want to store the user’s age. The first part is easy, we just add an additional property to AppUser:

public int Age { get; set; }

We’ll do the same for RegisterModel:

[Required]
public int Age { get; set; }

Then update the Register action to map the property from the incoming model:

var user = new AppUser
{
    UserName = model.Email,
    Country = model.Country,
    Age = model.Age
};

If you run the application at this point and attempt to register you’ll get an error like so:

The model backing the ‘IdentityDbContext`1’ context has changed since the database was created. Consider using Code First Migrations to update the database.

Entity Framework is smart enough to know that the AppUser class has been modified and that database changes are required.

Fortunately EF Migrations makes this easy to do. In the package manager console run the following command:

PM> Enable-Migrations -EnableAutomaticMigrations

Then:

PM> Update-Database

Run the application again and you should be able to register. If you view the AspNetUsers table in Server Explorer you should be able to see the user Age has been persisted.

© 2022 Ben Foster