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 and they are 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 provides the access to the page, which 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 2 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<UserManager<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 policy; i.e., create and register policy stating the claims requirement. The simple type of claim policy checks only for the existence of the claim but with advanced level, we can check the user claim with its value. We can also assign more than one value for a claim check.
In this following example, I have created the policy that checks the 2 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 attribute using "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 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, user role 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 controller or action method by using 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 to authorization. We can either use pre-configured policies or can create a custom policy based on our requirement.
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 AddAuthorization service configuration.
Authorization Requirements
The requirement is the collection of data which can be used to evaluate the user principal. To create the requirement, the class must implement interface IAuthorizationRequirement that 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 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 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 spent by user within the organization. If this meets the requirement passed in authorization service then the user is authorized to access. I have the call context.Succeed(), it 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 "services.AddSingleton<IAuthorizationHandler, ourHandlerClass>()" method where we need to pass 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>();
}
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. If 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 condition, we can implement multiple handlers for the 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 for validating 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 the 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 RequireAssertion policy builder method, we can add simple expression for the policy without requirement and handler. The code mentioned for he above example can be re-written as following
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 an 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. Probably it is not possible with another kind of authorization such as role-based authorization. The claim-based authorization can be achieved by the policy based authorization by using a pre-configured policy. We can either use pre-configured policies or can create a custom policy based on our requirement for authorization.
You can view or download the source code from the GitHub link here.