Role Based Security in an ASP.NET Core Application

Posted by: Mahesh Sabnis , on 8/16/2019, in Category ASP.NET Core
Views: 6528
Abstract: ASP.NET Core provides necessary APIs to implement secure access to an application.In this tutorial, we will see how to implement Role-Base security in an ASP.NET Core 2.2 application.

Security is the most important requirement for a modern web application.

Every web application owner should ensure that all users must have secure access to the web application. Each user must be assigned appropriate credentials as well as roles. The web application must have control on user authentication and authorization.

ASP.NET Core 2.2 provides necessary APIs to implement secure access to an application.

Figure 1 gives a basic idea of how security works in ASP.NET Core applications.

 

security-architecture

Figure 1: Security implementation in ASP.NET Core applications

In this tutorial, we will see how to implement Role-Base security in ASP.NET Core 2.2 application.

Note: If you are working on ASP.NET MVC 5 and want to know about implementation of Role-Based Security in MVC applications, then visit the following links:

https://www.dotnetcurry.com/aspnet-mvc/1102/aspnet-mvc-role-based-security

https://www.dotnetcurry.com/aspnet-mvc/1268/access-action-method-multiple-user-roles-using-config

 

Importance of Role-Based Security for a Web Application

In case of Web Applications, we generally think of implementing security using UserName and Password credentials.

In this case, the user has to register himself/herself for accessing the Web Site. Hence once the user logs in, he/she can access all the resources of the application. This is a simple case of Authentication.

But if the web site owner wants to control the user access to only a specific part of the application, then each authenticated user must be assigned a Role and the web site access can be controlled based on the assigned roles. This is known as Role-Based Secure Access or also known as Authorization.

Using Authorization, each authenticated user can be restricted to have limited access of the Web Site.

In ASP.NET Core 2.2, we have the Microsoft.AspNetCore.Identity namespace. This namespace provides necessary classes to create and manage users and role. We will be using the following classes for implementing Role-Based Security:

RoleManager - used to create Roles and store it in the Persistent Store e.g. Database

IdentityRole - represents the Role object

IdentityUser - represents the User object

UserManager  - used to create a User and store it in the Persistent Store e.g. Database

The code for this article is written using VS 2017 with Update 15.9.9 and .NET Core SDK 2.2.

Step 1: Open Visual Studio 2017 and create a new ASP.NET Core project and name it as Core_RBS. Select Web Application MVC  as shown in Figure 2.

create-new-mvc-app

Figure 2: Create new ASP.NET Core 2.2 MVC application 

Click on the Change Authentication button and select Individual User Accounts as shown in Figure 3.

individual-user-accounts

Figure 3: Selecting Individual User Account 

When the option of ‘Individual User Accounts’ is selected, the project will be generated with defaults for Database connection String for Security database along with required packages for New User registration and Login process.

Step 2: Since we need to generate Database for storing User and Roles information, we will modify the ConnectionString in appsettings.json as shown in Listing 1:

"ConnectionStrings": {
    "DefaultConnection": "Server=.;Database=RBSAuthDb;Trusted_Connection=True;MultipleActiveResultSets=true"
  }

Listing 1: The Connection string in the appsettings.json

We will be creating a database of the name RBSAuthDb in the SQL Server local instance. (Note: You can change the connection string as per SQL Server instance name in your environment.).

To generate the database, we need to run EF Core migration commands, but in this case, we will be using the default migration files provided by the ASP.NET Core 2.2 project in the Data folder.

Open the Startup.cs file and check the following lines in the Configure() method of the Startup class.

......
if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
........

Listing 2: The Database Error Page

Listing 2 clearly states that if there are any database availability or connection errors, then the default database error page will be displayed. We will make use of this feature to generate Database using migrations. Run the application, the browser will show result as shown in Figure 4.

home-page-register-login-links

Figure 4: The Home page with Register and Login Links

As seen in Figure 4, the page shows both the Register and Login links. Click on the Register link. This will bring up the Register view as shown in Figure 5.

 

register-view

Figure 5: The Register View

Enter Email, Password and Confirm password and click on the Register button, a Database error will be displayed as shown in Figure 6.

 

database-error-page

Figure 6: The Database Error page

The error clearly shows that the database cannot be opened. This is because the database is not present.

Thanks to the ASP.NET Core 2.2 template which provides the Apply Migrations button. This button when clicked, will use the Database migrations provided in the project and will generate a database.

Click on the button. When the database is successfully generated based on the connection string, it will show the result as shown in Figure 7.

 

migration-result

Figure 7: The Migration result

