Authorization with roles

Roles are a common approach to handling authorization and permissions in a web application. For example, it's common to create an Administrator role that gives admin users more permissions or power than normal users.

In this project, you'll add a Manage Users page that only administrators can see. If normal users try to access it, they'll see an error.

Add a Manage Users page

First, create a new controller:

Controllers/ManageUsersController.cs

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using AspNetCoreTodo.Models;
using Microsoft.EntityFrameworkCore;

namespace AspNetCoreTodo.Controllers
{
    [Authorize(Roles = "Administrator")]
    public class ManageUsersController : Controller
    {
        private readonly UserManager<ApplicationUser>
            _userManager;

        public ManageUsersController(
            UserManager<ApplicationUser> userManager)
        {
            _userManager = userManager;
        }

        public async Task<IActionResult> Index()
        {
            var admins = (await _userManager
                .GetUsersInRoleAsync("Administrator"))
                .ToArray();

            var everyone = await _userManager.Users
                .ToArrayAsync();

            var model = new ManageUsersViewModel
            {
                Administrators = admins,
                Everyone = everyone
            };

            return View(model);
        }
    }
}

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

using System.Collections.Generic;

namespace AspNetCoreTodo.Models
{
    public class ManageUsersViewModel
    {
        public ApplicationUser[] Administrators { get; set; }

        public ApplicationUser[] Everyone { get; set;}
    }
}

Finally, create a Views/ManageUsers folder and a view for the Index action:

Views/ManageUsers/Index.cshtml

@model ManageUsersViewModel

@{
    ViewData["Title"] = "Manage users";
}

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

<h3>Administrators</h3>

<table class="table">
    <thead>
        <tr>
            <td>Id</td>
            <td>Email</td>
        </tr>
    </thead>

    @foreach (var user in Model.Administrators)
    {
        <tr>
            <td>@user.Id</td>
            <td>@user.Email</td>
        </tr>
    }
</table>

<h3>Everyone</h3>

<table class="table">
    <thead>
        <tr>
            <td>Id</td>
            <td>Email</td>
        </tr>
    </thead>

    @foreach (var user in Model.Everyone)
    {
        <tr>
            <td>@user.Id</td>
            <td>@user.Email</td>
        </tr>
    }
</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, it isn't possible for anyone to register a new administrator account themselves. In fact, the Administrator role doesn't even exist in the database yet!

You can add the Administrator role plus a test administrator account to the database the first time the application starts up. Adding first-time data to the database is called initializing or seeding the database.

Create a new class in the root of the project called SeedData:

SeedData.cs

using System;
using System.Linq;
using System.Threading.Tasks;
using AspNetCoreTodo.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace AspNetCoreTodo
{
    public static class SeedData
    {
        public static async Task InitializeAsync(
            IServiceProvider services)
        {
            var roleManager = services
                .GetRequiredService<RoleManager<IdentityRole>>();
            await EnsureRolesAsync(roleManager);

            var userManager = services
                .GetRequiredService<UserManager<ApplicationUser>>();
            await EnsureTestAdminAsync(userManager);
        }
    }
}

The InitializeAsync() method uses an IServiceProvider (the collection of services that is set up in the Startup.ConfigureServices() method) to get the RoleManager and UserManager from ASP.NET Core Identity.

Add two more methods below the InitializeAsync() method. First, the EnsureRolesAsync() method:

private static async Task EnsureRolesAsync(
    RoleManager<IdentityRole> roleManager)
{
    var alreadyExists = await roleManager
        .RoleExistsAsync(Constants.AdministratorRole);

    if (alreadyExists) return;

    await roleManager.CreateAsync(
        new IdentityRole(Constants.AdministratorRole));
}

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

namespace AspNetCoreTodo
{
    public static class Constants
    {
        public const string AdministratorRole = "Administrator";
    }
}

If you want, you can update the ManageUsersController to use this constant value as well.

Next, write the EnsureTestAdminAsync() method:

SeedData.cs

private static async Task EnsureTestAdminAsync(
    UserManager<ApplicationUser> userManager)
{
    var testAdmin = await userManager.Users
        .Where(x => x.UserName == "[email protected]")
        .SingleOrDefaultAsync();

    if (testAdmin != null) return;

    testAdmin = new ApplicationUser
    {
        UserName = "[email protected]",
        Email = "[email protected]"
    };
    await userManager.CreateAsync(
        testAdmin, "NotSecure123!!");
    await userManager.AddToRoleAsync(
        testAdmin, Constants.AdministratorRole);
}

If there isn't already a user with the username [email protected] 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!

Next, you need to tell your application to run this logic when it starts up. Modify Program.cs and update Main() to call a new method, InitializeDatabase():

Program.cs

public static void Main(string[] args)
{
    var host = BuildWebHost(args);
    InitializeDatabase(host);
    host.Run();
}

Then, add the new method to the class below Main():

private static void InitializeDatabase(IWebHost host)
{
    using (var scope = host.Services.CreateScope())
    {
        var services = scope.ServiceProvider;

        try
        {
            SeedData.InitializeAsync(services).Wait();
        }
        catch (Exception ex)
        {
            var logger = services
                .GetRequiredService<ILogger<Program>>();
            logger.LogError(ex, "Error occurred seeding the DB.");
        }
    }
}

Add this using statement to the top of the file:

using Microsoft.Extensions.DependencyInjection;

This method gets the service collection that SeedData.InitializeAsync() needs and then runs the method to seed the database. If something goes wrong, an error is logged.

Because InitializeAsync() returns a Task, the Wait() method must be used to make sure it finishes before the application starts up. You'd normally use await for this, but for technical reasons you can't use await in the Program class. This is a rare exception. You should use await everywhere else!

When you start the application next, the [email protected] 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

@using Microsoft.AspNetCore.Identity
@using AspNetCoreTodo.Models

@inject SignInManager<ApplicationUser> signInManager
@inject UserManager<ApplicationUser> userManager

@if (signInManager.IsSignedIn(User))
{
    var currentUser = await userManager.GetUserAsync(User);

    var isAdmin = currentUser != null
        && await userManager.IsInRoleAsync(
            currentUser,
            Constants.AdministratorRole);

    if (isAdmin)
    {
        <ul class="nav navbar-nav navbar-right">
            <li>
                <a asp-controller="ManageUsers" 
                   asp-action="Index">
                   Manage Users
                </a>
            </li>
        </ul>
    }
}

It's conventional to name shared partial views starting with an _ underscore, but it's not required.

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 and the user is an adminstrator, a Manage users link is added to the navbar.

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

Views/Shared/_Layout.cshtml

<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <!-- existing code here -->
    </ul>
    @await Html.PartialAsync("_LoginPartial")
    @await Html.PartialAsync("_AdminActionsPartial")
</div>

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

Manage Users link

results matching ""

    No results matching ""