다음을 통해 공유


ASP.NET Core MVC: Authentication And Claim Based Authorisation With ASP.NET Identity Core

Introduction

The authentication determines application identity while authorization determines what a subject may or may not do. The claim based identity is attaching the concept of claim to the identity. The claims are not what the subject can and cannot do. They are what the subject is or is not. It does the simple process of the authentication.

When an identity is created for authenticated user, it may be assigned one or more claims which are issued by trusted party. The claim is a name-value pair that represents what the subject is or is not instead of what the subject can and cannot do. The claim based authorization checks the value of the claim and allows to access the resource based upon that value.

For example, A person is indian civilians and may have a voter id card which issued by Election commission of India. The voter id has age on it. In this case, the claim name would be Age and this claim value would be an age of a person. For example, age is 18 on it which means the person has authority to cast his/her vote.

A claim can contain multiple values and an identity can contain multiple claims of the same type.

The source code of this article is available at MSDN Sample.

Implement ASP.NET Core Identity

First, create an empty ASP.NET Core project. As this project doesn’t hold default implementation of ASP.NET Core Identity, so we build an application step by step with ASP.NET Core Identity. We don’t have the default implementation of ASP.NET Core Identity so project.json file doesn't have any identity NuGet packages. Now, open the project.json file and modify it to add the dependencies and the tools, as per the code snippet is given below.

{
  "dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.1.0",
      "type": "platform"
    },
    "Microsoft.AspNetCore.Razor.Tools": {
      "version": "1.0.0-preview2-final",
      "type": "build"
    },
    "Microsoft.EntityFrameworkCore.Tools": {
      "version": "1.0.0-preview2-final",
      "type": "build"
    },
    "BundlerMinifier.Core": "2.2.306",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
    "Microsoft.AspNetCore.StaticFiles": "1.1.0",
    "Microsoft.EntityFrameworkCore.SqlServer": "1.1.0",
    "Microsoft.EntityFrameworkCore.SqlServer.Design": "1.1.0",
    "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0",
    "Microsoft.Extensions.Configuration.Json": "1.1.0",
    "Microsoft.Extensions.Logging": "1.1.0",
    "Microsoft.Extensions.Logging.Console": "1.1.0",
    "Microsoft.Extensions.Logging.Debug": "1.1.0",
    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
    "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.1.0",
    "Microsoft.AspNetCore.Authentication.Cookies": "1.1.0",
    "Microsoft.AspNetCore.Diagnostics": "1.1.0",
    "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.1.0",
    "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.1.0",
    "Microsoft.AspNetCore.Routing": "1.1.0",
    "Microsoft.AspNetCore.Mvc": "1.1.1"
  },
  
  "tools": {
    "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final",
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final",
    "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"
  },
  
  "frameworks": {
    "netcoreapp1.1": {
      "imports": [
        "dotnet5.6",
        "portable-net45+win8"
      ]
    }
  },
 
  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true
  },
  
  "runtimeOptions": {
    "configProperties": {
      "System.GC.Server": true
    }
  },
  
  "publishOptions": {
    "include": [
      "wwwroot",
      "**/*.cshtml",
      "appsettings.json",
      "web.config"
    ]
  },
  
  "scripts": {
    "prepublish": [ "bower install", "dotnet bundle"  ],
    "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
  }
}

This file includes NuGet packages for both Entity Framework Core and ASP.NET Core Identity.

Now, create custom ApplicationUser class, which inherits IdentityUser class. This class holds the additional field for the identity user. The IdentityUser class holds user basic information such as Email, UserName, Password etc. The ApplicationUser class extends this class. It is used to store the user information. The code snippet is given below for the same.

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
 
namespace ClaimApplication.Data
{
    public class  ApplicationUser: IdentityUser
    {
        public string  Name { get; set; }
    }
}

To perform the database operations, we create an IdentityDbContext class named ApplicationDbContext, as per the code snippet is given below.

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
 
namespace ClaimApplication.Data
{
    public class  ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityRole, string>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
 
        }
    }
}

This class needs to know that which type Application user and role are dealing with the Application. We passed ApplicationUser and IdentityRole as a parameter, while creating the object of ApplicationDbContext class. Here, the third parameter represents the primary key data type for both IdentityUser and IdentityRole.

