Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Understanding, Using and Customizing ASP.NET Identity System for Authentication and Authorization

0.00/5 (No votes)
16 Jul 2014 1  
In this article we will look into ASP.NET Identity System which comes as the default authentication and authorization mechanism with ASP.NET MVC 5 internet application template. We will try to understand the ASP.NET Identity system and compare it with the ASP.NET simple membership providor and the c

Introduction

In this article we will look into ASP.NET Identity System which comes as the default authentication and authorization mechanism with ASP.NET MVC 5 internet application template. We will try to understand the ASP.NET Identity system and compare it with the ASP.NET simple membership providor and the classic ASP.NET roles and membership provider API.

Background

Let us start the discussion by looking at what authentication and authorization is. Authentication means validating users. In this step, we verify user credentials to check whether the person tying to log in is the right one or not. Authorization on the other hand is keeping track of what the current user is allowed to see and what should be hidden from him. It is more like keeping a register to what to show and what not to show to the user.

Whenever a user logs in, he will have to authenticate himself with his credentials. Once he is authenticated, he will be authorized to see resources/pages of the website. Mostly these two concepts go together.

ASP.NET supports two basic types of authentication:

  1. Windows authentication: Based on Windows accounts and access control lists (not very practical for web scenario)
  2. Forms authentication: In this mode the application will use its own username and password to authenticate the user. Once a user is authentication, an authentication cookie will be created. On subsequent requests, this cookie is checked. If it is a valid, the request is served. otherwise, the user is redirected to a login page.

To ease the process of forms authentication ASP.NET provides ASP.NET membership APIs. With the release of ASP.NET 2.0, came the Roles and membership APIs in ASP.NET framework which provided all the required boilerplate code and database schema that is needed to address the issue of authentication and authorization. Implementing authentication and authorization was just a matter of pluging in the ASP.NET membership API and the membership provider gave us the authentication and authorization functionality out of the box. To know more on this please refer: Understanding ASP.NET Roles and Membership - A Beginner's Tutorial[^]

In ASP.NET membership provider API The information in the user and role table was predefined and it cannot be customized. User profile information is also stored in the same database. It was not very easy and straight forward to take full control of the database using ASP.NET membership provider. and this some applications still found themselves a need to implement their mechanism of authentication and authorization using their own database for tracking users and their roles. Some reasons for having such a requirements could be:

  • We have an existing database and we are trying to implement an application using that.
  • The Roles and Membership functionality is overkill for our application.
  • The Roles and Membership functionality is not sufficient for our applications and we need custom data.

It was possible to using the custom authentication and authorization mechanism with ASP.NET. This technique was known as custom forms authentication mechanism. To know more on custom forms authentication, please refer:

With ASP.NET MVC 4, the simple membership provider API has been provided by Microsoft. Compared to the ASP.NET membership provider, the major benefit of the simple membership API is more simple, mature and relatively straight forward to take full control of. Another major benefit of the simple membership provider is that is persistence ignorant. Meaning it provides us a set of models to work with to achieve the authentication and authorization and uses the code first approach. It is up to the application developers to decide where and how these models should get persisted. (supported by Entity framework code first).

The simple membership provider gave a simpler database and an easy way to create custom schema for the user and roles but it had few problems. The first problem was that it was not possible to store the membership data in a non relational database. sure it was persistent ignorant but as long as the database we are choosing is a relational database. The second problem was that it doesn't work with classic ASP.NET membership provider. So if I have an enterprise application using ASP.NET membership API and i want to implement a new module using simple membership provider, it get a little tricky to get the application to work. We might have to put in some workaroundish bridges to get this to work. Finally the simple membership provider does not work with OWIN forms authentication.

To solve all the above problems, ASP.NET identity system has been created. The main idea behind having the ASP.NET identity system is to get the best of both/all worlds. ASP.NET one identity system provides all the benefits of one simple membership provider and also overcomes its limitations.

Using the code

Let us try to create a simple ASP.NET MVC5 internet application and observe the simple membership provider in action. When we create the ASP.NET MVC 5 internet application template, the project structure will look like:

Understanding the default template