As shown in Figure 7, “Try refreshing the page” and the result will be displayed as shown in Figure 8.

Figure 8: The User Registered and Logged-in

The user is Registered and Logged-in.

The question is how did it happen?

The answer lies in the ConfigureServices() method of the Startup class in Startup.cs

......

 services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));
......

Listing 3: The DbContext registration code

This means that the connection string is read from appsettings.json and connection is established with the database server instance. The database is successfully created.

We can verify the generated database in the SQL Server instance given in the connection string.

Figure 9 shows the database and the corresponding tables for security.

 

db-design

Figure 9: Database schema

Once again, you may wonder how did these tables appear?

The reason is that in the data folder we already have ApplicationDbContext.cs class which has the following code:

.......
public class ApplicationDbContext : IdentityDbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
    }
.......

Listing 4: ApplicationDbContext class

The ApplicationDbContext class is derived from IndentityDbContext class. This class is provided using Microsoft.AspNetCore.Identity.EntityFrameworkCore namespace.

The IndentityDbContext class is actually responsible for containing table mappings with tables like AspNetUsers, AspNetRoles, based on the  IdentityUser and IdentityRole classes from Microsoft.AspNetCore.Identity namespace.

Now the most important point is that if we compare the current feature of User creation in ASP.NET Core with earlier ASP.NET MVC 5 projects or even ASP.NET Core 2.0 projects, then we know that these projects have the AccountController class to create Users and Roles.

After running the application, when the request is made to the AccountController, it is used to respond with views to Register user, Login user, etc.

But in the current project ASP.NET Core 2.2, you will not find this class.

So then how does this work in ASP.NET Core 2.2?

It is done using Razor Library template. ASP.NET Core 2.1 (and hence .NET Core 2.1), provides the Razor Library template. We can use this template to create reusable views across projects (not in the scope of the article). You can read more about this over here.

Step 3: Although, we have been provided with ready library Views for register and login, we need to override them for Role creations and linking users with roles. To do so, right-click on the project and select Add > New Scaffolded Item as shown in Figure 10.

 

add-new-scaffolded-item

Figure 10: Add New Scaffolded Item

This will show the Add Scaffold window as shown in Figure 11.

add-identity-scaffold

Figure 11: Add Scaffold Window for Identity

Select the Identity option and click on the Add button. This will show Add Identity window. In this window, check on Override all files checkbox. This will make sure that all identity code files will be added to the project.

We need to select the Data Context class for all these Identity files so that when we create new Roles or want to make any code modification, it must know what database connection is used and where to store all the data.  Figure 12 shows the Add Identity window:

 

add-identity-options

Figure 12: Add Identity Window

Click on the Add button, the Identity pages will be added to the project in the Areas folder as shown in Figure 13.

 

identity-pages

Figure 13: Identity pages

The files for the View are Register.cshtml, Login.cshtml, etc. One breaking feature is that these view files have code-behind similar to ASP.NET WebForms.

Expand the Register.cshtm, it will show Register.cshtml.cs, the code-behind class derived from PageModel base class. This class contains public child class as the InputModel which contains properties for registering a user. 

The RegisterModel class constructor is injected with various Identity classes e.g. UserManager, SignManager, etc. This class contains public property Input of the type InputModel. This property is used to bind with Register.cshtml to accept user registration information e.g. Email, Password, ConfirmPassword, etc. The RegisterModel class contains OnGet() and OnPostAsync() methods for performing Get and Post requests respectively.

So far, we have seen what is offered by default by the ASP.NET Core 2.2 project. But the motive of this article is to explain you Role-Based-Security, so what we need here is the View for creating Roles. But that’s missing!

So the question is, how to create this view?

Adding RoleController in the application

Step 4: In the Controllers folder, add a new empty MVC Controller and name it as RoleController. Add the following code in this controller:

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Threading.Tasks;

namespace Core_RBS.Controllers
{
    public class RoleController : Controller
    {
        RoleManager<IdentityRole> roleManager;

        /// 
        /// Injecting Role Manager
        /// 
        /// 
        public RoleController(RoleManager<IdentityRole> roleManager)
        {
            this.roleManager = roleManager;
        }

        public IActionResult Index()
        {
            var roles = roleManager.Roles.ToList();
            return View(roles);
        }

        public IActionResult Create()
        {
            return View(new IdentityRole());
        }

        [HttpPost]
        public async Task<IActionResult>Create(IdentityRole role)
        {
            await roleManager.CreateAsync(role);
            return RedirectToAction("Index");
        }
    }
}

Listing 5: The RoleController