The application needs to configure to implement ASP.NET Core Identity. Now, add the identity in the method Configure of the Startup class. The code snippet is given below to add the identity in the application.

app.UseIdentity();

As the concept of Dependency Injection is central to ASP.NET Core Application, we register context, identity and policy to Dependency Injection during the application start up. Thus, we register these as a Service in the ConfigureServices method of the StartUp class, as per the code snippet given below.

public void  ConfigureServices(IServiceCollection services)
       {
           services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
 
           services.AddIdentity<ApplicationUser, IdentityRole>()
               .AddEntityFrameworkStores<ApplicationDbContext>()
               .AddDefaultTokenProviders();
           services.AddMvc();
           services.AddAuthorization(options =>
           {
               options.AddPolicy("AddEditUser", policy => {
                   policy.RequireClaim("Add User", "Add User");
                   policy.RequireClaim("Edit User", "Edit User");
                   });               
               options.AddPolicy("DeleteUser", policy => policy.RequireClaim("Delete User", "Delete User"));
           });
       }

As per above code snippet, We need to build and register the policy. This takes places as part of the Authorisation service configuration. The claims requirements are policy based. We create policy which can contain one and more than one claims. The authenticate user has these claims then he/she authorises to access the resources.

In this case the AddEditUser policy checks for the presence of an “Add User” and “Edit User” claims on the current identity.

Here, the DefaultConnection is the connection string, which is defined in appsettings.json file, as per the code snippet given below.

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=DESKTOP-RG33QHE;Initial Catalog=ClaimDb;User ID=sa; Password="
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

