(C# ASP.NET Core) Claim based Authorization with Identity

In its simplest terms, a claim is a name-value pair used to verify authorization. For example, you may be authorized to access a page if a claim of name "my_role" and of value "admin" is attached to your identity. In this tutorial we learn how to configure a project for claim based authorization, and how to add claim based authorization to a page.

Categories | About |     |  

Parveen,

Step 1 - About the UserClaimsPrincipalFactory

Let us suppose we have an admin account of email - admin@mydomain.com. Let us also suppose that this is a special account and that when this user logs in, a claim of name, say, "Role", and of value "Admin" will be attached to this user so that he would be able to gain access to the pages of an administrator.

The question is how to attach a claim to a user who has just logged in?

For this, we have to inherit a class from the built-in class UserClaimsPrincipalFactory, and then we have to over-ride its CreateAsync method and apply the claims. Let's see how.

Open the solution explorer and add a folder of any readable name such as Utility. The whole purpose is to ensure that this class exists somewhere in our project.

Right click and add a class called AdditionalUserClaimsPrincipalFactory. Double click the file to open it so that we can examine the code.

If you are following the course, then the source code is available in your downloads.

This is the AdditionalUserClaimsPrincipalFactory.cs file.

// Utility -> AdditionalUserClaimsPrincipalFactory.cs 
using Microsoft.AspNetCore.Identity;

using Microsoft.Extensions.Options;

using System.Security.Claims;

namespace MyRazorApp.Utility
{

public class AdditionalUserClaimsPrincipalFactory :
  UserClaimsPrincipalFactory<IdentityUser, IdentityRole>
  {

    public AdditionalUserClaimsPrincipalFactory(
      UserManager<IdentityUser> userManager,
      RoleManager<IdentityRole> roleManager,
      IOptions<IdentityOptions> optionsAccessor)
      : base(userManager, roleManager, optionsAccessor)
    {

    }

    public async override 
        Task<ClaimsPrincipal?> CreateAsync(IdentityUser user)
    {

      ClaimsPrincipal? principal = await base.CreateAsync(user);

      ClaimsIdentity? identity = principal?.Identity as ClaimsIdentity;

      // query some criterion and 
      // decide on the role 
      if (user.Email.Equals("admin@mydomain.com"))
      {

        Claim claim = new Claim(ClaimTypes.Role, "Admin");

        identity?.AddClaim(claim);

      }

      return principal;

    }

  }

}

First we have the namespace directives.

As you can see, the class AdditionalUserClaimsPrincipalFactory inherits from the UserClaimsPrincipalFactory class.

Then we have the constructor of this class.

Next we have an over-ride for the CreateAsync method. This is the most important part where the claim will be added to a logged user.

First we obtain the ClaimsPrincipal and then we use it to obtain the ClaimsIdentity.

At this point we can make any queries and decisions to attach various types of claims to a user who has just logged in.

We have, for example, used an if condition to compare the email of the user. If the user has an email same as that of the site administrator, then we have created a Claim and added it to the identity. The name of the claim is a pre-defined string constant ClaimTypes.Role. ASP.NET Core provides many pre-defined names for claims so we do not have to hard-code any strings.

Our next step now is to configure the Program.cs file for claims based authorization.

Video Explanation (see it happen!)

Please watch the following youtube video:

Step 2 - Configuring Claim based Authorization

Open the solution explorer and locate the Program.cs file. Double click to open it.

This is the Program.cs file.

// program.cs 
using Microsoft.AspNetCore.Identity;

using MyRazorApp.Data;

using MyRazorApp.Utility;

using System.Security.Claims;

var builder = WebApplication.CreateBuilder();

builder.Services.AddDbContext<MyAuthContext>();

// REMOVE THIS 
// builder.Services 
//    .AddDefaultIdentity<IdentityUser>( 
//        options => options.SignIn.RequireConfirmedAccount = true 
//        ) 
//    .AddEntityFrameworkStores<MyAuthContext>(); 

// (1) REPLACE THIS . . . 
builder.Services
  .AddIdentity<IdentityUser, IdentityRole>(
    options => options.SignIn.RequireConfirmedAccount = true
)
.AddTokenProvider<DataProtectorTokenProvider<IdentityUser>>(
    TokenOptions.DefaultProvider)
.AddEntityFrameworkStores<MyAuthContext>();


// (2) ADD THIS ALSO 
builder.Services.AddAuthorization(options =>
{

    options.AddPolicy("MyPolicy", policy =>
  {

      policy.RequireClaim(ClaimTypes.Role, "Admin");

  });

});

// (3) ADD THIS ALSO 
builder
  .Services
  .AddScoped<IUserClaimsPrincipalFactory<IdentityUser>,
      AdditionalUserClaimsPrincipalFactory>();

// authentication 
builder.Services
.ConfigureApplicationCookie(options =>
{

  options.LoginPath = "/Auth/Login";

  options.AccessDeniedPath = "/Auth/AccessDenied";

  // Cookie settings 
  // prevent cookie from being accessed 
  // through javascript on the client side 
  options.Cookie.HttpOnly = true;

  options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

  options.SlidingExpiration = true;

}

);

builder.Services.Configure<IdentityOptions>(options =>
{

  // Password settings. 
  options.Password.RequireDigit = true;

  options.Password.RequireLowercase = true;

  options.Password.RequireNonAlphanumeric = true;

  options.Password.RequireUppercase = true;

  options.Password.RequiredLength = 6;

  options.Password.RequiredUniqueChars = 1;

  // Lockout settings. 
  options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);

  options.Lockout.MaxFailedAccessAttempts = 5;

  options.Lockout.AllowedForNewUsers = true;

  // User settings. 
  options.User.AllowedUserNameCharacters =
  "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";

  options.User.RequireUniqueEmail = false;

});

