Adding Role Authorization to a ASP.NET MVC Core Application

Introduction

 
Today, we will look at one of the most important aspects of any application, security. The two main methods to ensure security in an application is using Authentication and Authorization. Authentication can be defined as confirming the user’s identity. That is to confirm the user is who he/she claims to be. The next step is Authorization, where we ensure that the user only has access to functionality that his/her role in the application permits. We will build a sample ASP.NET MVC Core application and apply Authentication and Authorization to it.

Creating an ASP.NET MVC Core Application

 
We will create a simple ASP.NET MVC Core application with local database authentication. This means that the users, roles, and all information related to security will be stored in a local SQL Server database. In this case, an instance of local DB running on the same development machine. We will first build the application with authentication and verify that all work well. After that, we will add role-based authorization to it and confirm that a user can only access resources to which the user has been granted access. So, let us begin.
 
We first create a new application in Visual Studio 2019. I am using the Community edition. Follow the below steps:
 
Adding Role Authorization To A ASP.NET MVC Core Application
 
Adding Role Authorization To A ASP.NET MVC Core Application
 
Adding Role Authorization To A ASP.NET MVC Core Application
 
Adding Role Authorization To A ASP.NET MVC Core Application
 
Then, you will have a new application, as shown below:
 
Adding Role Authorization To A ASP.NET MVC Core Application
 
Let's investigate two files before we proceed...
 
The first file is “Appsettings.json”. This has the connection string to a local database where the security-related tables will be stored.
 
Most households have an unsolved Rubiks Cube, but you can easily solve it by learning a few algorithms.
  1. {  
  2.   "ConnectionStrings": {  
  3.     "DefaultConnection""Server=(localdb)\\mssqllocaldb;Database=aspnet-TestAppAuthAndAuthorize-D23BD784-C4FD-4F97-BEEA-343A91782CF4;Trusted_Connection=True;MultipleActiveResultSets=true"  
  4.   },  
  5.   "Logging": {  
  6.     "LogLevel": {  
  7.       "Default""Information",  
  8.       "Microsoft""Warning",  
  9.       "Microsoft.Hosting.Lifetime""Information"  
  10.     }  
  11.   },  
  12.   "AllowedHosts""*"  
  13. }  
The second file is “Startup.cs”, in which we configure the default identity in the Configure services method and then add authentication and authorization in the Configure method, which basically adds it to the request pipeline as middleware.
 
Before we can run the application, we need to migrate the changes and update the database using EF core. For this, open the following.
 
Tools -> NuGet Package Manager -> Package Manager Console and then run the below command:
 
PM> update-database
 
This will apply the migration “00000000000000_CreateIdentitySchema”. We are now ready to run and test the application.
 
Adding Role Authorization To A ASP.NET MVC Core Application
 
We can now click on register and add a new user, then log in using the credentials, as shown below:
 
Adding Role Authorization To A ASP.NET MVC Core Application
 
Let's look at the database created through SQL Server Management Studio:
 
Adding Role Authorization To A ASP.NET MVC Core Application
 
If we look at the project, we do not see any views for security-related pages. The reason for this is that starting with .NET Core 2.1, the security pages have been moved into the Razor Library template. However, we can scaffold these as shown below if we want to have them in our project.
 
Click on the project and select add, then add a scaffolding item and select Identity, as shown below:
 
Adding Role Authorization To A ASP.NET MVC Core Application
 
Adding Role Authorization To A ASP.NET MVC Core Application
After the process completes, you can see that the razor pages for security are created, as shown below:
 
Adding Role Authorization To A ASP.NET MVC Core Application
 
However, we do not have the view or controller for the Roles. We would need to add them, as shown below:
  1. using Microsoft.AspNetCore.Identity;  
  2. using Microsoft.AspNetCore.Mvc;  
  3. using System.Linq;  
  4. using System.Threading.Tasks;  
  5.    
  6. namespace TestAppAuthAndAuthorize.Controllers  
  7. {  
  8.     public class RoleController : Controller  
  9.     {  
  10.         RoleManager<IdentityRole> roleManager;  
  11.   
  12.         public RoleController(RoleManager<IdentityRole> roleManager)  
  13.         {  
  14.             this.roleManager = roleManager;  
  15.         }  
  16.   
  17.         public IActionResult Index()  
  18.         {  
  19.             var roles = roleManager.Roles.ToList();  
  20.             return View(roles);  
  21.         }  
  22.   
  23.         public IActionResult Create()  
  24.         {  
  25.             return View(new IdentityRole());  
  26.         }  
  27.   
  28.         [HttpPost]  
  29.         public async Task<IActionResult> Create(IdentityRole role)  
  30.         {  
  31.             await roleManager.CreateAsync(role);  
  32.             return RedirectToAction("Index");  
  33.         }  
  34.     }  
  35. }  