The template gives a following out of the box:

  • ApplicationUser: A model which contains all the profile information for the user. We can add custom profile data for the user by adding more properties to this ApplicationUser class(Models/IdentityModels.cs).
  • ApplicationDbContext: Context class which is responsible for performing the CRUD operations on the Identity tables(Models/IdentityModels.cs).
  • RegisterViewModel: A view model to help in new user registration(Models/AccountViewModels.cs).
  • LoginViewModel: A view model to help in user login(Models/AccountViewModels.cs).
  • ManageUserViewModel: A view model to help in user profile management. By default it supports password change functionality but it can be customized as per needs(Models/AccountViewModels.cs).
public class ApplicationUser : IdentityUser
{
}
public class ApplicationDbContext : IdentityDbContext<applicationuser>
{
    public ApplicationDbContext()
        : base("DefaultConnection")
    {
    }
}                                                                  
   
public class RegisterViewModel
{
    [Required]
    [Display(Name = "User name")]
    public string UserName { get; set; }
    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }
    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}
public class LoginViewModel
{
    [Required]
    [Display(Name = "User name")]
    public string UserName { get; set; }
    [Required]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }
    [Display(Name = "Remember me?")]
    public bool RememberMe { get; set; }
}
public class ManageUserViewModel
{
    [Required]
    [DataType(DataType.Password)]
    [Display(Name = "Current password")]
    public string OldPassword { get; set; }
    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "New password")]
    public string NewPassword { get; set; }
    [DataType(DataType.Password)]
    [Display(Name = "Confirm new password")]
    [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}

Understanding the details under the hood

The next thing to notice is the AccountController. We will not be discussing the complete class here but this class contains all the CRUD operations on our user profile in such a way that it is wrapped in form of "Register", "Login", "Manage" functions. The important thing to notice is that this class uses UserManager<T> to perform all the user management activities. This class has been instantiated to use the UserStore<ApplicationUser> which uses the ApplicationUser class as the user profile class and ApplicationDbContext to perform the CRUD operations.

Now this use of UserStore can be visualized as using a façade to access a complex set of classes that deals with authentication and authorization. The most important thing to note in the controller is that the controller uses UserManager class and the UserStore is injected into the class. Which would mean that we can have custom classes(implementing IUSerStore) for all the authentication and authorization logic and they they can be injected into the Controller easily. Another important thing to notice is that the UserStore also uses the ApplicationDbContext that was injected into it, that would mean that we can have other contexts injected into it too. Which makes make the Identity system fully extensible and unit testable.

Now when we run the application, we can perform all the operations like "Register", "Login" and "Manage" using the One identity provider.

Adding custom fields in user model - the easy part

Having custom fields in the user model is fairly straightforward when we are using the ASP.NET identity system. Just add more fields to the ApplicationUser model and they will be ready to use. We will not be discussing this in details here.

Customizing everything - from User model to storage location

Now just to understand the possibility of easy customization when it comes to storage of user data, let us try to write our own version of account controller. this controller will use our custom class for authenticating the user. This custom class will use an in memory collection to validate the user credentials.

Let us start by creating a custom user model that we will be using. lets call it DummyUser.

public class DummyUser : IdentityUser
{
        
}

Next thing we need is the Context class that will save this user information. Since our plan is to save the user details in memory, lets create a simple class that will hold a list of dummy users.

public static class InMemoryUserContext
{
    public static List<dummyuser> DummyUsersList { get; set; }
    static InMemoryUserContext()
    {
        DummyUsersList = new List<dummyuser>();
    }
    public static bool Add(DummyUser user)
    {
        DummyUsersList.Add(user);
        return true;
    }        
}

Note: We will only be implementing the register and login functionality and thus this class looks very contrived. Also, the purpose of this article is to demonstrate the possibility of customization. I am compromising on many best practices by creating statics just to keep the sample code simple.

Next we need to implement a CustomUserStore which will use our DummyModel and this InMemoryUserContext.