builder.Services.AddRazorPages();

var app = builder.Build();

// create the database 
// these calls can be removed in 
// a production scenario 
// the recommended method is through migrations 
// as explained in the first tutorial of this chapter 
using (var scope = app.Services.CreateScope())

{

  var services = scope.ServiceProvider;

  var ctx = services.GetRequiredService<MyAuthContext>();

  ctx.Database.EnsureCreated();

}

app.UseAuthentication();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

First we have the namespaces. You can always refer these if you get compilation errors in your class or function names.

Then, we have the builder and DbContext - and we have been using them througout our Identity tutorials.

But we need a few changes for claims based authorization.

First of all AddDefaultIdentity must be commented out and removed. It should be replaced by AddIdentity. Secondly, we have to add the authorization service. Notice that Claims are registered through a policy. We have called the policy as MyPolicy, but any readable name could be given.

And, thirdly, we have to add a service for IUserClaimsPrincipalFactory through the AdditionalUserClaimsPrincipalFactory class that we added just before this. So now you should be able to connect the threads with the AdditionalUserClaimsPrincipalFactory class!

No other change is required in this file.

The cookie options remain as usual.

The password options remain as usual.

Everything else remains same till the app.Run method. You can obtain the source code from your downloads.

Step 3 - Apply Claim based Authorization

Now let us apply claim based authorization to a restricted page.

Open the solution explorer and locate the page called Restricted and double click to open its backing class.

// Restricted.cshtml.cs 
using Microsoft.AspNetCore.Authorization;

using Microsoft.AspNetCore.Mvc.RazorPages;

namespace MyRazorApp.Pages
{

  // using Microsoft.AspNetCore.Authorization; 

  [Authorize(Policy = "MyPolicy")]
  public class RestrictedModel : PageModel
  {

  }

}

As you can see we already have a simple authorization through the Authorize attribute. A simple authorization makes a simple authentication check. So, any user who has logged in successfully is granted access. No roles, no claims, no policies are checked.

Modify the attribute to restrict access to a policy called MyPolicy. We have registered this policy in our program.cs file. it will automatically apply the restriction of the Role-Admin claim.

Run the Application

Let us now run the app and open the home page. Click the "Click to View Restricted Page" link to open the restricted page.

We are taken to the login page. Enter a registered email, other than that of the administrator. Click the login button. We verify that we are denied access and redirected to the AccessDenied page.

Next close the app and again reach login page. Enter the administrator's email admin@mydomain.com. Please make sure that this account is first registered by clicking the Register link.

Click the login button. We verify that we are able to access the restricted page! Thanks!


This Blog Post/Article "(C# ASP.NET Core) Claim based Authorization with Identity" by Parveen is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.