Then. create the views, as shown below:
 
Index View
  1. @model IEnumerable<Microsoft.AspNetCore.Identity.IdentityRole>  
  2. @{  
  3.     ViewData["Title"] = "Index";  
  4. }  
  5.   
  6. <h1>List of Roles</h1>  
  7. <a asp-action="Create">Create</a>  
  8. <table class="table table-striped table-bordered">  
  9.     <thead>  
  10.         <tr>  
  11.             <td>Id</td>  
  12.             <td>Name</td>  
  13.         </tr>  
  14.     </thead>  
  15.     <tbody>  
  16.         @foreach (var role in Model)  
  17.         {  
  18.             <tr>  
  19.                 <td>@role.Id</td>  
  20.                 <td>@role.Name</td>  
  21.             </tr>  
  22.         }  
  23.     </tbody>  
  24. </table>  
Create View
  1. @model Microsoft.AspNetCore.Identity.IdentityRole  
  2.   
  3. @{  
  4.     ViewData["Title"] = "Create";  
  5. }  
  6. <h1>Create Role</h1>  
  7. <hr />  
  8. <div class="row">  
  9.     <div class="col-md-4">  
  10.         <form asp-action="Create">  
  11.             <div asp-validation-summary="ModelOnly" class="text-danger"></div>  
  12.             <div class="form-group">  
  13.                 <label asp-for="Name" class="control-label"></label>  
  14.                 <input asp-for="Name" class="form-control" />  
  15.                 <span asp-validation-for="Name" class="text-danger"></span>  
  16.             </div>  
  17.             <div class="form-group">  
  18.                 <input type="submit" value="Create" class="btn btn-primary" />  
  19.             </div>  
  20.         </form>  
  21.     </div>  
  22. </div>  
  23. <div>  
  24.     <a asp-action="Index">Back to List</a>  
  25. </div>  
  26. @section Scripts {  
  27.     @{await Html.RenderPartialAsync("_ValidationScriptsPartial");  
  28.     }  
  29. }  
Next, we need to modify the Startup.cs file, as shown below:
  1. services.AddDbContext<ApplicationDbContext>(options =>  
  2.                 options.UseSqlServer(  
  3.                     Configuration.GetConnectionString("DefaultConnection")));  
  4.             services.AddIdentity<IdentityUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)  
  5.                  .AddDefaultUI()  
  6.                  .AddEntityFrameworkStores<ApplicationDbContext>()  
  7.                  .AddDefaultTokenProviders();  
  8.             services.AddControllersWithViews();  
  9.             services.AddRazorPages();  
In order to display the link on the main page, update the _Layout.cshtml file:
  1. <!DOCTYPE html>  
  2. <html lang="en">  
  3. <head>  
  4.     <meta charset="utf-8" />  
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0" />  
  6.     <title>@ViewData["Title"] - TestAppAuthAndAuthorize</title>  
  7.     <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />  
  8.     <link rel="stylesheet" href="~/css/site.css" />  
  9. </head>  
  10. <body>  
  11.     <header>  
  12.         <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">  
  13.             <div class="container">  
  14.                 <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">TestAppAuthAndAuthorize</a>  
  15.                 <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"  
  16.                         aria-expanded="false" aria-label="Toggle navigation">  
  17.                     <span class="navbar-toggler-icon"></span>  
  18.                 </button>  
  19.                 <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">  
  20.                     <partial name="_LoginPartial" />  
  21.                     <ul class="navbar-nav flex-grow-1">  
  22.                         <li class="nav-item">  
  23.                             <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>  
  24.                         </li>  
  25.                         <li class="nav-item">  
  26.                             <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>  
  27.                         </li>  
  28.                         <li class="nav-item">  
  29.                             <a class="nav-link text-dark" asp-area="" asp-controller="Role" asp-action="Index">Role</a>  
  30.                         </li>  
  31.                     </ul>  
  32.                 </div>  
  33.             </div>  
  34.         </nav>  
  35.     </header>  
  36.     <div class="container">  
  37.         <main role="main" class="pb-3">  
  38.             @RenderBody()  
  39.         </main>  
  40.     </div>  
  41.   
  42.     <footer class="border-top footer text-muted">  
  43.         <div class="container">  
  44.             © 2020 - TestAppAuthAndAuthorize - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>  
  45.         </div>  
  46.     </footer>  
  47.     <script src="~/lib/jquery/dist/jquery.min.js"></script>  
  48.     <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>  
  49.     <script src="~/js/site.js" asp-append-version="true"></script>  
  50.     @RenderSection("Scripts", required: false)  
  51. </body>  
  52. </html>  