The RoleController is injected using RoleManager class. Using this class we can create and manage roles for the application. The Index action method returns all Roles, and Create action method is used to create a new role.

Step 5: Scaffold Index and Create views for the Role. Note that you need to scaffold Empty Views Without model and modify its HTML code explicitly as shown in listings 6 & 7:

Index.cshtml

@model IEnumerable<Microsoft.AspNetCore.Identity.IdentityRole>
@{
    ViewData["Title"] = "Index";
}

<h1>List of Roles</h1>
<a asp-action="Create">Create</a>
<table class="table table-striped table-bordered">
    <thead>
        <tr>
            <td>Id</td>
            <td>Name</td>
        </tr>
    </thead>
    <tbody>
        @foreach(var role in Model)
        {
        <tr>
            <td>@role.Id</td>
            <td>@role.Name</td>
        </tr>
        }
    </tbody>
</table>

Listing 6: Index.cshtml

Create.cshtml

@model Microsoft.AspNetCore.Identity.IdentityRole 

@{    
 ViewData["Title"] = "Create"; 
} 
<h1>Create Role</h1>
 <hr />
 <div class="row">    
 <div class="col-md-4">      
   <form asp-action="Create">          
   <div asp-validation-summary="ModelOnly" class="text-danger"></div>        
     <div class="form-group">             
    <label asp-for="Name" class="control-label"></label>      
           <input asp-for="Name" class="form-control" />       
          <span asp-validation-for="Name" class="text-danger"></span>      
       </div>                     
<div class="form-group">               
  <input type="submit" value="Create" class="btn btn-primary" />         
    </div>        
 </form>   
  </div> 
</div> 
<div>    
 <a asp-action="Index">Back to List</a> </div>
 @section Scripts {  
   @{await Html.RenderPartialAsync("_ValidationScriptsPartial");
} 
}

Listing 7: Create.cshtml

We will use these views for Listing and Creating roles respectively.

Step 6: To run the RoleController, we need to make sure that RoleManager is successfully injected in the RoleController's constructor. To successfully execute this dependency injection, we need to modify the ConfigureServices() method by replacing services.AddDefaultIndentity() line by the following code:

...............
 services.AddIdentity<<IdentityUser,IdentityRole>()
                .AddDefaultUI(UIFramework.Bootstrap4)
                .AddEntityFrameworkStores();
...............

Listing 8: Modifying the Identity Service in Startup class

By adding the above line in the code, we make sure that the RoleManager<IdentityRole> class instance is available for managing roles.

Step 7: Modify the _Layout.cshtml under the Shared sub-folder of the Views folder by adding link for the Role controller as shown in the following listing:

<li class="nav-item">
           <a class="nav-link text-dark" asp-area="" asp-controller="Role" asp-action="Index">Role</a>
</li>

Listing 9: The Role link in the _Layout.cshtml 

Step 8: Run the application, the Home page will be displayed in the browser. In this page click on the Role link. This will display the Index View of the RoleController. On this view click on the Create link to load Create View of the RoleController. This will display Create View as shown in the following figure:

 

create-role

Figure14: The Create View for Roles

Enter Role Name in the Name TextBox and click on the Create button, role will be created and it will be displayed in the Index View. We will create three roles as Admin, Manager and Clerk. Figure 15 shows the roles in the Index View:

 

list-of-roles

Figure 15: Index of Roles

Thus Roles are created for the application.

Step 9: We need to assign these roles while registering users for the application. We will modify Register.cshtml.cs by adding a new property Name in the InputModel class as shown in listing 10.

.......
public string Name { get; set; }
.......

Listing 10: The Name property in the InputModel class :

Modify the RegisterModel class by declaring a private member of the type RoleManager<IdentityRole> and inject the RoleManager<IdentityRole> in the constructor as shown in listing 11:

RoleManager<IdentityRole> _roleManager;

public RegisterModel(
    UserManager<IdentityUser> userManager,
    SignInManager<IdentityUser> signInManager,
    ILogger<RegisterModel> logger,
    IEmailSender emailSender, 
    RoleManager<IdentityRole> roleManager)
{
    _userManager = userManager;
    _signInManager = signInManager;
    _logger = logger;
    _emailSender = emailSender;
    _roleManager = roleManager;
}

Listing 11: The RoleManager injected in the RegisterModel class

Step 10: Modify OnGet() and OnPostAsync()  methods for returning ViewData for roles list and receiving the Role Name posted from view respectively as shown in the following listing (RED Marked).

Note: We can write the code of the OnPostAsync() method by implementing a separate method (recommended).

