ASP.NET Core 2.2 - Claims Authorization Policy
Update 04/16/2019
A comment indicates this article is confusing when applied to a new project and they are correct. I have included the article Require A Confirmed Email as a prerequisite. When you require a confirmed email and allow a user to update their email, you edit the register and login pages to use the Username property not the Email property as the login name.
This article will cover adding an authorization policy using claims to ASP.NET Core 2.2. I will assume you have created a new ASP.NET Core 2.2 Razor Pages project with Individual User Accounts, updated the database with the CreateIdentitySchema migration, scaffolded the Identity UI, implemented an EmailSender and require a confirmed email. Or you can download the free ASP.NET Core 2.2 - Bootstrap Native Project from Manage > Assets. I was able to restore, build and run the project with VS 2017, VS 2019 and VS Code.
There are plenty of discussions about role vs claims authorization. An interesting observation is that a role is a claim. The main difference is how and where the data is stored. An advantage of a claim is that it has type and value. If you employ claims authorization, there is no need for roles. With claims authorization, you need to use authorization policies.
Let's start by creating a new page which will have restricted access to authorized users. Create a new folder in Areas named Admin and a new folder in Admin named Pages. Copy _ViewImports.cshtml and _ViewStart.cshtml from Identity > Pages to Admin > Pages.
Edit _ViewImports.cshtml:
@namespace BootstrapNative.Areas.Admin.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Add a new razor page named Index to the new Admin > Pages folder.
Edit Index.cshtml:
@page @model IndexModel @{ ViewData["Title"] = "Admin"; } <h1>@ViewData["Title"]</h1> <hr /> <div class="row"> <div class="col-12"> This page is for administrators only. </div> </div>
Restrict page access to authenticated users with AuthorizeAreaPage.
Edit Startup.cs > ConfigureServices > services.AddMvc > AddRazorPagesOptions, add AuthorizeAreaPage:
options.Conventions.AuthorizeAreaPage("Admin", "/Index");
Now create a user with an administrator claim.
Edit Startup.cs, add the administrator constants to the Startup class:
//Unique string for administrator claim type const string AdministratorClaimType = "http://yourdomain.com/claims/administrator"; const string AdministratorOnlyPolicy = "AdministratorOnly";
Edit Startup.cs, add a CreateApplicationUsersAsync method:
private async Task CreateApplicationUsersAsync(IServiceScopeFactory scopeFactory) { var scope = scopeFactory.CreateScope(); var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>(); var signInManager = scope.ServiceProvider.GetRequiredService<SignInManager<ApplicationUser>>(); //Create admin user if not existing var adminUserName = "Administrator"; ApplicationUser adminUser = await userManager.FindByNameAsync(adminUserName); if (adminUser == null) { adminUser = new ApplicationUser { UserName = adminUserName, Email = "administrator@yourdomain.com", EmailConfirmed = true }; var result = await userManager.CreateAsync(adminUser, "P@ssw0rd"); if (!result.Succeeded) { throw new InvalidOperationException($"Unexpected error occurred creating new admin user in Startup.cs."); } } //Add Administrator claim type to the admin user if not has claim var admincp = await signInManager.ClaimsFactory.CreateAsync(adminUser); if (!admincp.HasClaim(c => c.Type == AdministratorClaimType)) { var result = await userManager.AddClaimAsync(adminUser, new Claim(AdministratorClaimType, string.Empty)); if (!result.Succeeded) { throw new InvalidOperationException($"Unexpected error occurred adding admin claim to user in Startup.cs."); } } }
Edit Startup.cs, resolve namespace issues:
using System; using System.Security.Claims; using System.Threading.Tasks;
Edit Startup.cs, add async keyword to the Configure method's signature:
public async void Configure(IApplicationBuilder app, IHostingEnvironment env, IEmailSender emailSender)
Edit Startup.cs > Configure, call the CreateApplicationUsersAsync method from the end of the Configure method:
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); await CreateApplicationUsersAsync(scopeFactory);
Add services.AddAuthorization with an AddPolicy option.
Edit Startup.cs > ConfigureServices:
services.AddAuthorization(options => { options.AddPolicy(AdministratorOnlyPolicy, policy => policy.RequireClaim(AdministratorClaimType)); });
Decorate the IndexModel class with an AuthorizeAttribute.
Edit Index.cshtml.cs:
[Authorize(Policy = "AdministratorOnly")] public class IndexModel : PageModel { public void OnGet() { } }
You can add a NavBar item which is only displayed for authenticated and authorized users.
Edit Pages > Shared > _Layout.cshtml, inject IAuthorizationService above <!DOCTYPE html>:
@using Microsoft.AspNetCore.Authorization @inject IAuthorizationService AuthorizationService
Edit Pages > Shared > _Layout.cshtml, add a conditional nav-item after the last nav-item:
@if (User.Identity.IsAuthenticated && (await AuthorizationService.AuthorizeAsync(User, "AdministratorOnly")).Succeeded) { <li class="nav-item"> <a class="nav-link text-dark" asp-area="Admin" asp-page="/Index">Admin</a> </li> }
With the Bootstrap Native Project, you need to enable the CreateApplicationUsersAsync method.
Edit Startup.cs > Configure, uncomment CreateApplicationUsersAsync:
// Update the database before uncommenting var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); await CreateApplicationUsersAsync(scopeFactory);
After updating the database, build and run. Create a new user which will not have the Administrator claim. Verify access is denied when you browse to /Admin. Then logout and login with the new Administrator user. Verify navigation and access.
Comments(0)