Now, when we run the application, we see the below screen:
 
Adding Role Authorization To A ASP.NET MVC Core Application
 
We now add 3 roles, as shown below:
 
Adding Role Authorization To A ASP.NET MVC Core Application
 
Next, we need to add the role to the page where we add users in order to assign the role to a user when the user is created. We would need to make the below changes in the Register.cshtml.cs file:
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.ComponentModel.DataAnnotations;  
  4. using System.Linq;  
  5. using System.Text;  
  6. using System.Text.Encodings.Web;  
  7. using System.Threading.Tasks;  
  8. using Microsoft.AspNetCore.Authentication;  
  9. using Microsoft.AspNetCore.Authorization;  
  10. using Microsoft.AspNetCore.Identity;  
  11. using Microsoft.AspNetCore.Identity.UI.Services;  
  12. using Microsoft.AspNetCore.Mvc;  
  13. using Microsoft.AspNetCore.Mvc.RazorPages;  
  14. using Microsoft.AspNetCore.WebUtilities;  
  15. using Microsoft.Extensions.Logging;  
  16.   
  17. namespace TestAppAuthAndAuthorize.Areas.Identity.Pages.Account  
  18. {  
  19.     [AllowAnonymous]  
  20.     public class RegisterModel : PageModel  
  21.     {  
  22.         private readonly SignInManager<IdentityUser> _signInManager;  
  23.         private readonly UserManager<IdentityUser> _userManager;  
  24.         private readonly ILogger<RegisterModel> _logger;  
  25.         private readonly IEmailSender _emailSender;  
  26.         private readonly RoleManager<IdentityRole> _roleManager;  
  27.   
  28.         public RegisterModel(  
  29.             UserManager<IdentityUser> userManager,  
  30.             SignInManager<IdentityUser> signInManager,  
  31.             ILogger<RegisterModel> logger,  
  32.             IEmailSender emailSender,  
  33.             RoleManager<IdentityRole> roleManager)  
  34.         {  
  35.             _userManager = userManager;  
  36.             _signInManager = signInManager;  
  37.             _logger = logger;  
  38.             _emailSender = emailSender;  
  39.             _roleManager = roleManager;  
  40.         }  
  41.   
  42.         [BindProperty]  
  43.         public InputModel Input { getset; }  
  44.   
  45.         public string ReturnUrl { getset; }  
  46.   
  47.         public IList<AuthenticationScheme> ExternalLogins { getset; }  
  48.   
  49.         public class InputModel  
  50.         {  
  51.             [Required]  
  52.             [EmailAddress]  
  53.             [Display(Name = "Email")]  
  54.             public string Email { getset; }  
  55.   
  56.             [Required]  
  57.             [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]  
  58.             [DataType(DataType.Password)]  
  59.             [Display(Name = "Password")]  
  60.             public string Password { getset; }  
  61.   
  62.             [DataType(DataType.Password)]  
  63.             [Display(Name = "Confirm password")]  
  64.             [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]  
  65.             public string ConfirmPassword { getset; }  
  66.             public string Name { getset; }  
  67.         }  
  68.   
  69.         public async Task OnGetAsync(string returnUrl = null)  
  70.         {  
  71.             ViewData["roles"] = _roleManager.Roles.ToList();  
  72.             ReturnUrl = returnUrl;  
  73.             ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();  
  74.         }  
  75.   
  76.         public async Task<IActionResult> OnPostAsync(string returnUrl = null)  
  77.         {  
  78.             returnUrl = returnUrl ?? Url.Content("~/");  
  79.             var role = _roleManager.FindByIdAsync(Input.Name).Result;  
  80.             ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();  
  81.             if (ModelState.IsValid)  
  82.             {  
  83.                 var user = new IdentityUser { UserName = Input.Email, Email = Input.Email };  
  84.                 var result = await _userManager.CreateAsync(user, Input.Password);  
  85.                 if (result.Succeeded)  
  86.                 {  
  87.                     _logger.LogInformation("User created a new account with password.");  
  88.                     await _userManager.AddToRoleAsync(user, role.Name);  
  89.   
  90.                     var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);  
  91.                     code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));  
  92.                     var callbackUrl = Url.Page(  
  93.                         "/Account/ConfirmEmail",  
  94.                         pageHandler: null,  
  95.                         values: new { area = "Identity", userId = user.Id, code = code },  
  96.                         protocol: Request.Scheme);  
  97.   
  98.                     await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",  
  99.                         $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");  
  100.   
  101.                     if (_userManager.Options.SignIn.RequireConfirmedAccount)  
  102.                     {  
  103.                         return RedirectToPage("RegisterConfirmation"new { email = Input.Email });  
  104.                     }  
  105.                     else  
  106.                     {  
  107.                         await _signInManager.SignInAsync(user, isPersistent: false);  
  108.                         return LocalRedirect(returnUrl);  
  109.                     }  
  110.                 }  
  111.                 foreach (var error in result.Errors)  
  112.                 {  
  113.                     ModelState.AddModelError(string.Empty, error.Description);  
  114.                 }  
  115.             }  
  116.   
  117.             ViewData["roles"] = _roleManager.Roles.ToList();  
  118.             // If we got this far, something failed, redisplay form  
  119.             return Page();  
  120.         }  
  121.     }  
  122. }  
