Introduction
Authorization is a process of determining whether the user is able to access the system resource. In my previous article, I have explained role-based authorization. The identity membership system allows us to map one or more roles with the user; based on the role, we can do authorization. In this article, I will explain how to do authorization based on policy and claim.
Claim-based authorization
Claims are the user data that is issued by a trusted source. If we are working with token-based authentication, a claim may be added within a token by the server that generates the token. A claim can have any kind of data such as "DateOfJoining", "DateOfBirth", "email", etc. Based on a claim that a user has a system that provides access to the page, that is called claim-based authorization. For example, the system will provide access to the page if the user has a "DateOfBirth" claim. In short, claim-based authorization checks the value of the claim and allows access to the system resource based on the value of a claim.
To demonstrate with an example, I have created two users and associated some claim identity with the user. I have achieved this by using the following code.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider)
{
....
....
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
CreateUserAndClaim(serviceProvider).Wait();
}
private async Task CreateUserAndClaim(IServiceProvider serviceProvider)
{
var UserManager = serviceProvider.GetRequiredService<UserMfanager<IdentityUser>>();
IdentityUser user = await UserManager.FindByEmailAsync("[email protected]");
if (user == null)
{
user = new IdentityUser()
{
UserName = "[email protected]",
Email = "[email protected]",
};
await UserManager.CreateAsync(user, "Test@123");
}
var claimList = (await UserManager.GetClaimsAsync(user)).Select(p => p.Type);
if (!claimList.Contains("DateOfJoing")){
await UserManager.AddClaimAsync(user, new Claim("DateOfJoing", "09/25/1984"));
}
if (!claimList.Contains("IsAdmin")){
await UserManager.AddClaimAsync(user, new Claim("IsAdmin", "true"));
}
IdentityUser user2 = await UserManager.FindByEmailAsync("[email protected]");
if (user2 == null)
{
user2 = new IdentityUser()
{
UserName = "[email protected]",
Email = "[email protected]",
};
await UserManager.CreateAsync(user2, "Test@123");
}
var claimList1 = (await UserManager.GetClaimsAsync(user2)).Select(p => p.Type);
if (!claimList.Contains("IsAdmin"))
{
await UserManager.AddClaimAsync(user2, new Claim("IsAdmin", "false"));
}
}
Claim-based authorization can be done by creating a policy, i.e., creating and registering a policy stating the claims requirement. The simple type of claim policy checks only for the existence of the claim, but with an advanced level, we can check the user claim with its value. We can also assign more than one value for a claim check.
In the following example, I have created the policy that checks the two claims for user authorization: one for "DateofJoining" and another for "IsAdmin". Here "DateofJoining" is a simple type of claim; i.e., it only checks if a claim exists or not whereas "IsAdmin" claim checks with its value.
public void ConfigureServices(IServiceCollection services)
{
....
....
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthorization(options =>
{
options.AddPolicy("IsAdminClaimAccess", policy => policy.RequireClaim("DateOfJoing"));
options.AddPolicy("IsAdminClaimAccess", policy => policy.RequireClaim("IsAdmin", "true"));
});
}
We can apply this policy to Authorize attributes using the "Policy" property. Here, we have to specify the name of the policy.
[Authorize(Policy = "IsAdminClaimAccess")]
public IActionResult TestMethod1()
{
return View("MyPage");
}
We can also apply multiple policies to the controller or action. To grant access, all policies must be passed.
[Authorize(Policy = "IsAdminClaimAccess")]
[Authorize(Policy = "NonAdminAccess")]
public IActionResult TestMethod2()
{
return View("MyPage");
}
In the above examples, we assign claims to the user and authorize them by creating the policy. Alternatively, claims can also be assigned to a user role, so using this, an entire group of users can access the page or resources. I have made a small change in my code that generates user and user roles and adds claims to the roles.
private async Task CreateUserAndClaim(IServiceProvider serviceProvider)
{
var UserManager = serviceProvider.GetRequiredService<UserManager<IdentityUser>>();
var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
//Added Roles
var roleResult = await RoleManager.FindByNameAsync("Administrator");
if (roleResult == null)
{
roleResult = new IdentityRole("Administrator");
await RoleManager.CreateAsync(roleResult);
}
var roleClaimList = (await RoleManager.GetClaimsAsync(roleResult)).Select(p => p.Type);
if(!roleClaimList.Contains("ManagerPermissions"))
{
await RoleManager.AddClaimAsync(roleResult, new Claim("ManagerPermissions", "true"));
}
IdentityUser user = await UserManager.FindByEmailAsync("[email protected]");
if (user == null)
{
user = new IdentityUser()
{
UserName = "[email protected]",
Email = "[email protected]",
};
await UserManager.CreateAsync(user, "Test@123");
}
await UserManager.AddToRoleAsync(user, "Administrator");
....
....
....
....
}
Same as the above-mentioned code, we can create a policy for the role-based claim and applied to the controller or action method by using the Authorize attribute.
public void ConfigureServices(IServiceCollection services)
{
...
...
services.AddAuthorization(options =>
{
...
...
options.AddPolicy("RoleBasedClaim", policy => policy.RequireClaim("ManagerPermissions", "true"));
});
}
[Authorize(Policy = "RoleBasedClaim")]
public IActionResult TestMethod3()
{
return View("MyPage");
}
Policy-based authorization
The .NET Core Framework allows us to create policies for authorization. We can either use pre-configured policies or create a custom policy based on our requirements.
In the role-based authorization and claims-based authorization (refer to the preceding section), we are using pre-configured policies such as RequireClaim and RequireRole. The policy contains one or more requirements and registers in the AddAuthorization service configuration.
Authorization Requirements
The requirement is the collection of data that can be used to evaluate the user principle. To create the requirement, the class must implement the interface IAuthorizationRequirement which is an empty interface. The requirement does not contain any data and evaluation mechanism.
Example
In the following example, I have created a requirement for minimum time spent for the organization.
public class MinimumTimeSpendRequirement: IAuthorizationRequirement
{
public MinimumTimeSpendRequirement(int noOfDays)
{
TimeSpendInDays = noOfDays;
}
protected int TimeSpendInDays { get; private set; }
}
Authorization handlers
The authorization handler contains the evaluation mechanism for properties of requirement. The handler must evaluate the requirement properties against the AuthorizationContext and decide if the user is allowed to access the system resources or not. One requirement may have multiple handlers. The authorization handler must inherit from the AuthorizationHandler<T> class; here, T is a type of requirement class.
Example
In the following example code, I have created a handler for the requirement MinimumTimeSpendRequirement. This handler first looks for the date of joining the claim (DateOfJoining). If this claim does not exist for the user, we can mark this as an unauthorized request. If the user has a claim, then we calculate how many days are spent by the user within the organization. If this meets the requirement passed in the authorization service, then the user is authorized to access. I have the call context.Succeed() means that the user fulfilled all the requirements.
public class MinimumTimeSpendHandler : AuthorizationHandler<MinimumTimeSpendRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumTimeSpendRequirement requirement)
{
if (!context.User.HasClaim(c => c.Type == "DateOfJoining"))
{
return Task.FromResult(0);
}
var dateOfJoining = Convert.ToDateTime(context.User.FindFirst(
c => c.Type == "DateOfJoining").Value);
double calculatedTimeSpend = (DateTime.Now.Date - dateOfJoining.Date).TotalDays;
if (calculatedTimeSpend >= requirement.TimeSpendInDays)
{
context.Succeed(requirement);
}
return Task.FromResult(0);
}
}
Handler registration
The handle that is using authorization must register in service collection. We can add the service collection by using the "services.AddSingleton<IAuthorizationHandler, ourHandlerClass>()" method, where we need to pass the handler class.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthorization(options =>
{
...
...
options.AddPolicy("Morethan365DaysClaim", policy => policy.Requirements.Add(new MinimumTimeSpendRequirement(365)));
}
services.AddSingleton<IAuthorizationHandler, MinimumTimeSpendHandler>();
}
The handler does not return any value, so a handler indicates the success by calling context.Succeed(requirement) method, here, whatever requirement we are passing that has been successfully validated. Suppose we do not call context.Succeed method, handler automatically fails or we can call context.Fail() method.
Multiple handlers for a Requirement
In some cases, we are required to evaluate requirements based on OR conditions, and we can implement multiple handlers for a single requirement. For example, we have a requirement, like a page can be accessed by the user if he spends at least 365 days in an organization or a user from the HR department. In this case, we have a single requirement to access the page, not multiple handlers to validate a single requirement.
Example
using Microsoft.AspNetCore.Authorization;
using System;
using System.Threading.Tasks;
namespace ClaimBasedPolicyBasedAuthorization.Policy
{
public class PageAccessRequirement : IAuthorizationRequirement
{
}
public class TimeSpendHandler : AuthorizationHandler<PageAccessRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PageAccessRequirement requirement)
{
if (!context.User.HasClaim(c => c.Type == "DateOfJoining"))
{
return Task.FromResult(0);
}
var dateOfJoining = Convert.ToDateTime(context.User.FindFirst(
c => c.Type == "DateOfJoining").Value);
double calculatedTimeSpend = (DateTime.Now.Date - dateOfJoining.Date).TotalDays;
if (calculatedTimeSpend >= 365)
{
context.Succeed(requirement);
}
return Task.FromResult(0);
}
}
public class RoleCheckerHandler : AuthorizationHandler<PageAccessRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PageAccessRequirement requirement)
{
if (!context.User.HasClaim(c => c.Type == "IsHR"))
{
return Task.FromResult(0);
}
var isHR = Convert.ToBoolean(context.User.FindFirst(c => c.Type == "IsHR").Value);
if (isHR)
{
context.Succeed(requirement);
}
return Task.FromResult(0);
}
}
}
Here, we need to register both handlers. If any one handler of the two succeeds, the policy evaluation has succeeded.
public void ConfigureServices(IServiceCollection services)
{
....
....
....
services.AddAuthorization(options =>
{
....
....
options.AddPolicy("AccessPageTestMethod5", policy => policy.Requirements.Add(new PageAccessRequirement()));
});
...
...
services.AddSingleton<IAuthorizationHandler, TimeSpendHandler>();
services.AddSingleton<IAuthorizationHandler, RoleCheckerHandler>();
}
Using the RequireAssertion policy builder method, we can add simple expressions for the policy without requirement and handler. The code mentioned for the above example can be re-written as follows.
services.AddAuthorization(options =>
{
...
....
options.AddPolicy("AccessPageTestMethod6",
policy => policy.RequireAssertion(context =>
context.User.HasClaim(c =>
(c.Type == "IsHR" && Convert.ToBoolean(context.User.FindFirst(c2 => c2.Type == "IsHR").Value)) ||
(c.Type == "DateOfJoining" && (DateTime.Now.Date - Convert.ToDateTime(context.User.FindFirst(c2 => c2.Type == "DateOfJoining").Value).Date).TotalDays >= 365))
));
});
Can we access request Context in Handlers?
The HandleRequirementAsync method contains two parameters: AuthorizationContext and Requirement. Some of the frameworks, such as MVC, are allowed to add any object to the Resource property of the AuthorizationContext to pass extra information. This Resource property is specific to the framework, so we can cast context. Resource object to appropriate context and access the resources,
var myContext = context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext;
if (myContext != null)
{
// Examine MVC specific item.
var controllerName = ((Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor)myContext.ActionDescriptor).ControllerName;
}
Summary
Claim-based authorization allows us to validate the user based on other characteristics such as username, date of joining, employee, other information, etc. It is Probably not possible with another kind of authorization, such as role-based authorization. Claim-based authorization can be achieved through policy-based authorization by using a pre-configured policy. We can either use pre-configured policies or create a custom policy based on our requirement for authorization.
You can view or download the source code from the GitHub link here.