public void OnGet(string returnUrl = null)  
{
    // pass the Role List using ViewData
    ViewData["roles"] = _roleManager.Roles.ToList();
    ReturnUrl = returnUrl;
}

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // search role
    var role = _roleManager.FindByIdAsync(Input.Name).Result;
    if (ModelState.IsValid)
    {
        var user = new IdentityUser { UserName = Input.Email, Email = Input.Email };
        var result = await _userManager.CreateAsync(user, Input.Password);
        if (result.Succeeded)
        {
            _logger.LogInformation("User created a new account with password.");

            // code for adding user to role
            await _userManager.AddToRoleAsync(user, role.Name);
            // ends here

         .................Some code is removed from here......

            await _signInManager.SignInAsync(user, isPersistent: false);
            return LocalRedirect(returnUrl);
        }
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

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

Listing 12: The code modification for passing Roles to View and Receiving Role from View

In the above code, the OnGet() method uses ViewData to pass Roles list to Register view so that all roles can be displayed on the View using the drop-down list. In the OnPostAsync() method, the Role is searched based on the Role Id posted from the View. Then using AddToRoleAsync() method, the User is added to Role.

Step 11: Modify the Register.cshtml to use the ViewData for displaying roles in drop-down list as shown in the following listing (RED Marked)

@page
@model RegisterModel
@{
    ViewData["Title"] = "Register";
    var roles = (List<IdentityRole>)ViewData["roles"];
}

<h1>@ViewData["Title"]</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-route-returnUrl="@Model.ReturnUrl" method="post">
            <h4>Create a new account.</h4>
            <hr />
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Input.Email"></label>
                <input asp-for="Input.Email" class="form-control" />
                <span asp-validation-for="Input.Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.Password"></label>
                <input asp-for="Input.Password" class="form-control" />
                <span asp-validation-for="Input.Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.ConfirmPassword"></label>
                <input asp-for="Input.ConfirmPassword" class="form-control" />
                <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
            </div>
            <div class="form-group">                 <label asp-for="Input.Name"></label>                 <select asp-for="Input.Name" class="form-control"                         asp-items='new SelectList(roles,"Id","Name")'></select>                 <span asp-validation-for="Input.Name" class="text-danger"></span>             </div>
            <button type="submit" class="btn btn-primary">Register</button>
        </form>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

Listing 13: Modification in Register.cshtml to display all Roles


Step 12:
Run the application and click on the Register link on the Home page, it will show the Register view with roles in the drop-down list as shown in Figure 16:

 

register-view-with-roles

Figure 16: The Register View with roles

Create the following three users with roles:

ms@mypp.com –> Admin

ls@myapp.com –>  Manager

ts@myapp.com –> Clerk

Adding Role Based Authorization Policies

An important feature provided in ASP.NET Core is to define policies for authorization based on one or more roles. This feature makes it easy to configure application authorization based on combination of roles. We can use these policies to control access of controller's action methods, so that instead of applying roles on action methods for authorization, we can apply these policies to authorize action method access.

When the user is trying to access a specific controller and its action method, the user will be granted access to the action method based on its role and policy.

Step 13: Modify the ConfigureServices() method of the Startup.cs to add policies as shown in listing 14:

services.AddAuthorization(options => {
    options.AddPolicy("readonlypolicy",
        builder => builder.RequireRole("Admin", "Manager", "Clerk"));
    options.AddPolicy("writepolicy",
        builder => builder.RequireRole("Admin", "Manager"));
});

Listing 14: Authorization policies

In Listing 14, we have defined readonlypolicy and writepolicy. In these policies we have clubbed roles.We will be using these policies to define and apply authorization on controllers of the application.

Step 14: In the Models folder add a new class file, name this as Logic.cs and add the following code in it:

using System.Collections.Generic;

namespace Core_RBS.Models
{
    public class Employee
    {
        public int EmpNo { get; set; }
        public string EmpName { get; set; }
        public int Salary { get; set; }
    }

    public class Employees : List
    {
        public Employees()
        {
            Add(new Employee() {EmpNo=101,EmpName="Emp-1",Salary=1200});
            Add(new Employee() { EmpNo = 102, EmpName = "Emp-2", Salary = 2200 });
        }
    }

}

Listing 15: Logic.cs with Employee class and Employees Collection

The above code shows Employee entity class and Employees List that contains default data in the list.

Step 15: In the project, add a new folder and name it as Services. In this folder, add a new class file and name it as EmployeeService.cs and add the following code in it.

using Core_RBS.Models;
using System.Collections.Generic;
namespace Core_RBS.Services
{
    public class EmployeeService
    {
        Employees emps;

        public EmployeeService(Employees emps)
        {
            this.emps = emps;
        }
        public List Get()
        {
            return emps;
        }

        public List Create(Employee emp)
        {
            emps.Add(emp);
            return emps;
        }
    }
}

Listing 16: The Employee Service code that contains Create and Read Operations


Step 16:
Lets register the EmployeeService class and Employees class in the ASP.NET Core Services in the ConfigureServices() method as shown in listing 17

services.AddScoped();

Note: Makes sure that you need to import namespaces for Employees and EmployeeService in Startup.cs file.

Listing 17: Registering Employees and EmployeeService

Step 18: In the Controllers folder, add a new MVC Empty Controller and name it as EmployeeController and add the following code in it.

using Core_RBS.Models;
using Core_RBS.Services;
using Microsoft.AspNetCore.Mvc;

namespace Core_RBS.Controllers
{
    public class EmployeeController : Controller
    {
        EmployeeService serv;

        public EmployeeController(EmployeeService serv)
        {
            this.serv = serv;
        }
        public IActionResult Index()
        {
            var emps = serv.Get();
            return View(emps);
        }

        public IActionResult Create()
        {
            return View(new Employee());
        }
        [HttpPost]
        public IActionResult Create(Employee emp)
        {
            var emps = serv.Create(emp);
            return View("Index",emps);
        }
    }
}

Listing 18: EmployeeController

Scaffold Index and Create Views from the EmployeeController action methods and in _Layout.cshtml file of the Shared sub-folder of the Views folder, add the link for the EmployeeController as we did it for the RoleController in Step 7.

Run the application, here you will be able to create new Employees. Note I am not using database for storing Employee information, you can add new Connection string in appsettings.json and work with EF Core migrations to generate database with Employee table in it.

Step 19: To implement Role-Based security base on Role policies, modify the EmployeeController as shown in Listing 19 (RED Marked).

using Core_RBS.Models;
using Core_RBS.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Core_RBS.Controllers
{
    public class EmployeeController : Controller
    {
        EmployeeService serv;

        public EmployeeController(EmployeeService serv)
        {
            this.serv = serv;
        }

        [Authorize(Policy = "readonlypolicy")]
        public IActionResult Index()
        {
            var emps = serv.Get();
            return View(emps);
        }
        [Authorize(Policy = "writepolicy")]
        public IActionResult Create()
        {
            return View(new Employee());
        }
        [HttpPost]
        public IActionResult Create(Employee emp)
        {
            var emps = serv.Create(emp);
            return View("Index",emps);
        }
    }
}

Listing 19: EmployeeController with Authorization polices

The Authorize attribute is applied on Index method with policy as readonlypolicy and the Create method is having policy as writepolicy, as declared in Startup.cs . This means that user of roles grouped in these policies can access respective methods.

Step 20: Run the application and click on Employee link on the Home view,  this will make a call to Index action method the EmployeeController, but since this method is having Authorize attribute, the request will be redirected to Login View as shown in Figure 17:

redirect-to-login

Figure 17: Login View

Enter username and password e.g. ts@myapp.com. This is the user in the Clerk role. We have Clerk role in the readonlypolicy. This will provide an access to the Index view. On Index view for the current logged in user ts@myapp.com, click on Create New link, it will show the Access Denied view as shown in figure 18.

access-denied

Figure 18: Access Denied View

Our role based policy authentication is working correctly.

Log off as ts@myapp.com and then login as either using ms@myapp.com or ls@myapp.com, these are Admin and Manager role users. These users can access Index and Create views since they are in roles grouped in writepolicy. Thus it is possible to design the security architecture of the application using policies.

Conclusion:

In an ASP.NET Core application, the Role-Based Policies Security makes it possible to group roles for accessing various methods of controllers with authorization. 

Download the entire source code of this article from github.com/dotnetcurry/aspnet-core-role-based-security

This article was technically reviewed by Dobromir Nikolov.

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce the The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this eBook aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on .NET Standard and the upcoming C# 8.0 too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Mahesh Sabnis is a DotNetCurry author and Microsoft MVP having over 17 years of experience in IT education and development. He is a Microsoft Certified Trainer (MCT) since 2005 and has conducted various Corporate Training programs for .NET Technologies (all versions). Follow him on twitter @maheshdotnet


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!

Categories

JOIN OUR COMMUNITY

POPULAR ARTICLES

C# .NET BOOK

C# Book for Building Concepts and Interviews

Tags

JQUERY COOKBOOK

jQuery CookBook