Introduction
In my previous article, I talked about policy-based authorization. We can register all the required policies using the AddPolicy method of the AuthorizationOptions class. If we have a large number of policies, this is not a desirable way to register all policies. In such a case, we can use a custom policy provider (IAuthorizationPolicyProvider).
The following scenarios may be a candidate for Custom Authorization Policy Provider.
- Policy evaluation provided by the external service.
- For a large range of policies (i.e., a variety of policies are to provide access. It doesn't make any sense to add every policy to authorization options).
- Create a Policy that uses runtime information to provide access.
Example
In my previous article, I explained how to add an example of the "minimum time spent" policy. Here, I have added a policy that allows access for the employees who have completed 365 days in the company. If different controller actions are available for different days completed by the company employee, it does not make any sense to add many policies to AuthorizationOptions.AddPolicy method. In such a case, we can use IAuthorizationPolicyProvider (custom policy provider).
The IAuthorizationPolicyProvider interface is used to retrieve authorization policies. The DefaultAuthorizationPolicyProvider is registered and used to retrieve authorization policies that are provided by the AuthorizationOptions in an authorization method. We can customize the behavior by doing different implementations of the IAuthorizationPolicyProvider interface. This interface contains the following two APIs.
- GetPolicyAsync: This method returns authorization police for a provided name.
- GetDefaultPolicyAsync: This method returns the default authorization policy that is used by the "Authorize" attribute without specifying any policy.
I have different action methods in the controller and these actions can be accessed by a different group of users i.e. "Method1" can be accessed by the user who completed 365 or more days in the company, "Method2" is accessed by the user who completed 180 or more days in company and "Method3" is accessed by the user who completed 10 or more days in company. This requirement can be achieved by creating a custom authorization policy provider using IAuthorizationPolicyProvider.
Here, I will create a policy runtime and assign this policy to customize the authorized attribute. The authorization policy is identified by the name and the policy name will be generated based on the custom authorize attribute parameter.
I can achieve my requirements using the following four steps.
Step 1. Create a custom Authorize filter
The first step to creating a custom authorize attribute that accepts the number of days as input based on the input value is to generate a policy name and assign the "Policy" property of the base class. So, when executing this filter, it will consider policy rules that are provided to validate the user's access.
In the following example code, I have created the "MinimumTimeSpendAuthorize" attribute, and based on the input it dynamically created a policy name i.e. "MinimumTimeSpend.{input value}" and assigned it to Policy property. This policy can be created and registered in the next step.
using Microsoft.AspNetCore.Authorization;
namespace PolicyBasedAuthorization.Policy
{
public class MinimumTimeSpendAuthorize : AuthorizeAttribute
{
public MinimumTimeSpendAuthorize(int days)
{
NoOfDays = days;
}
int days;
public int NoOfDays
{
get
{
return days;
}
set
{
days = value;
Policy = $"MinimumTimeSpend.{value.ToString()}";
}
}
}
}
Step 2. Create Authorization Requirements and Authorization handler
The Authorization Requirement is the collection of data that can be used to evaluate the user principal and the Authorization handler contains an evaluation mechanism for properties of requirement. One requirement may be associated with multiple handlers.
Here, I have created the requirement for minimum time spent for the organization and created the handler that calculates the of days for an employee by subtracting today's date from claim "DateOfJoing" and if the result is greater than or equal to the supplied data then the user is authorized to access.
Authorization Requirement
using Microsoft.AspNetCore.Authorization;
namespace PolicyBasedAuthorization.Policy
{
public class MinimumTimeSpendRequirement : IAuthorizationRequirement
{
public MinimumTimeSpendRequirement(int noOfDays)
{
TimeSpendInDays = noOfDays;
}
public int TimeSpendInDays { get; private set; }
}
}
Authorization handler
using Microsoft.AspNetCore.Authorization;
using System;
using System.Threading.Tasks;
namespace PolicyBasedAuthorization.Policy
{
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);
}
}
}
Step 3. Create a custom policy provider
In this step, we will create the policy by implementing IAuthorizationPolicyProvider. This interface has two abstract methods: GetDefaultPolicyAsync and GetPolicyAsync. In a GetDefaultPolicyAsync method, I will return all the policies that are provided by the default provider. In a GetPolicyAsync method, I will check if the policy name pattern is following a pattern (i.e. MinimumTimeSpend.{input value}) generated in the custom authorize attribute. If the pattern matches, then I will extract the number of days from the policy name and supplied requirement.
using System;
using System.Threading.Tasks;
using ClaimBasedPolicyBasedAuthorization.Policy;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
namespace PolicyBasedAuthorization.Policy
{
public class MinimumTimeSpendPolicy : IAuthorizationPolicyProvider
{
public DefaultAuthorizationPolicyProvider DefaultPolicyProvider { get; }
public MinimumTimeSpendPolicy(IOptions<AuthorizationOptions> options)
{
DefaultPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
{
return DefaultPolicyProvider.GetDefaultPolicyAsync();
}
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
string[] subStringPolicy = policyName.Split(new char[] { '.' });
if (subStringPolicy.Length > 1 &&
subStringPolicy[0].Equals("MinimumTimeSpend", StringComparison.OrdinalIgnoreCase) &&
int.TryParse(subStringPolicy[1], out var days))
{
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new MinimumTimeSpendRequirement(days));
return Task.FromResult(policy.Build());
}
return DefaultPolicyProvider.GetPolicyAsync(policyName);
}
}
}
Step 4. Register Handler and Policy Provider
The next and final step is to register the authorization handler and policy provider. To use custom policies, we must register an authorization handler that is associated with the requirement used in customs policy and also we need to register a custom policy provider in the ConfigureServices method of the startup class.
public void ConfigureServices(IServiceCollection services)
{
// Add your service registrations here
services.AddTransient<IAuthorizationPolicyProvider, MinimumTimeSpendPolicy>();
services.AddSingleton<IAuthorizationHandler, MinimumTimeSpendHandler>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthorization();
}
To get the desired result, we need to apply a custom authorized filter to the controller action method. In the following example code, "Method1" can be accessed by the user who completed 365 or more days in a company, "Method2" is accessed by the user who completed 180 or more days in a company and "Method3" is accessed by the user who completed 10 or more days in the company.
public class DemoController : Controller
{
[MinimumTimeSpendAuthorize(180)]
public IActionResult TestMethod2()
{
return View("MyPage");
}
[MinimumTimeSpendAuthorize(365)]
public IActionResult TestMethod1()
{
return View("MyPage");
}
[MinimumTimeSpendAuthorize(10)]
public IActionResult TestMethod3()
{
return View("MyPage");
}
}
Summary
Using the method described in this article, we can create custom authorization policy providers using IAuthorizationPolicyProvider. It is very useful when we have a large range of policies or a policy created based on runtime information.
You can view or download the source code from the GitHub link here.