public class CustomUserStore : IUserStore<dummyuser>, IUserPasswordStore<dummyuser>
{
    public System.Threading.Tasks.Task<dummyuser> FindByNameAsync(string userName)
    {
        DummyUser user = InMemoryUserContext.DummyUsersList.Find(item => item.UserName == userName);
        return Task.FromResult<dummyuser>(user);
    }
    public System.Threading.Tasks.Task CreateAsync(DummyUser user)
    {
        return Task.FromResult<bool>(InMemoryUserContext.Add(user));
    }
    public Task<string> GetPasswordHashAsync(DummyUser user)
    {
        return Task.FromResult<string>(user.PasswordHash.ToString());
    }
    public Task SetPasswordHashAsync(DummyUser user, string passwordHash)
    {
        return Task.FromResult<string>(user.PasswordHash = passwordHash);
    }
    #region Not implemented methods     
    public System.Threading.Tasks.Task DeleteAsync(DummyUser user)
    {
        throw new NotImplementedException();
    }
    public System.Threading.Tasks.Task<dummyuser> FindByIdAsync(string userId)
    {
        throw new NotImplementedException();
    }
    public System.Threading.Tasks.Task UpdateAsync(DummyUser user)
    {
        throw new NotImplementedException();
    }
    public Task<bool> HasPasswordAsync(DummyUser user)
    {
        throw new NotImplementedException();
    }
    public void Dispose()
    {
        throw new NotImplementedException();
    }
    #endregion
}

Note: Since we are going to implement only register and login, I have left a few functions unimplemented.

Finally, lets create the DummyAccountController which will use our custom user model and store class.

public class DummyAccountController : Controller
{
    public DummyAccountController()
        : this(new UserManager<dummyuser>(new CustomUserStore()))
    {
    }
    public DummyAccountController(UserManager<dummyuser> userManager)
    {
        UserManager = userManager;
    }
    public UserManager<dummyuser> UserManager { get; private set; }
    //
    // GET: /Account/Register
    [AllowAnonymous]
    public ActionResult Register()
    {
        return View();
    }
    //
    // POST: /Account/Register
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<actionresult> Register(RegisterViewModel model)
    {
        if (ModelState.IsValid)
        {
            var user = new DummyUser() { UserName = model.UserName };
            var result = await UserManager.CreateAsync(user, model.Password);
            if (result.Succeeded)
            {
                await SignInAsync(user, isPersistent: false);
                return RedirectToAction("Index", "Home");
            }
            else
            {
                AddErrors(result);
            }
        }
        // If we got this far, something failed, redisplay form
        return View(model);
    }
    //
    // GET: /Account/Login
    [AllowAnonymous]
    public ActionResult Login(string returnUrl)
    {
        ViewBag.ReturnUrl = returnUrl;
        return View();
    }
    //
    // POST: /Account/Login
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<actionresult> Login(LoginViewModel model, string returnUrl)
    {
        if (ModelState.IsValid)
        {
            var user = await UserManager.FindAsync(model.UserName, model.Password);
            if (user != null)
            {
                await SignInAsync(user, model.RememberMe);
                return RedirectToLocal(returnUrl);
            }
            else
            {
                ModelState.AddModelError("", "Invalid username or password.");
            }
        }
        // If we got this far, something failed, redisplay form
        return View(model);
    }
    //
    // POST: /Account/LogOff
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult LogOff()
    {
        AuthenticationManager.SignOut();
        return RedirectToAction("Index", "Home");
    }
    #region Helpers
    private IAuthenticationManager AuthenticationManager
    {
        get
        {
            return HttpContext.GetOwinContext().Authentication;
        }
    }
    private async Task SignInAsync(DummyUser user, bool isPersistent)
    {
        AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
        var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
        AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
    }
    private void AddErrors(IdentityResult result)
    {
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError("", error);
        }
    }
    private ActionResult RedirectToLocal(string returnUrl)
    {
        if (Url.IsLocalUrl(returnUrl))
        {
            return Redirect(returnUrl);
        }
        else
        {
            return RedirectToAction("Index", "Home");
        }
    }
    #endregion
}

Now we can run the application, register a new user using our DummyAccountController. the authentication and authorization mechanism of ASP.NET identity system will use our in memory user list.

Note: Please check the sample application to understand the code in details.

Point of interest

In this article we have looked at the ASP.NET identity system. We saw how the identity system is an improvement over simple membership provider. We looked at the details of the default template and customized the default template to use ASP.NET identity system as per our needs. This has been written from a beginner's perspective. I hope this has been informative.

History

  • 15 July 2014: First version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here