## 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** ```csharp 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 _userManager; public ManageUsersController( UserManager userManager) { _userManager = userManager; } public async Task 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** ```csharp 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** ```html @model ManageUsersViewModel @{ ViewData["Title"] = "Manage users"; }

@ViewData["Title"]

Administrators

@foreach (var user in Model.Administrators) { }
Id Email
@user.Id @user.Email

Everyone

@foreach (var user in Model.Everyone) { }
Id Email
@user.Id @user.Email
``` 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](access-denied.png) 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** ```csharp 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>(); await EnsureRolesAsync(roleManager); var userManager = services .GetRequiredService>(); 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: ```csharp private static async Task EnsureRolesAsync( RoleManager 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** ```csharp 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** ```csharp private static async Task EnsureTestAdminAsync( UserManager userManager) { var testAdmin = await userManager.Users .Where(x => x.UserName == "admin@todo.local") .SingleOrDefaultAsync(); if (testAdmin != null) return; testAdmin = new ApplicationUser { UserName = "admin@todo.local", Email = "admin@todo.local" }; await userManager.CreateAsync( testAdmin, "NotSecure123!!"); await userManager.AddToRoleAsync( testAdmin, Constants.AdministratorRole); } ``` 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! 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** ```csharp public static void Main(string[] args) { var host = BuildWebHost(args); InitializeDatabase(host); host.Run(); } ``` Then, add the new method to the class below `Main()`: ```csharp 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>(); logger.LogError(ex, "Error occurred seeding the DB."); } } } ``` Add this `using` statement to the top of the file: ```csharp 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 `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** ```html @using Microsoft.AspNetCore.Identity @using AspNetCoreTodo.Models @inject SignInManager signInManager @inject UserManager userManager @if (signInManager.IsSignedIn(User)) { var currentUser = await userManager.GetUserAsync(User); var isAdmin = currentUser != null && await userManager.IsInRoleAsync( currentUser, Constants.AdministratorRole); if (isAdmin) { } } ``` > 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** ```html ``` When you log in with an administrator account, you'll now see a new item on the top right: ![Manage Users link](manage-users.png)