ASP.NET Core 2.2 - Claims Authorization Policy


Ken Haggerty
Created 04/07/2019 - Updated 04/16/2019 15:36
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.

Manage Assets

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.


Article Tags:

Authorization Claims Identity

1 Following


Comment Count = 3

Nick Downes
Nick Downes
Member Since 04/15/2019
Posted 04/16/2019 12:47

Hi Ken, another great article, but CreateApplicationUsersAsync creates the admin user and sets their UserName to 'Administator'. However when registering a new user, the email address entered on the default registration page is entered into that field but CreateApplicationUsersAsync sets the UserName to 'Administrator'. When trying to login, if you use 'Administrator' the Input model decoration [EmailAddress] fails the entry and I get a 'Not an email address' error. Logging in with the email address used in CreateApplicationUsersAsync (administrator@yourdomain.com) I get an Invalid Login error. Commenting out the decoration allows me to login using 'Administrator' and see the new menu item 'Admin'. I guess I have missed something.

Ken Haggerty
Ken Haggerty *
Member Since 01/04/2019
Posted 04/16/2019 14:40

Thanks for bringing this to my attention. I have updated the article to include the Require A Confirmed Email article as a prerequisite which will mitigate this issue.

Serguei Khous
Serguei Khous
Member Since 05/31/2019
Posted 06/04/2019 04:34

One more time, thank you Ken! I feel very lucky to come across your articles. Lots of great stuff! My best regards

Please log in to comment or follow.

Login Register