Add the below changes to the Register.cshtml file:
  1. @page  
  2. @model RegisterModel  
  3. @{  
  4.     ViewData["Title"] = "Register";  
  5.     var roles = (List<IdentityRole>)ViewData["roles"];  
  6. }  
  7.   
  8. <h1>@ViewData["Title"]</h1>  
  9.   
  10. <div class="row">  
  11.     <div class="col-md-4">  
  12.         <form asp-route-returnUrl="@Model.ReturnUrl" method="post">  
  13.             <h4>Create a new account.</h4>  
  14.             <hr />  
  15.             <div asp-validation-summary="All" class="text-danger"></div>  
  16.             <div class="form-group">  
  17.                 <label asp-for="Input.Email"></label>  
  18.                 <input asp-for="Input.Email" class="form-control" />  
  19.                 <span asp-validation-for="Input.Email" class="text-danger"></span>  
  20.             </div>  
  21.             <div class="form-group">  
  22.                 <label asp-for="Input.Password"></label>  
  23.                 <input asp-for="Input.Password" class="form-control" />  
  24.                 <span asp-validation-for="Input.Password" class="text-danger"></span>  
  25.             </div>  
  26.             <div class="form-group">  
  27.                 <label asp-for="Input.ConfirmPassword"></label>  
  28.                 <input asp-for="Input.ConfirmPassword" class="form-control" />  
  29.                 <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>  
  30.             </div>  
  31.             <div class="form-group">  
  32.                 <label asp-for="Input.Name"></label>  
  33.                 <select asp-for="Input.Name" class="form-control" asp-items='new SelectList(roles,"Id","Name")'>  
  34.                 </select>  
  35.                 <span asp-validation-for="Input.Name" class="text-danger"></span>  
  36.             </div>  
  37.             <button type="submit" class="btn btn-primary">Register</button>  
  38.         </form>  
  39.     </div>  
  40.     <div class="col-md-6 col-md-offset-2">  
  41.         <section>  
  42.             <h4>Use another service to register.</h4>  
  43.             <hr />  
  44.             @{  
  45.                 if ((Model.ExternalLogins?.Count ?? 0) == 0)  
  46.                 {  
  47.                     <div>  
  48.                         <p>  
  49.                             There are no external authentication services configured. See <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>  
  50.                             for details on setting up this ASP.NET application to support logging in via external services.  
  51.                         </p>  
  52.                     </div>  
  53.                 }  
  54.                 else  
  55.                 {  
  56.                     <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">  
  57.                         <div>  
  58.                             <p>  
  59.                                 @foreach (var provider in Model.ExternalLogins)  
  60.                                 {  
  61.                                     <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>  
  62.                                 }  
  63.                             </p>  
  64.                         </div>  
  65.                     </form>  
  66.                 }  
  67.             }  
  68.         </section>  
  69.     </div>  
  70. </div>  
  71.   
  72. @section Scripts {  
  73.     <partial name="_ValidationScriptsPartial" />  
  74. }  
