Authorization with roles

Roles are a common approach to handling authorization and permissions in a web application. For example, you might have an Administrator role that allows admins to see and manage all the users registered for your app, while normal users can only see their own information.

Add a Manage Users page

First, create a new controller:

Controllers/ManageUsersController.cs

  1. using System;
  2. using System.Linq;
  3. using System.Threading.Tasks;
  4. using Microsoft.AspNetCore.Mvc;
  5. using Microsoft.AspNetCore.Authorization;
  6. using Microsoft.AspNetCore.Identity;
  7. using AspNetCoreTodo.Models;
  8. using Microsoft.EntityFrameworkCore;
  9. namespace AspNetCoreTodo.Controllers
  10. {
  11. [Authorize(Roles = "Administrator")]
  12. public class ManageUsersController : Controller
  13. {
  14. private readonly UserManager<ApplicationUser> _userManager;
  15. public ManageUsersController(UserManager<ApplicationUser> userManager)
  16. {
  17. _userManager = userManager;
  18. }
  19. public async Task<IActionResult> Index()
  20. {
  21. var admins = await _userManager
  22. .GetUsersInRoleAsync("Administrator");
  23. var everyone = await _userManager.Users
  24. .ToArrayAsync();
  25. var model = new ManageUsersViewModel
  26. {
  27. Administrators = admins,
  28. Everyone = everyone
  29. };
  30. return View(model);
  31. }
  32. }
  33. }

Setting the Roles property on the [Authorize] attribute will ensure that the user must be logged in and assigned the Administrator role in order to view the page.

Next, create a view model:

Models/ManageUsersViewModel.cs

  1. using System.Collections.Generic;
  2. using AspNetCoreTodo.Models;
  3. namespace AspNetCoreTodo
  4. {
  5. public class ManageUsersViewModel
  6. {
  7. public IEnumerable<ApplicationUser> Administrators { get; set; }
  8. public IEnumerable<ApplicationUser> Everyone { get; set; }
  9. }
  10. }

Finally, create a view for the Index action:

Views/ManageUsers/Index.cshtml

  1. @model ManageUsersViewModel
  2. @{
  3. ViewData["Title"] = "Manage users";
  4. }
  5. <h2>@ViewData["Title"]</h2>
  6. <h3>Administrators</h3>
  7. <table class="table">
  8. <thead>
  9. <tr>
  10. <td>Id</td>
  11. <td>Email</td>
  12. </tr>
  13. </thead>
  14. @foreach (var user in Model.Administrators)
  15. {
  16. <tr>
  17. <td>@user.Id</td>
  18. <td>@user.Email</td>
  19. </tr>
  20. }
  21. </table>
  22. <h3>Everyone</h3>
  23. <table class="table">
  24. <thead>
  25. <tr>
  26. <td>Id</td>
  27. <td>Email</td>
  28. </tr>
  29. </thead>
  30. @foreach (var user in Model.Everyone)
  31. {
  32. <tr>
  33. <td>@user.Id</td>
  34. <td>@user.Email</td>
  35. </tr>
  36. }
  37. </table>

Start up the application and try to access the /ManageUsers route while logged in as a normal user. You’ll see this access denied page:

Access denied error

That’s because users aren’t assigned the Administrator role automatically.

Create a test administrator account

For obvious security reasons, there isn’t a checkbox on the registration page that makes it easy for anyone to create an administrator account. Instead, you can write some code in the Startup class that will create a test admin account the first time the application starts up.

Add this code to the if (env.IsDevelopment()) branch of the Configure method:

Startup.cs

  1. if (env.IsDevelopment())
  2. {
  3. // (... some code)
  4. // Make sure there's a test admin account
  5. EnsureRolesAsync(roleManager).Wait();
  6. EnsureTestAdminAsync(userManager).Wait();
  7. }

The EnsureRolesAsync and EnsureTestAdminAsync methods will need access to the RoleManager and UserManager services. You can inject them into the Configure method, just like you inject any service into your controllers:

  1. public void Configure(IApplicationBuilder app,
  2. IHostingEnvironment env,
  3. UserManager<ApplicationUser> userManager,
  4. RoleManager<IdentityRole> roleManager)
  5. {
  6. // ...
  7. }

Add the two new methods below the Configure method. First, the EnsureRolesAsync method:

  1. private static async Task EnsureRolesAsync(RoleManager<IdentityRole> roleManager)
  2. {
  3. var alreadyExists = await roleManager.RoleExistsAsync(Constants.AdministratorRole);
  4. if (alreadyExists) return;
  5. await roleManager.CreateAsync(new IdentityRole(Constants.AdministratorRole));
  6. }