Now, we have configured settings to create the database, so we have time to create a database, using migration. For database migration , we need to follow the following steps.

  1. Tools -> NuGet Package Manager -> Package Manager Console.
  2. Run PM> Add-Migration MyFirstMigration to scaffold a migration to create the initial set of tables for our model. If we receive an error , which states the term `add-migration' is not recognized as the name of a cmdlet, then close and reopen Visual Studio.
  3. Run PM> Update-Database to apply the new migration to the database. Since our database doesn't exist yet, it will be created for us before the migration is applied.

Application Users

This section demonstrates that how to create, edit and delete the identity users and how to assign the claims to a user. There is UserManager, which exposes the user related APIs. This creates the user in the Application and is stored in the database.

Now Claim data create which be assigned to the user. The following code snippet for some claim data.

using System.Collections.Generic;
 
namespace ClaimApplication.Data
{
    public static  class ClaimData
    {
        public static  List<string> UserClaims { get; set; } = new  List<string>
                                                            {
                                                                "Add User",
                                                                "Edit User",
                                                                "Delete User"
                                                            };
    }
}

Now, we proceed to the controller. We create a controller named UserController under the Controllers folder of the application. It has all ActionResult methods for the end user interface of operations. We create the UserManager instances. Subsequently, we inject it in the controller's constructor to get its object. The following is a partial code snippet for the UserController in which UserManager is injected, using constructor dependency Injection.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
 
namespace IdentitySampleApplication.Controllers
{
    [Authorize]
    public class  UserController : Controller
    {
        private readonly  UserManager<ApplicationUser> userManager;
 
        public UserController(UserManager<ApplicationUser> userManager)
        {
            this.userManager = userManager;
        }
    }
}

The AuthorizeAttribute attribute applied to the entire controller which means this controller access by authenticated users only.

We can notice that Controller takes UserManager as a constructor parameter. ASP.NET Core Dependency Injection will take care of passing the instance of UserManager into UserController. The controller is developed to handle the operations requests for the application identity user. Now, let's develop the user interface for the user listing, add user, edit user and delete user. Let's see each one by one.

User List View

When we click on top User menu of the application, it shows the user listing, as shown in figure 1. The user data is displayed in a tabular format and on this view, it has linked to adding a new user, edit a user and delete a user.

To pass the data from the controller to view, create named UserListViewModel view model, as per the code snippet, mentioned below. This view model is used for the user listing.

namespace ClaimApplication.Models
{
    public class  UserListViewModel
    {
        public string  Id { get; set; }
        public string  Name { get; set; }
        public string  Email { get; set; }    
    }
}

Now, we create an action method, which returns an index view with the data. The code snippet of an Index action method in UserController is mentioned below.

[HttpGet]
     public IActionResult Index()
     {
         List<UserListViewModel> model = new  List<UserListViewModel>();
         model = userManager.Users.Select(u => new  UserListViewModel
         {
             Id = u.Id,
             Name = u.Name,
             Email = u.Email
         }).ToList();
         return View(model);
     }

Now, we create an index view, as per the code snippet, mentioned below under the User folder of the views.

@model IEnumerable<UserListViewModel>
@using ClaimApplication.Models
@using ClaimApplication.Code
 
<div class="top-buffer"></div>
<div class="panel panel-primary">
    <div class="panel-heading panel-head">Users</div>
    <div class="panel-body">
        <div class="btn-group">
            <a id="createEditUserModal" data-toggle="modal" asp-action="AddUser" data-target="#modal-action-user" class="btn btn-primary">
                <i class="glyphicon glyphicon-plus"></i>  Add User
            </a>
        </div>
        <div class="top-buffer"></div>
        <table class="table table-bordered table-striped table-condensed">
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Email</th>                   
                    <th>Action</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var item in Model)
                {
                    <tr>
                        <td>@item.Name</td>
                        <td>@item.Email</td>                        
                        <td>
                            <a id="editUserModal" data-toggle="modal" asp-action="EditUser" asp-route-id="@item.Id" data-target="#modal-action-user"
                               class="btn btn-info">
                                <i class="glyphicon glyphicon-pencil"></i>  Edit
                            </a>
                            <a id="deleteUserModal" data-toggle="modal" asp-action="DeleteUser" asp-route-id="@item.Id" data-target="#modal-action-user" class="btn btn-danger">
                                <i class="glyphicon glyphicon-trash"></i>  Delete
                            </a>
                        </td>
                    </tr>
                }
            </tbody>
        </table>
    </div>
</div>
 
@Html.Partial("_Modal", new BootstrapModel { ID = "modal-action-user", AreaLabeledId = "modal-action-user-label", Size = ModalSize.Medium })
 
@section scripts
{
    <script src="~/js/user-index.js" asp-append-version="true"></script>
}

It shows all the operational forms in the Bootstrap model popup, so create the user - index.js file, as per the code snippet is given below.

(function ($) {
    function User() {
        var $this = this;
 
        function initilizeModel() {
            $("#modal-action-user").on('loaded.bs.modal', function  (e) {
 
            }).on('hidden.bs.modal', function  (e) {
                $(this).removeData('bs.modal');
            });
        }
        $this.init = function  () {
            initilizeModel();
        }
    }
    $(function () {
        var self = new User();
        self.init();
    })
}(jQuery))

When the Application runs and calls the index() action method from UserController with a HttpGet request, it gets all the users, which are listed in the UI, as shown in figure 1.

Figure 1: Application User Listing

Add User

To pass the data from UI to a controller to add a user, it uses view model named UserViewModel. The code snippet is given below for the same.

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
 
namespace ClaimApplication.Models
{
    public class  UserViewModel
    {
        public string  Id { get; set; }
        public string  UserName { get; set; }
        [DataType(DataType.Password)]
        public string  Password { get; set; }
        [Display(Name="Confirm Password")]
        [DataType(DataType.Password)]
        public string  ConfirmPassword { get; set; }
        public string  Name { get; set; }
        public string  Email { get; set; }
        [Display(Name = "User Claims")]
        public List<SelectListItem> UserClaims { get; set; }   
    }
}

The UserController has an action method, which is named as AddUser, which returns the view to add a user. The code snippet mentioned below is for same action method for both GET and Post requests.

[Authorize(Policy = "AddEditUser")]
        [HttpGet]
        public IActionResult AddUser()
        {
            UserViewModel model = new  UserViewModel();
            model.UserClaims = ClaimData.UserClaims.Select(c => new  SelectListItem
            {
                Text = c,
                Value = c
            }).ToList();
            return PartialView("_AddUser", model);
        }
 
        [HttpPost]
        public async Task<IActionResult> AddUser(UserViewModel model)
        {
            if (ModelState.IsValid)
            {
                ApplicationUser user = new  ApplicationUser
                {
                    Name = model.Name,
                    UserName = model.UserName,
                    Email = model.Email
                };
                List<SelectListItem> userClaims = model.UserClaims.Where(c => c.Selected).ToList();
                foreach (var claim in userClaims)
                {
                    user.Claims.Add(new IdentityUserClaim<string>
                    {
                        ClaimType = claim.Value,
                        ClaimValue = claim.Value
                    });
                }
 
                IdentityResult result = await userManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {
                    return RedirectToAction("Index");
                }
            }
            return View(model);
        }

As per above code snippet, AddEditUser policy applied using the Policy property on the AuthorizeAttribute attribute to specify the policy name; in this instance, only identities matching the policy will be allowed access to AddUserAction on the controller.

If we have a controller that is protected by the AuthorizeAttribute attribute, but want to allow anonymous access to particular actions we apply the AllowAnonymousAttribute attribute.

The CreateAsync asynchronous method of the UserManager class performs an operation to creates a new user in the Application. This method has new ApplicationUser as a parameter.

The GET request for the AddUser action method returns _AddUser partial view; the code snippet follows under the User folder of views.

@model UserViewModel
@using ClaimApplication.Models
 
<form asp-action="AddUser" role="form">
    @await Html.PartialAsync("_ModalHeader", new ModalHeader { Heading = "Add User" })
    <div class="modal-body form-horizontal">
        <div class="row">            
                <div class="form-group">
                    <label asp-for="Name" class="col-lg-3 col-sm-3 control-label"></label>
                    <div class="col-lg-6">
                        <input asp-for="Name" class="form-control" />
                    </div>
                </div>
                <div class="form-group">
                    <label asp-for="Email" class="col-lg-3 col-sm-3 control-label"></label>
                    <div class="col-lg-6">
                        <input asp-for="Email" class="form-control" />
                    </div>
                </div>
                <div class="form-group">
                    <label asp-for="UserClaims" class="col-lg-3 col-sm-3 control-label"></label>
                    <div class="col-lg-9">
                        @for(int i =0; i< Model.UserClaims.Count;i++)
                        {
                            <div class="col-lg-4">
                                <input type="checkbox" asp-for="@Model.UserClaims[i].Selected" />
                                <label asp-for="@Model.UserClaims[i].Selected">@Model.UserClaims[i].Text</label>
                                <input type="hidden" asp-for="@Model.UserClaims[i].Value" />
                                <input type="hidden" asp-for="@Model.UserClaims[i].Text" />
                            </div>
                        }
                    </div>
                </div>                
                    <div class="form-group">
                        <label asp-for="UserName" class="col-lg-3 col-sm-3 control-label"></label>
                        <div class="col-lg-6">
                            <input asp-for="UserName" class="form-control" />
                        </div>
                    </div>
                    <div class="form-group">
                        <label asp-for="Password" class="col-lg-3 col-sm-3 control-label"></label>
                        <div class="col-lg-6">
                            <input asp-for="Password" class="form-control" />
                        </div>
                    </div>
                    <div class="form-group">
                        <label asp-for="ConfirmPassword" class="col-lg-3 col-sm-3 control-label"></label>
                        <div class="col-lg-6">
                            <input asp-for="ConfirmPassword" class="form-control" />
                        </div>
                    </div>
                </div>
            </div>        
        @await Html.PartialAsync("_ModalFooter", new ModalFooter { })
</form>

When the application runs and we click on the Add User button, it makes a GET request for the AddUser() action; add a user screen, as shown in Figure 2.

Figure 2: Add User

Edit User

To pass the data from UI to the controller to edit a user, use view model named EditUserViewModel. The following code snippet for EditUserViewModel.

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
 
namespace ClaimApplication.Models
{
    public class  EditUserViewModel
    {
        public string  Id { get; set; }
        public string  Name { get; set; }
        public string  Email { get; set; }      
        [Display(Name = "User Claims")]
        public List<SelectListItem> UserClaims { get; set; }
    }
}

The UserController has an action method named EditUser, which returns the view to edit a user. The code snippet mentioned below is for the same action method for both GET and Post requests.

[Authorize(Policy = "AddEditUser")]
        [HttpGet]
        public async Task<IActionResult> EditUser(string id)
        {
            EditUserViewModel model = new  EditUserViewModel();
 
            if (!String.IsNullOrEmpty(id))
            {
                ApplicationUser applicationUser = await userManager.FindByIdAsync(id);
                if (applicationUser != null)
                {
                    model.Name = applicationUser.Name;
                    model.Email = applicationUser.Email;
                    var claims = await userManager.GetClaimsAsync(applicationUser);
                    model.UserClaims = ClaimData.UserClaims.Select(c => new  SelectListItem
                    {
                        Text = c,
                        Value = c,
                        Selected = claims.Any(x => x.Value == c)
                    }).ToList();
                }
                else
                {
                    model.UserClaims = ClaimData.UserClaims.Select(c => new  SelectListItem
                    {
                        Text = c,
                        Value = c
                    }).ToList();
                }
 
            }
            return PartialView("_EditUser", model);
        }
 
        [HttpPost]
        public async Task<IActionResult> EditUser(string id, EditUserViewModel model)
        {
            if (ModelState.IsValid)
            {
                ApplicationUser applicationUser = await userManager.FindByIdAsync(id);
                if (applicationUser != null)
                {
                    applicationUser.Name = model.Name;
                    applicationUser.Email = model.Email;
                    var claims = await userManager.GetClaimsAsync(applicationUser);
                    List<SelectListItem> userClaims = model.UserClaims.Where(c => c.Selected && claims.Any(u => u.Value != c.Value)).ToList();
                    foreach (var claim in userClaims)
                    {
                        applicationUser.Claims.Add(new IdentityUserClaim<string>
                        {
                            ClaimType = claim.Value,
                            ClaimValue = claim.Value
                        });
                    }
                    IdentityResult result = await userManager.UpdateAsync(applicationUser);
                    List<Claim> userRemoveClaims = claims.Where(c => model.UserClaims.Any(u => u.Value == c.Value && !u.Selected)).ToList();
                    foreach (Claim claim in userRemoveClaims)
                    {
                        await userManager.RemoveClaimAsync(applicationUser, claim);
                    }
                    if (result.Succeeded)
                    {
                        return RedirectToAction("Index");
                    }
                }
            }
            return PartialView("_EditUser", model);
        }

There are four asynchronous methods, which are used for the UserManager class, which performs an action, as shown below.

  • FindByIdAsync: This method has an Application user Id as a parameter and returns already existing user, which is based on the input.
  • GetClaimsAsync: This method asynchronously returns the claims for a user.
  • UpdateAsync: This method has an existing ApplicationUser as a parameter and updates that user in the Application.
  • RemoveClaimAsync: This method has two parameters, where one is an existing Application user and another is assigned claim. This method asynchronously removes a claim from a user.

The GET request for the EditUser action method returns _EditUser partial view; the code snippet follows under the User folder of views.

@model EditUserViewModel
@using ClaimApplication.Models
 
<form asp-action="EditUser" role="form">
    @await Html.PartialAsync("_ModalHeader", new ModalHeader { Heading = "Edit User" })
    <div class="modal-body form-horizontal">
        <div class="row">            
            <div class="form-group">
                <label asp-for="Name" class="col-lg-3 col-sm-3 control-label"></label>
                <div class="col-lg-6">
                    <input asp-for="Name" class="form-control" />
                </div>
            </div>            
            <div class="form-group">
                <label asp-for="Email" class="col-lg-3 col-sm-3 control-label"></label>
                <div class="col-lg-6">
                    <input asp-for="Email" class="form-control" />
                </div>
            </div>
 
            <div class="form-group">
                <label asp-for="UserClaims" class="col-lg-3 col-sm-3 control-label"></label>
                <div class="col-lg-9">
                    @for (int i = 0; i < Model.UserClaims.Count; i++)
                    {
                        <div class="col-lg-4">
                            <input type="checkbox" asp-for="@Model.UserClaims[i].Selected" />
                            <label asp-for="@Model.UserClaims[i].Selected">@Model.UserClaims[i].Text</label>
                            <input type="hidden" asp-for="@Model.UserClaims[i].Value" />
                            <input type="hidden" asp-for="@Model.UserClaims[i].Text" />
                        </div>
                    }
                </div>
               </div>
            </div>
    </div>
    @await Html.PartialAsync("_ModalFooter", new ModalFooter { })
</form>

When the Application runs and you click on the Edit button in the User listing, it makes a GET request for the EditUser() action, followed by editing the user screen, which is shown in figure 3.

Figure 3: Edit User

Delete User

The UserController has an action method named DeleteUser, which returns the view to delete a user. The code snippet mentioned below is for the same action method for both GET and Post requests.

[Authorize(Policy = "DeleteUser")]
       [HttpGet]
       public async Task<IActionResult> DeleteUser(string id)
       {
           string name = string.Empty;
           if (!String.IsNullOrEmpty(id))
           {
               ApplicationUser applicationUser = await userManager.FindByIdAsync(id);
               if (applicationUser != null)
               {
                   name = applicationUser.Name;
               }
           }
           return PartialView("_DeleteUser", name);
       }
 
       [HttpPost]
       public async Task<IActionResult> DeleteUser(string id, IFormCollection form)
       {
           if (!String.IsNullOrEmpty(id))
           {
               ApplicationUser applicationUser = await userManager.FindByIdAsync(id);
               if (applicationUser != null)
               {
                   IdentityResult result = await userManager.DeleteAsync(applicationUser);
                   if (result.Succeeded)
                   {
                       return RedirectToAction("Index");
                   }
               }
           }
           return View();
       }

Here, DeleteAsync method of UserManager is used, which takes an existing Application user as an input parameter. It deletes an existing Application user.

The AuthoriseAttribute attribute with policy name “DeleteUser” represents that it be accessed by only those user who has application claims for this policy.

The GET request for the DeleteUser action method returns _DeleteUser partial View. The code snippet mentioned below is under the User folder of Views

@model string
@using ClaimApplication.Models
 
<form asp-action="DeleteUser" role="form">
    @Html.Partial("_ModalHeader", new ModalHeader { Heading = "Delete User" })
 
    <div class="modal-body form-horizontal">
        Are you want to delete @Model?
    </div>
    @Html.Partial("_ModalFooter", new ModalFooter { SubmitButtonText = "Delete" })
</form>

When the Application runs and a user clicks on the "Delete" button in the user listing, it makes a GET request for the DeleteUser() action, then the delete user screen is shown below.

Figure 4: Delete User

Authentication and Authorisation

This section demonstrates the login and logout functionality of the application. As the Application users already exist in the system, to implement login and logout functionality, create AccountController under the Controllers folder. This controller holds both login and logout action methods. We create a SignInManager instance. Now, we inject it in the controller's constructor to get its object. The following is a partial code snippet for the AccountController in which SignInManager is injected, using constructor Dependency Injection.

using ClaimApplication.Data;
using ClaimApplication.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
 
 
namespace ClaimApplication.Controllers
{
    public class  AccountController : Controller
    {
        private readonly  SignInManager<ApplicationUser> signInManager;
 
        public AccountController(SignInManager<ApplicationUser> signInManager)
        {
            this.signInManager = signInManager;
        }
    }
}

The SignInManager manages the sign operations for users.

Login

To pass the data from UI to a controller to login an application user, it uses view model named LoginViewModel. The code snippet is given below for the same.

using System.ComponentModel.DataAnnotations;
 
namespace ClaimApplication.Models
{
    public class  LoginViewModel
    {
        [Required]
        public string  UserName { get; set; }
        [Required]
        [DataType(DataType.Password)]
        public string  Password { get; set; }
        [Display(Name = "Remember me?")]
        public bool  RememberMe { get; set; }
    }
}

The AccountController has two action methods, where one is for GET request named Login, which returns the view to login and another has the same name. It also handles POST request to login an Application user. The code snippet mentioned below is for same action method for both GET and Post requests.

public IActionResult Login(string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
            return View();
        }
 
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginViewModel model, string  returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
            if (ModelState.IsValid)
            {                
                var result = await signInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, lockoutOnFailure: false);
                if (result.Succeeded)
                {                   
                    return RedirectToLocal(returnUrl);
                }               
                else
                {
                    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
                    return View(model);
                }
            }           
            return View(model);
        }
        private IActionResult RedirectToLocal(string returnUrl)
        {
            if (Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction(nameof(HomeController.Index),  "Home");
            }
        }

Here the Login action method has a parameter named returnUrl, which represents that a user is redirected to a page after login. Suppose an end user is not authenticated and he tries to access the internal page via URL, then this internal page URL is stored in this parameter and the user is redirected to the login screen. Afterwards, the user authenticates from the login screen, followed by redirecting on that URL page rather than a regular redirection.

The SignInManager class exposes API methods, which is used to manage sign in operations. There is an asynchronous method, which is PasswordSignInAsync. This method takes the username and password of a user as inputs and checks its validity and issues the application cookie, if they are correct.

The GET request for the Login action method returns Login view and the code snippet follows under the Account folder of views.

@model LoginViewModel
@using ClaimApplication.Models
 
<div class="row">
    <div class="col-md-3"></div>
    <div class="col-md-6">
        <div class="top-buffer"></div>
        <div class="panel panel-primary">
            <div class="panel-heading">Login</div>
            <div class="panel-body">
                <section>
                    <form asp-controller="Account" asp-action="Login" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal">
                        <h4>Use a local account to log in.</h4>
                        <hr />
                        <div asp-validation-summary="All" class="text-danger"></div>
                        <div class="form-group">
                            <label asp-for="UserName" class="col-md-2 control-label"></label>
                            <div class="col-md-10">
                                <input asp-for="UserName" class="form-control" />
                                <span asp-validation-for="UserName" class="text-danger"></span>
                            </div>
                        </div>
                        <div class="form-group">
                            <label asp-for="Password" class="col-md-2 control-label"></label>
                            <div class="col-md-10">
                                <input asp-for="Password" class="form-control" />
                                <span asp-validation-for="Password" class="text-danger"></span>
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-md-offset-2 col-md-10">
                                <div class="checkbox">
                                    <label asp-for="RememberMe">
                                        <input asp-for="RememberMe" />
                                        @Html.DisplayNameFor(m => m.RememberMe)
                                    </label>
                                </div>
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-md-offset-2 col-md-10">
                                <button type="submit" class="btn btn-primary">Log in</button>
                            </div>
                        </div>
                    </form>
                </section>
            </div>
        </div>
    </div>
    <div class="col-md-3"></div>
    </div>

When the Application runs and you click on the LogIn button, it makes a GET request for the Login() action and show the login screen, as shown in Figure 5.

Figure 5: Login

The Application has a HomeController, which is accessed after successful authentication. It holds an action method named Index, which returns a view by authenticating the username.

using ClaimApplication.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
 
namespace ClaimApplication.Controllers
{
    public class  HomeController : Controller
    {
        private readonly  UserManager<ApplicationUser> userManager;
 
        public HomeController(UserManager<ApplicationUser> userManager)
        {
            this.userManager = userManager;
        }
        [Authorize]
        public IActionResult Index()
        {
            string userName = userManager.GetUserName(User);
            return View("Index", userName);
        }
    }
}

Here, Authorize attribute is used on the controller level, which means that this controller is accessed by only authenticate users. The action method has also used Authorize attribute, which represents that authenticated user can access this action method.

The GetUserName method of UserManager returns the authenticate user’s username, which is based on the user.

The authorized GET request for the Index action method returns an Index view. The code snippet follows under the Home folder of views.

@model string
<h1> Welcome @Model</h1>

The Application has a partial view, which is used to show following details on the top header. If the user is not authenticated, it shows Log In button on top. If the user is authenticated, it shows the username and Log Off button.

@using Microsoft.AspNetCore.Identity
@using ClaimApplication.Models
@using ClaimApplication.Data
 
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
 
@if (SignInManager.IsSignedIn(User))
{
    <form asp-area="" asp-controller="Account" asp-action="SignOff" method="post" id="logoutForm" class="navbar-right">
        <ul class="nav navbar-nav navbar-right">
            <li>
                <a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage">Hello @UserManager.GetUserName(User)!</a>
            </li>
            <li>
                <button type="submit" class="btn btn-link navbar-btn navbar-link">Log off</button>
            </li>
        </ul>
    </form>
}
else
{
    <ul class="nav navbar-nav navbar-right">      
        <li><a asp-area="" asp-controller="Account" asp-action="Login">Log in</a></li>
    </ul>
}

As per the code snippet is given above, the view has Injected Dependency on its level and created instances of both SignInManager and UserManager. The IsSignedIn method of SignInManager class checks whether a user login or not in the application.

Now, we login with the valid credentials of a user, followed by redirecting on the Index action method of HomeController. The Index view shows as shown in figure 6.

Figure 6: Welcome screen

Claim Based Authorisation

As we created two policies based on the claims in this application. The AddEditUser policy contains two claims one is “Add User” and another is “Edit User”. The DeleteUser policy contains “Delete User” claim. We used these policies on the action method of the UserController controller. If a user doesn’t have one of the claims which include in AddEditUser policy then the authenticated user doesn’t authorize to access that action method on which policy has been implemented.

Now, we login with valid credentials of a user while that user doesn’t have “Delete User” claim as well then it redirects on the AccessDenied action method of AccountController controller.

public IActionResult AccessDenied()
        {
            return View();
        }       

The AccessDenied action method returns the AccessDenied view, which has the code snippet given below.

<div class="row">
    <div class="col-md-3"></div>
    <div class="col-md-6">
        <div class="top-buffer"></div>
        <div class="panel panel-danger">
            <div class="panel-heading">Access Denied</div>
            <div class="panel-body">
                <section>
                    <h1 class="text-danger">403 ! Access Denied</h1>
                    <br />
                    <a href="javascript:void(0)" onClick="backAway()" class="btn btn-success">Back</a>
                </section>
            </div>
        </div>
    </div>
    <div class="col-md-3"></div>
</div>
 
@section scripts
{
    <script>
        function backAway() {
            if (history.length === 1) {
                window.location = "http://localhost:50841/"
            } else {
                history.back();
            }
        }
    </script>
}

Now, run the Application and login with the valid credentials. Its authentication is successful. This authenticates the user, who doesn’t have ‘Delete User’ claim. We click on Delete button on user listing and it is being redirected on access denied because the user doesn’t have ‘Delete User’ claim. The screen given below shows in case of access denied.

Figure 7: Unauthorized screen

As the application action methods call via ajax request so the unauthorized user is being redirected on access denied using following code snippet on _Layout.cshtml.

<script type="text/javascript">
      $(function () {
          $(document).ajaxError(function (xhr, props) {               
              if (props.status == 403) {
                 window.location = "http://localhost:49957/Account/AccessDenied";
              } else  if (props.status == 401) {
                  window.location = "http://localhost:49957/Account/Login";
              }
          });
  });
  </script>

Logout

Now, create an action method for Logout in AccountController, as per the code snippet is given below.

[HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> SignOff()
        {
            await signInManager.SignOutAsync();           
            return RedirectToAction("Login");
        }

This SignOutAsync clears the user claims, which are stored in cookies. This action method calls, when we click on Logout button, which is placed at the top.

Downloads

You can download the complete source code from the MSDN Sample, using the links, mentioned below.

  1. Rating Star Application in ASP.NET Core
  2. CRUD Operations in ASP.NET Core and Entity Framework Core
  3. Repository Pattern In ASP.NET Core
  4. Generic Repository Pattern in ASP.NET Core
  5. Onion Architecture In ASP.NET Core MVC
  6. ASP.NET Core MVC: Authentication and Role Based Authorisation with Identity
  7. ASP.NET Core MVC: Authentication and Claim Based authorization with Identity

See Also

It's recommended to read more articles related to ASP.NET Core.

  1. ASP.NET Core: Overview
  2. ASP.NET Core With Visual Studio 2017 RC
  3. ASP.NET Core Entity Framework Core Code First: CRUD Operations
  4. Repository Pattern In ASP.NET Core
  5. ASP.NET Core: Generic Repository Pattern
  6. Onion Architecture In ASP.NET Core MVC
  7. ASP.NET Core MVC: Authentication And Role Based Authorization With ASP.NET Core Identity
  8. ASP.NET Core : Overview Of Dependency Injection
  9. ASP.NET Core: In-Memory Caching

Conclusion

This article introduced the authentication and authorization in ASP.NET Core with ASP.NET Core Identity, using Entity Framework Core with the "code first" development approach. It explained the claim based authorization in the Application. We used Bootstrap, CSS and JavaScript for the user interface design in this Application.