Authentication in ASP.Net MVC 5 using Identity Libraries – Part 3

Authentication in ASP.Net MVC 5 using Identity Libraries – Part 3

My earlier post listed the steps to set up cookie-based authentication in ASP.Net MVC 5 project using Identity libraries.
There was, however, a hardcoded username and password used for the authentication logic.
I will replace the same with the new membership features in ASP.Net Identity, by validating the credentials against information stored in the SQL database.

  • Install a new Nuget package To store the user information in the database we need to install another nuget package.
    Microsoft.AspNet.Identity.EntityFramework
    This can be installed from the Package explorer using
    Install-Package Microsoft.AspNet.Identity.EntityFramework

  • This library uses Entity Framework to persist user data to SQL Server. Update the connection string in the web.config file accordingly.
    I'm using the localdb database and hence my web.config file has the following settings...

<connectionStrings>
  <add name="DefaultConnection" 
       connectionString="Data Source=(LocalDb)\mssqllocaldb;AttachDbFilename=|DataDirectory|\ASPNetMVC5Identity.mdf;Initial Catalog=ASPNetMVC5Identity;Integrated Security=True" 
       providerName="System.Data.SqlClient" />
</connectionStrings>
<entityFramework>
  <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
    <parameters>
      <parameter value="mssqllocaldb" />
    </parameters>
  </defaultConnectionFactory>
  <providers>
    <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
  </providers>
</entityFramework>
  • Next, we need to create a class to represent our users.
    ASP.Net provides a class IdentityUser which is a default implementation for the IUser interface.
    We can subclass the IdentityUser class and add any additional properties that we plan to have for the user.
    For this, I have created a new class file IdentityConfig.cs under App_Start folder.
    Add the following code to this class. I have added the property Country to the user class.
public class AppUser : IdentityUser
{
  public string Country { get; set; }
}
  • ASP.Net provides an inbuilt IdentityDbContext<TUser> to interface with Entity Framework.
    However, it is recommended that you create your Entity Framework DbContext.
    To do this, create a new IdentityModel.cs class file under the Models folder.
    Add the following code to it...
public class AppDbContext : IdentityDbContext<AppUser>
{
  public AppDbContext()
      : base("DefaultConnection")
  {

  }

  public static AppDbContext Create()
  {
      return new AppDbContext();
  }
}

Note:
The Create method has been added as an alternative to Dependency Injection(DI).
If you are using DI libraries like Ninject, you may appropriately handle this instantiation.

  • The ASP.NET Identity UserManager class is used to manage users.
    Example:
    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.

  • We'll add our own UserManager class by subclassing the UserManager<TUser> as follows...

public class AppUserManager : UserManager<AppUser>
{
    public AppUserManager(IUserStore<AppUser> store)
        : base(store)
    {
    }

    public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context)
    {
        var userManager = new AppUserManager(new UserStore<AppUser>(context.Get<AppDbContext>()));
        // Configure validation logic for usernames
        userManager.UserValidator = new UserValidator<AppUser>(userManager)
        {
            AllowOnlyAlphanumericUserNames = false,
            RequireUniqueEmail = true
        };

        // Configure validation logic for passwords
        userManager.PasswordValidator = new PasswordValidator
        {
            RequiredLength = 6,
            //RequireNonLetterOrDigit = true,
            //RequireDigit = true,
            //RequireLowercase = true,
            //RequireUppercase = true,
        };

        // Configure user lockout defaults
        userManager.UserLockoutEnabledByDefault = true;
        userManager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
        userManager.MaxFailedAccessAttemptsBeforeLockout = 5;


        return userManager;
    }
 }
  • We'll now make the UserManager<AppUser> instance accessible from AuthController. To do this add the following to the AuthController class...
public class AuthController : Controller
{
    private  AppUserManager _userManager;

    public AuthController()
    {

    }
    public AuthController(AppUserManager userManager)
    {
        UserManager = userManager;
    }

    public AppUserManager UserManager
    {
        get
        {
            return _userManager ?? HttpContext.GetOwinContext().GetUserManager<AppUserManager>();
        }
        private set
        {
            _userManager = value;
        }
     }
 // Rest of the code    
}
  • We also want to make sure that we dispose of the underlying Entity Framework DbContext at the end of the request.
    To do this we override the Dispose method in the AuthController as...
protected override void Dispose(bool disposing)
{
    if (disposing && UserManager != null)
    {
        UserManager.Dispose();
    }
    base.Dispose(disposing);
}
  • We can now replace the hardcoded authentication logic in the Login action of the AuthController is as follows...
[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)
    {
        await SignIn(user);

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

    // In case user authentication fails.
    ModelState.AddModelError("", "Invalid email or password");
    return View();
}

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

    GetAuthenticationManager().SignIn(identity);
}

private IAuthenticationManager GetAuthenticationManager()
{
    var ctx = Request.GetOwinContext();
    var authManager = ctx.Authentication;

    return authManager;
}

private string GetRedirectUrl(string returnUrl)
{
    if (string.IsNullOrEmpty(returnUrl) || !Url.IsLocalUrl(returnUrl))
    {
        return Url.Action("index", "home");
    }

    return returnUrl;
}

We try to achieve the following here...

  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).

  • With this the logic to log in the user is complete.
    We now need a way to register the user.
    We'll first create a view model to register the user, say RegisterModel.
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; }
}
  • We can now add Register actions to the AuthController as...
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,
        Email = 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.

  • Finally, we create a Register view as...

@model ASPNetMVC5Identity.Models.RegisterModel
@{
    ViewBag.Title = "Register";
}

<h2>Register</h2>

@Html.ValidationSummary(false)

@using (Html.BeginForm())
{
    @Html.EditorForModel()
    <p>
        <button type="submit">Register</button>
    </p>
}
  • With this, we can now run the application and verify the end-to-end flow.

  • For handling the claims-related information we can use our own ClaimsIdentityFactory as follows...
    This can be added in the IdentityConfig.cs file.

public class AppUserClaimsIdentityFactory : ClaimsIdentityFactory<AppUser>
{

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

        return identity;
    }
}
  • Now we need to add a reference to an instance of the above ClaimsIdentityFactory in our UserManager class is as follows...
public class AppUserManager : UserManager<AppUser>
{
    public AppUserManager(IUserStore<AppUser> store)
        : base(store)
    {
    }

    public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context)
    {
        //Earlier Code

        userManager.ClaimsIdentityFactory = new AppUserClaimsIdentityFactory();

        return userManager;
    }
}

With this we come to the end of this 3 part series.
Hope this benefits everyone.