This method checks to see if an Administrator role exists in the database. If not, it creates one. Instead of repeatedly typing the string "Administrator", create a small class called Constants to hold the value:

Constants.cs

  1. namespace AspNetCoreTodo
  2. {
  3. public static class Constants
  4. {
  5. public const string AdministratorRole = "Administrator";
  6. }
  7. }

Feel free to update the ManageUsersController you created before to use this constant value as well.

Next, write the EnsureTestAdminAsync method:

Startup.cs

  1. private static async Task EnsureTestAdminAsync(UserManager<ApplicationUser> userManager)
  2. {
  3. var testAdmin = await userManager.Users
  4. .Where(x => x.UserName == "admin@todo.local")
  5. .SingleOrDefaultAsync();
  6. if (testAdmin != null) return;
  7. testAdmin = new ApplicationUser { UserName = "admin@todo.local", Email = "admin@todo.local" };
  8. await userManager.CreateAsync(testAdmin, "NotSecure123!!");
  9. await userManager.AddToRoleAsync(testAdmin, Constants.AdministratorRole);
  10. }

If there isn’t already a user with the username admin@todo.local in the database, this method will create one and assign a temporary password. After you log in for the first time, you should change the account’s password to something secure.

Because these two methods are asynchronous and return a Task, the Wait method must be used in Configure to make sure they finish before Configure moves on. You’d normally use await for this, but for technical reasons you can’t use await in Configure. This is a rare exception - you should use await everywhere else!

When you start the application next, the admin@todo.local account will be created and assigned the Administrator role. Try logging in with this account, and navigating to http://localhost:5000/ManageUsers. You’ll see a list of all users registered for the application.

As an extra challenge, try adding more administration features to this page. For example, you could add a button that gives an administrator the ability to delete a user account.

Check for authorization in a view

The [Authorize] attribute makes it easy to perform an authorization check in a controller or action method, but what if you need to check authorization in a view? For example, it would be nice to display a “Manage users” link in the navigation bar if the logged-in user is an administrator.

You can inject the UserManager directly into a view to do these types of authorization checks. To keep your views clean and organized, create a new partial view that will add an item to the navbar in the layout:

Views/Shared/_AdminActionsPartial.cshtml

  1. @using Microsoft.AspNetCore.Identity
  2. @using AspNetCoreTodo.Models
  3. @inject SignInManager<ApplicationUser> SignInManager
  4. @inject UserManager<ApplicationUser> UserManager
  5. @if (SignInManager.IsSignedIn(User))
  6. {
  7. var currentUser = await UserManager.GetUserAsync(User);
  8. var isAdmin = currentUser != null
  9. && await UserManager.IsInRoleAsync(currentUser, Constants.AdministratorRole);
  10. if (isAdmin) {
  11. <ul class="nav navbar-nav navbar-right">
  12. <li><a asp-controller="ManageUsers" asp-action="Index">Manage Users</a></li>
  13. </ul>
  14. }
  15. }

A partial view is a small piece of a view that gets embedded into another view. It’s common to name partial views starting with an _ underscore, but it’s not necessary.

This partial view first uses the SignInManager to quickly determine whether the user is logged in. If they aren’t, the rest of the view code can be skipped. If there is a logged-in user, the UserManager is used to look up their details and perform an authorization check with IsInRoleAsync. If all checks succeed, a navbar item is rendered.

To include this partial in the main layout, edit _Layout.cshtml and add it in the navbar section:

Views/Shared/_Layout.cshtml

  1. <div class="navbar-collapse collapse">
  2. <ul class="nav navbar-nav">
  3. <li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
  4. <li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
  5. <li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
  6. </ul>
  7. @await Html.PartialAsync("_LoginPartial")
  8. @await Html.PartialAsync("_AdminActionsPartial")
  9. </div>

When you log in with an administrator account, you’ll now see a new item on the top right:

Manage Users link

Wrap up

ASP.NET Core Identity is a powerful security and identity system that helps you add authentication and authorization checks, and makes it easy to integrate with external identity providers. The dotnet new templates give you pre-built views and controllers that handle common scenarios like login and registration so you can get up and running quickly.

There’s much more that ASP.NET Core Identity can do. You can learn more in the documentation and examples available at https://docs.asp.net.