Authorization policies play a major role in modern ASP.NET Core authorization. We generally register authorization policies by giving them a name and specifying their requirements while configuring our startup application. We’ve covered how to do that in the previous article. In more advanced cases, however, we can programmatically fetch and provide an authorization policy for a particular policy name. In this article, let’s learn how to write a custom authorization policy provider in ASP.NET Core to do just that.
Let’s start.
What Are Authorization Policies?
In short, an AuthorizationPolicy
is a list of requirements and the authentication scheme to use with them (for retrieving the user’s claims, and, if authorization fails, selecting which Challenge and Forbid page to use).
Let’s say we have an AuthorizationPolicy
to control access to a cash register, and we use cookie authentication to authenticate employees.
In our AuthorizationPolicy
, we will define authorization requirements to ensure that we have a logged-in employee and that they have access to the cash register. We’ll also define the authentication scheme as cookie authentication so that the framework will direct them appropriately to the login page if they try to access forbidden pages.
We normally add these policies in our call to IServiceCollection.AddAuthorization
:
builder.Services.AddAuthorization(options => { options.AddPolicy("CanAccessCashRegister", policy => { policy.RequireAuthenticatedUser(); // Requirement definition omitted for brevity. policy.Requirements.Add(new HasAccessToCashRegisterRequirement()); }); });
We haven’t explicitly specified the authentication scheme as cookie authentication. If we have a default authentication scheme specified when calling AddAuthentication
, the framework will use that.
How ASP.NET Core Fetches Authorization Policies
When we authorize against a policy by its name (such as when decorating an endpoint with AuthorizeAttribute
), it might seem that the framework simply looks it up by its name from a list of configured policies.
In reality, though, it’s a little more involved than that. The framework relies on classes that implement IAuthorizationPolicyProvider
to fetch policies by their name.
The most important method in this interface is GetPolicyAsync()
. This method takes in the name of the policy as a parameter, and it asynchronously returns the AuthorizationPolicy
associated with that name. If it isn’t able to find any such policy, it returns null
.
ASP.NET Core ships with a default IAuthorizationPolicyProvider
, aptly named DefaultAuthorizationPolicyProvider
, that looks up and resolves policies that were registered in AuthorizationOptions
(when configuring authorization).
How To Write A Custom Authorization Policy Provider
One reason to write our custom authorization policy provider in ASP.NET Core using IAuthorizationPolicyProvider
could be that we want to fetch policies dynamically.
Let’s say we’re an airline that operates a loyalty program. Certain actions, such as priority check-in, might require a baseline membership tier and a minimum number of loyalty points. Our company regularly updates these requirements, so we want to fetch these from a database.
Remember that the framework fetches policies based on their name. We could prefix all loyalty-based requests with Loyalty:
and the associated action name with which we’ve stored the requirements. Our policy name for priority check-in could be Loyalty:PriorityCheckIn
.
Let’s write an IAuthorizationPolicyProvider
implementation to filter for these requests and construct an AuthorizationPolicy
from the requirements in the database:
public class LoyaltyProgramAuthorizationPolicyProvider : IAuthorizationPolicyProvider { private const string POLICY_PREFIX = "Loyalty:"; private readonly ILoyaltyRequirementsRepository LoyaltyRequirementsRepo { get; } public LoyaltyProgramAuthorizationPolicyProvider(ILoyaltyRequirementsRepository loyaltyRequirementsRepo) { LoyaltyRequirementsRepo = loyaltyRequirementsRepo; } public async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName) { if (!policyName.StartsWith(POLICY_PREFIX)) return null; var actionName = policyName[POLICY_PREFIX.Length..]; var requirements = await LoyaltyRequirementsRepo.GetByActionNameAsync(actionName); if (requirements is null) return null; var policyBuilder = new AuthorizationPolicyBuilder(CookieAuthenticationDefaults.AuthenticationScheme); policyBuilder.AddRequirements(new BaselineMembershipTierRequirement(requirements.BaselineMembershipTier)); policyBuilder.AddRequirements(new MinimumLoyaltyPointsRequirement(requirements.MinimumLoyaltyPoints)); return policyBuilder.Build(); } }
We first check if this policy is a loyalty policy by making sure the policy name begins with our prefix Loyalty:
. If it isn’t, we’ll return null
to signify that no policy was found. In some cases, we might want to delegate to another IAuthorizationPolicyProvider
instead—we’ll do that in a bit.
Next, we extract the name of the action to look up loyalty requirements in the database using a repository (we won’t focus on the repository’s implementation in this case). Because the framework looks up authorization policies by their name, we must embed any parameters into the policy name.
Once we have the requirements, we construct an AuthorizationPolicy
using an AuthorizationPolicyBuilder
.
We have to specify an authentication scheme that populates the user and controls the Challenge and Forbid pages—these are called when the user isn’t authenticated or isn’t authorized, respectively. We also add authorization requirements—classes implementing the IAuthorizationRequirement
marker interface—to the policy (we won’t go into the requirement and handler implementation because that’s part of authorization in general rather than authorization policy providers).
Finally, we build the policy and return the resulting AuthorizationPolicy
.
Default and Fallback Policies
Besides resolving policies by their name, IAuthorizationPolicyProvider
implementations can also resolve default and fallback policies.
The default policy is the one that the framework uses when we authorize a user without specifying a policy name. This policy usually requires a signed-in user only.
Let’s implement the default policy to do that:
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() { var policyBuilder = new AuthorizationPolicyBuilder(CookieAuthenticationDefaults.AuthenticationScheme); policyBuilder.RequireAuthenticatedUser(); return Task.FromResult(policyBuilder.Build()); }
The fallback policy, on the other hand, applies when we don’t explicitly authorize a user. In most cases, we don’t need any requirements in that case—because of this, we can simply return null
instead of an AuthorizationPolicy
:
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() => Task.FromResult<AuthorizationPolicy?>(null)
Delegate Policy Resolution to Another Authorization Policy Provider
Our policy provider currently returns null
for policies that don’t begin with our prefix of Loyalty:
. Since we’ll likely be using other authorization policies as well (besides those for our loyalty program), this probably isn’t the behavior we want.
Instead of returning null
when a policy doesn’t match our filter, let’s instead defer the request to another IAuthorizationPolicyProvider
implementation.
We’ll start by adding another field to point to the backup authorization policy provider:
private readonly ILoyaltyRequirementsRepository _loyaltyRequirementsRepository; private readonly IAuthorizationPolicyProvider _backupAuthorizationPolicyProvider; public LoyaltyProgramAuthorizationPolicyProvider( ILoyaltyRequirementsRepository loyaltyRequirementsRepository, IOptions<AuthorizationOptions> authorizationOptions ) { _loyaltyRequirementsRepository = loyaltyRequirementsRepository; _backupAuthorizationPolicyProvider = new DefaultAuthorizationPolicyProvider(authorizationOptions); }
Then, instead of returning null
in GetPolicyAsync
, let’s defer to the backup provider:
if (!policyName.StartsWith(POLICY_PREFIX)) return await _backupAuthorizationPolicyProvider.GetPolicyAsync(policyName);
With this, any authorization policy that doesn’t start with Loyalty:
will be handled by the default authorization policy provider as normal.
If we like, we can do the same thing with default and fallback policies.
How to Register the Authorization Policy Provider
Finally, for the framework to use our authorization policy provider, let’s register it as an IAuthorizationPolicyProvider
in our service container:
builder.Services.AddSingleton<IAuthorizationPolicyProvider, LoyaltyProgramAuthorizationPolicyProvider>();
Using Custom Authorization Policy Provider
After implementing our custom authorization policy provider, we can use it to authorize any action that we want.
Authorizing Against the Policy by Name
Let’s use our policy provider by annotating an action with AuthorizeAttribute
and specifying the dynamic policy name:
[Authorize("Loyalty:PriorityCheckIn")] public IActionResult PriorityCheckIn() { return View(); }
When the user visits the priority check-in page, the framework will fetch the Loyalty:PriorityCheckIn
policy using our IAuthorizationPolicyProvider
implementation and authorize against the AuthorizationPolicy
it returns.
Simpler Annotation with Custom Attributes
Instead of using the standard AuthorizeAttribute
and manually constructing our policy name, we can abstract that away in a simple custom attribute that derives from AuthorizeAttribute
:
public class LoyaltyAuthorizeAttribute : AuthorizeAttribute { private const string POLICY_PREFIX = "Loyalty:"; public LoyaltyAuthorizeAttribute(string actionName) : base(string.Concat(POLICY_PREFIX, actionName)) { } }
The constructor takes in the action name, prepends the policy prefix Loyalty:
, and, finally, invokes the base AuthorizeAttribute
constructor with the new name we construct.
Let’s use this to simplify the attribute annotating our action:
[LoyaltyAuthorize("PriorityCheckIn")] public IActionResult PriorityCheckIn() { return View(); }
Conclusion
ASP.NET Core uses a modular mechanism to resolve authorization policies by their name. While the default authorization policy provider suffices for almost all use cases, there are times when we can take advantage of this modularity to supply our provider—especially if we have dynamic requirements we want to authorize against.
We explored how to implement a custom authorization policy provider in ASP.NET Core and how to use the custom policies to authorize users.