Table of Contents (top down ↓)
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.