The last step is to define some policies. These are groups of roles that can be assigned to actions and controllers in order to control access to these functionalities. We will create the policies in the Startup.cs file, as shown below:
  1. public void ConfigureServices(IServiceCollection services)  
  2.         {  
  3.             services.AddDbContext<ApplicationDbContext>(options =>  
  4.                 options.UseSqlServer(  
  5.                     Configuration.GetConnectionString("DefaultConnection")));  
  6.             services.AddIdentity<IdentityUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)  
  7.                  .AddDefaultUI()  
  8.                  .AddEntityFrameworkStores<ApplicationDbContext>()  
  9.                  .AddDefaultTokenProviders();  
  10.             services.AddControllersWithViews();  
  11.             services.AddRazorPages();  
  12.   
  13.             services.AddAuthorization(options => {  
  14.                 options.AddPolicy("readpolicy",  
  15.                     builder => builder.RequireRole("Admin""Manager""User"));  
  16.                 options.AddPolicy("writepolicy",  
  17.                     builder => builder.RequireRole("Admin""Manager"));  
  18.             });  
  19.         }  
Then, we will apply these policies to the Role controller, as below:
  1. using Microsoft.AspNetCore.Authorization;  
  2. using Microsoft.AspNetCore.Identity;  
  3. using Microsoft.AspNetCore.Mvc;  
  4. using System.Linq;  
  5. using System.Threading.Tasks;  
  6.    
  7. namespace TestAppAuthAndAuthorize.Controllers  
  8. {  
  9.     public class RoleController : Controller  
  10.     {  
  11.         RoleManager<IdentityRole> roleManager;  
  12.   
  13.         public RoleController(RoleManager<IdentityRole> roleManager)  
  14.         {  
  15.             this.roleManager = roleManager;  
  16.         }  
  17.   
  18.         [Authorize(Policy = "readpolicy")]  
  19.         public IActionResult Index()  
  20.         {  
  21.             var roles = roleManager.Roles.ToList();  
  22.             return View(roles);  
  23.         }  
  24.   
  25.         [Authorize(Policy = "writepolicy")]  
  26.         public IActionResult Create()  
  27.         {  
  28.             return View(new IdentityRole());  
  29.         }  
  30.   
  31.         [HttpPost]  
  32.         public async Task<IActionResult> Create(IdentityRole role)  
  33.         {  
  34.             await roleManager.CreateAsync(role);  
  35.             return RedirectToAction("Index");  
  36.         }  
  37.     }  
  38. }  
We now will run the application, and when we click on the “Role” link, we are asked to log in. Once logged in, we see the list of roles. We can also click Create to add a new role, as shown below:
 
Adding Role Authorization To A ASP.NET MVC Core Application
 
Now, we will create a new user with the “User” role. Next, we will log in as this user and click the “Role” link. The list of roles will be displayed (as the user role has access to the Index action). However, as soon as we click the “Create” link on the roles page, we get the below screen:
 
Adding Role Authorization To A ASP.NET MVC Core Application
 
This is because the user role does not have access to create the action of the role controller.
 

Summary

 
In this article, we have looked at a simple example of using authentication and authorization in an ASP.NET MVC Core application. As we have seen, this process is quite straightforward. There are many other options for Authentication, including using Azure AD, third party providers like Google, Facebook and Microsoft, etc. I would recommend that you try these other options as well.