Friday 20 April 2018

Setup ASP.Net Core 2.0 App with Identity Service - Part 1: Authentication


This blog post  series is about setting up an ASP.Net Core 2.0 Application with Identity Service for Authentication and Authorization.

Almost all of the applications that we are developing required proper Authentication and Authorization mechanisms.  For ASP.Net Core applications , these functionalities can be handled using the ASP.NET Core Identity framework.

The advantage of using such framework is , you can put your  effort to build your application logic  while adding minimum effort to perform Authentication and Authorization. Other important aspect is , you need to follow proper standards and best practices when implementing a secured solution. When it comes to security its always better not to implement your "own security solutions" rather than looking for a standardized, battle tested solutions. 

ASP.NET Core Identity is an open-source membership system. It offers the ability to manage user accounts and implement authentication and authorization.  Following are the standard features which are offered by the framework .

1. Create user accounts
2. Password management .
3. Login and log out functionality
4. Integrating with external authentication providers, like Facebook and Google
5. Two-factor authentication

Apart from aforementioned standard features , you have the ability to customize a framework to suit your needs.

ASP.NET Core identity framework is  a claims-based security model, which was introduced in .NET version 4.5. A claim is just a property of an identity. An identity can have one or more claims associated with it. As an example , name , age , role etc.. could be identified as claims. 

In ASP.NET Core Identity framework, identity and it's list of claims are represented by the ClaimsIdentity class. The  ASP.NET Core can have multiple identities. A ClaimsPrincipal is made up of one or more ClaimsIdentities, which in turn can have one or more claims. Following diagram depicts the hierarchy and organization of claims. 



The most widely used approach for authentication involves cookies. ASP.NET Core comes with a cookie middleware system that enables cookie-based authentication. The cookie is created based on encrypted ClaimsPrincipal and inherited claims. All subsequent requests to the server will use this cookie from client. Server verifies the cookie once the request arrives to the server. 


Once it's verified, it's used to recreate the ClaimsPrincipal. The principal then gets assigned to the HTTP context user property. Now your application code has access to all the user information and claims linked to the principal. Lets move ahead and do some coding now. 

Create the demo Application 

To demonstrate examples, I am using an application that I have already created and you can find that app in this git repo.  

First and foremost, we need to add relevant nuget packages to the project that we are going to integrate the Identity service. 

Following are the packages. 

PM>Install-Package Microsoft.AspNetCore.Identity

PM>Install-Package Microsoft.AspNetCore.Identity.EntityFrameworkCore

Next, you need to add relevant dependencies in the configure services method as follows. 

services.AddIdentity<IdentityUser, IdentityRole>();

IdentityUser has properties specific to the user as en example , username, email, and a collection of user Claims.  If you need any customized properties, you need to inherit from the  IdentityUser class. The next generic parameter is Identity Role. It provides authorization information, like access rights. The default class has properties like Role Name. You can also derive from that class if you need any customized properties. 

Next we need to add the database context class to the identity service. This can be achieved by injecting the dependency as follows. 

services.AddDbContext<IdentityDbContext>(options => options.UseSqlServer(connectionString));

Next, we need to add the Migrations assembly into the Identity DBContext. The reasons behind this is IdentityDbContext isn't part of our project, and this will avoid errors during entity framework migrations.

Next we need to add token providers. These are involved in generating tokens for password reset and two-factor authentication functionality. So at this moment our application contains two DBContexts. One for the app and other one is for the Identity services. 

Toi enable the identity , there is one more step left. We need to register the cookie middleware in the Configure method ( startup class )

app.UseIdentity();

We need to do this before registering the Mvc middleware so it can redirect to the login page when Mvc detects unauthorized access.

Next we need to do the migration to create relevant tables in the DB. We can use the Nuget Package manager console for this. 

PM>Add-Migration InitIdentity -Context "IdentityDbContext" 

Note that we need to specify the context because we do have couple of them. 

Then we need to execute the data base update command as follows. 

PM>Update-Database -Context "IdentityDbContext" 

Now you can take a look at the identity related tables in you DB. 

Create the Login and Logout process with Identity 

First of all, we need to register users in our application. To perform the registration we need to add the registration View model. This model class will be use as the base class to create the registration view.

 [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} 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; }

In here I have used the annotations to perform validations and define display values. The corresponding view model should be define as well. 

 Following is the view model that we are going to use in the login process.

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

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

        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }
    }

As the next step , we need to add the Account controller to our solution. The account controller would be responsible to perform actions such as login, logout , new user registrations etc.. 

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

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

        //
        // GET: /Account/Register
        [HttpGet]
        public IActionResult Register()
        {
            return View();
        }

        //
        // POST: /Account/Register
        [HttpPost]
        public async Task<IActionResult> Register(RegisterViewModel model)
        {
            if (ModelState.IsValid)
            {
                IdentityUser user = new IdentityUser()
                {
                    Email = model.Email,
                    UserName = model.Email
                };

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

                if (result.Succeeded)
                {
                    return RedirectToAction("Login", "Account");
                }
                else
                {
                    foreach (var error in result.Errors)
                    {
                        ModelState.AddModelError(string.Empty, error.Description);
                    }
                }
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }
        //
        // GET: /Account/Login
        [HttpGet]
        public IActionResult Login()
        {
            return View();
        }

        //
        // POST: /Account/Login
        [HttpPost]
        public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
            if (ModelState.IsValid)
            {
                var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
                if (result.Succeeded)
                {
                    if (Url.IsLocalUrl(returnUrl))
                    {
                        return Redirect(returnUrl);
                    }
                    else
                    {
                        return RedirectToAction(nameof(StudentsController.Index), "Students");
                    }

                }
                else
                {
                    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
                }
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }

        //
        // POST: /Account/Logout
        [HttpPost]
        public async Task<IActionResult> Logout()
        {
            await _signInManager.SignOutAsync();
            return RedirectToAction(nameof(HomeController.Index), "Home");
        }
        [HttpGet]
        public IActionResult AccessDenied()
        {
            return View();
        }

    }

We need to load the UserManager and SignInManager classes using Asp.Net core default dependency injection container. 

In the register POST action, we need to create an object from IdentityUser class. After setting up correct properties,  we need to use the CreateAsync method in the _userManager object. 

The next step is to implement the sign in logic. The sign in manager class provides the authentication APIs we need to implement login and logout.We need to use the signInManager's PasswordSignInAsync method to login, and it accepts the email from our model as well as the password. 

The logout action method is also implemented as a POST and it is using the SignOutAsync method to log out the user from session. 

In the shared layout view, I have implemented following logic to navigate user based on the login status and the logout functionality. We need to inject the SignInManager<IdentityUser> to the view using the @inject.

You can get the fully implemented solution in this git repo

In the next part, I will walk you through how to implement the authorization process to the same project. Stay tuned !

No comments:

Post a Comment