Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
By Mike Rousos
This article describes how to implement the IAuthorizationPolicyProvider interface to create a custom authorization policy provider, including how to create an authorization attribute for the provider.
For a typical implementation of policy-based authorization, policies are registered by calling AuthorizationOptions.AddPolicy during authorization service configuration. Sometimes, it isn't possible or desirable to register many authorization policies in this manner.
For example, an app might require policy-based checks for building room numbers or user ages, where it doesn't make sense to create policies for building room numbers or ages with many AddPolicy calls. These scenarios are best implemented by passing a parameter that represents room numbers or ages to a custom [Authorize] attribute backed by a custom authorization policy provider. The custom policy provider receives the parameter value and dynamically creates a single policy to determine if authorization requirements are met. Using this approach, you avoid creating dozens or even hundreds of individual, explicit authorization policies.
Other scenarios where a custom policy provider is useful include:
- To dynamically, flexibly determine authorization requirements based on complex logic.
- To create policies at runtime based on information from an external data source, such as a database.
- When an external service is used to provide policy evaluation.
Sample app
The Blazor Web App sample for this article is the BlazorWebAppAuthorization sample app (dotnet/AspNetCore.Docs.Samples GitHub repository) (how to download). The sample app uses seeded accounts to demonstrate the examples in this article. For more information, see the sample's README file (README.md).
Caution
This sample app uses an in-memory database to store user information, which isn't suitable for production scenarios. The sample app is intended for demonstration purposes only and shouldn't be used as a starting point for production apps.
For an MVC sample, see the CustomPolicyProvider sample in the dotnet/aspnetcore GitHub repository.
Tip
Use the git sparse-checkout command to download a single folder from the main branch of a GitHub repository.
In the following example, the security/authorization/BlazorWebAppAuthorization subfolder is downloaded from the dotnet/AspNetCore.Docs.Samples repository. Replace https://github.com/dotnet/AspNetCore.Docs.Samples.git with the URL of the repository that you want to clone, and replace the security/authorization/BlazorWebAppAuthorization path with the path to the subfolder that you want to download:
git clone --depth 1 --filter=blob:none https://github.com/dotnet/AspNetCore.Docs.Samples.git --sparse
cd AspNetCore.Docs.Samples
git sparse-checkout init --cone
git sparse-checkout set security/authorization/BlazorWebAppAuthorization
Customize policy retrieval
The developer decides in advance how custom policies are named by inventing a naming scheme in a string format that's easily parsed to meet one or more requirements for each policy evaluation:
- The custom authorization attribute adopts the policy naming scheme.
- The custom policy provider adopts the same policy naming scheme.
For example, consider a minimum age naming scheme in the format MinimumAge{AGE}, where the {AGE} placeholder is any given age, for example, MinimumAge21. This naming scheme is easily identified by its prefix "MinimumAge" with an easily parsed string-based age ("21").
The developer customizes how authorization policies are provided by implementing the following APIs in the custom policy provider:
- The GetPolicyAsync method returns an authorization policy for a given name.
- The GetDefaultPolicyAsync method returns the default authorization policy. The AuthorizationOptions.DefaultPolicy applies whenever authorization is required, but no specific policy is set. If an
[Authorize]attribute is present without a policy name, the default policy is used instead of the fallback policy. This behavior ensures that endpoints explicitly requesting authorization (via[Authorize]orRequireAuthorization()) default to a secure policy. - The GetFallbackPolicyAsync method returns the Microsoft.AspNetCore.Authorization.AuthorizationOptions.FallbackPolicy when no authorization metadata (for example, no
[Authorize]attribute orRequireAuthorization()) is explicitly provided for a resource. The fallback policy only applies when there are no authorization attributes or explicit policies set. If a resource has an[Authorize]attribute (even without a policy name), the default policy is used instead of the fallback policy. This means fallback policy is mainly relevant for middleware-based authorization flows where no per-endpoint authorization is specified. By default, fallback policy isnull, meaning it has no effect unless explicitly set.
The general format of a custom policy provider that either returns a policy for a given matching name (represented by the {IDENTIFY POLICY BY NAME} placeholder) or returns null when no policy name matches is similar to the following.
Implementing the required default and fallback policies for a custom policy provider (GetDefaultPolicyAsync, GetFallbackPolicyAsync) are omitted in the following example for brevity but are described and shown in a complete example later in this article.
// Omitted for brevity:
// 'GetDefaultPolicyAsync' (default policy)
// 'GetFallbackPolicyAsync' (fallback policy)
internal class CustomPolicyProvider() : IAuthorizationPolicyProvider
{
public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
if ({IDENTIFY POLICY BY NAME})
{
var policy = new AuthorizationPolicyBuilder(
IdentityConstants.ApplicationScheme);
policy.AddRequirements(...);
return Task.FromResult<AuthorizationPolicy?>(policy.Build());
}
return Task.FromResult<AuthorizationPolicy?>(null);
}
}
Note
Use of IdentityConstants.ApplicationScheme in the preceding example represents the scheme used to identify application authentication cookies. An empty AuthorizationPolicyBuilder.AuthenticationSchemes list evaluates requirements against the default schemes—it doesn't authenticate every registered scheme.
ASP.NET Core only uses one instance of IAuthorizationPolicyProvider. DefaultAuthorizationPolicyProvider is the framework's default IAuthorizationPolicyProvider implementation for retrieving authorization policies by name. Policy provider behavior is customized by registering a custom policy provider implementation in the app's service container to replace the default implementation.
If a custom policy provider is able to explicitly match and return all of the authorization policies that the app uses, the policy provider can return Task.FromResult<AuthorizationPolicy>(null) from the GetPolicyAsync method when no policy name matches. However, most apps that implement a custom provider defer traditional policy retrieval, for example to handle role-based and claim-based policies, to the default policy provider. Such an app typically uses a custom provider that:
- Attempts to parse policy names, returning an authorization policy for a matching name with one or more requirements or assertions.
- Uses the framework's DefaultAuthorizationPolicyProvider by implementing GetDefaultPolicyAsync to provide an authorization policy for
[Authorize]attributes that don't specify a policy name:
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() =>
DefaultPolicyProvider.GetDefaultPolicyAsync();
Any of the following AuthorizationPolicyBuilder methods can be used for the custom policy provider's requirements and assertions in its GetPolicyAsync method:
The custom policy provider must also implement GetFallbackPolicyAsync. Choosing a non-null fallback is optional. The following example retrieves the fallback authorization policy by delegating to the default authorization policy provider's implementation:
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() =>
DefaultPolicyProvider.GetFallbackPolicyAsync();
The default policy or fallback policy can combine policies, which is useful, for example, when middleware-based authorization flows don't specify per-endpoint authorization. All combined requirements must succeed for the combined policy to succeed.
In the following example, the policy combines:
- A standard user policy that requires an authenticated user with a
Statusclaim ofActive. - A manager policy that requires the
Managerrole with aDepartmentclaim ofSales.
var standardUserPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireClaim("Status", "Active")
.Build();
var managerPolicy = new AuthorizationPolicyBuilder()
.RequireRole("Manager")
.RequireClaim("Department", "Sales")
.Build();
var combinedPolicy = AuthorizationPolicy.Combine(standardUserPolicy, managerPolicy);
return Task.FromResult<AuthorizationPolicy?>(combinedPolicy);
Custom authorization attribute
The recommended approach for applying policies in concert with a custom policy provider is to use a strongly-typed AuthorizeAttribute. A custom attribute implementation maps arguments into a string that's used to retrieve a corresponding authorization policy.
The following MinimumAgeAuthorizeAttribute example derives from AuthorizeAttribute and makes the Age property wrap the AuthorizeAttribute.Policy property. The attribute type has a policy string based on the hard-coded prefix (MinimumAge) and an integer passed in via its constructor (MinimumAge{AGE}), where the {AGE} placeholder is the minimum age, for example, MinimumAge21. The following attribute is used with the custom policy provider shown later in this article in the Minimum age custom policy provider example section.
Policies/Attributes/MinimumAgeAuthorizeAttribute.cs:
using Microsoft.AspNetCore.Authorization;
namespace BlazorWebAppAuthorization.Policies.Attributes;
public class MinimumAgeAuthorizeAttribute : AuthorizeAttribute
{
private const string PolicyPrefix = "MinimumAge";
public MinimumAgeAuthorizeAttribute(int age) => Age = age;
public int Age
{
get
{
if (!string.IsNullOrEmpty(Policy) &&
Policy.StartsWith(PolicyPrefix,
StringComparison.OrdinalIgnoreCase) &&
int.TryParse(Policy.AsSpan(PolicyPrefix.Length), out var age))
{
return age;
}
return default;
}
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
Policy = $"{PolicyPrefix}{value}";
}
}
}
You can apply the attribute for any given authorized minimum age with an integer parameter for the age. Examples are shown later in this article in the Use policies from a custom policy provider section.
Important
As with all policy-based authorization scenarios, create a requirement and an authorization handler for the policy. Register the handler in the app's service container.
For examples that work with the custom policy provider in this article, see the parameterized MinimumAgeRequirement and MinimumAgeHandler code in the Policy-based authorization article, which work with the MinimumAgePolicyProvider demonstrated in this article:
Minimum age custom policy provider example
Consider a situation where authorization is based on a user's minimum age and the authorization policy names follow the pattern MinimumAge{AGE}, where the {AGE} placeholder is a string representation of an integer age. This is the same naming scheme established for the MinimumAgeAuthorizeAttribute earlier in this article in the Custom authorization attribute section.
The custom policy provider should generate authorization policies by completing the following tasks:
- The age is parsed from the policy name.
- An authorization policy builder (AuthorizationPolicyBuilder) creates a new AuthorizationPolicy.
- The authorization policy builder is constructed with at least one authorization scheme name or always succeeds. Otherwise, there's no information on how to provide a challenge to the user and an exception is thrown.
- Set the list of authentication schemes in AuthorizationPolicyBuilder.AuthenticationSchemes for the built policy. The following example passes IdentityConstants.ApplicationScheme, which represents the scheme used to identify application authentication cookies. An empty (unassigned) AuthenticationSchemes list evaluates requirements against the custom policy provider's default schemes—it doesn't authenticate every registered scheme.
- Add one or more requirements to the policy for user age evaluations with AddRequirements.
The following example demonstrates a minimum age custom policy provider.
Policies/Providers/MinimumAgePolicyProvider.cs:
using BlazorWebAppAuthorization.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
namespace BlazorWebAppAuthorization.Policies.Providers;
public class MinimumAgePolicyProvider(IOptions<AuthorizationOptions> options)
: IAuthorizationPolicyProvider
{
private const string PolicyPrefix = "MinimumAge";
private DefaultAuthorizationPolicyProvider DefaultPolicyProvider { get; } =
new(options);
public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith(
PolicyPrefix, StringComparison.OrdinalIgnoreCase) &&
int.TryParse(policyName.AsSpan(PolicyPrefix.Length), out var age) &&
age >= 0)
{
var policy = new AuthorizationPolicyBuilder(
IdentityConstants.ApplicationScheme);
policy.AddRequirements(new MinimumAgeRequirement(age));
return Task.FromResult<AuthorizationPolicy?>(policy.Build());
}
return DefaultPolicyProvider.GetPolicyAsync(policyName);
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() =>
DefaultPolicyProvider.GetDefaultPolicyAsync();
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() =>
DefaultPolicyProvider.GetFallbackPolicyAsync();
}
Use policies from a custom policy provider
To use custom policies:
As with all policy-based authorization scenarios, create a requirement and an authorization handler for the policy. Register the handler in the app's service container.
For examples that work with the custom policy provider in this article, see the parameterized
MinimumAgeRequirementandMinimumAgeHandlercode in the Policy-based authorization article, which work with theMinimumAgePolicyProviderdemonstrated in this article:
Register the custom policy provider type to replace the default policy provider.
In the app's
Programfile:builder.Services.AddSingleton<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();
Register the custom policy provider type to replace the default policy provider.
In
Startup.ConfigureServicesof theStartup.csfile:services.AddSingleton<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();
The sample app institutes application cookie redirect mapping with the following code:
- Anonymous Users: When an unauthenticated user requests a protected endpoint, the cookie handler issues a challenge and redirects to CookieAuthenticationOptions.LoginPath with a return URL parameter.
- Underage Users: When an authenticated user fails the custom minimum age policy, the handler issues a forbid response and redirects to CookieAuthenticationOptions.AccessDeniedPath instead of the login page.
builder.Services.ConfigureApplicationCookie(options =>
{
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Account/AccessDenied";
});
For demonstration purposes, an AuthorizeView component can specify the weakly-typed MinimumAge21 ("MinimumAge" + Age) policy, as the following sample Razor component demonstrates. Using a weakly-typed policy name isn't the best approach for applying a custom authorization policy. After the following example, a strongly-typed AuthorizeAttribute is demonstrated using the MinimumAgeAuthorizeAttribute implementation described in the Custom authorization attribute section.
Components/Pages/PassMinimumAge21Policy.razor:
@page "/pass-minimumage21-policy"
<h1>Pass 'MinimumAge21' policy (weakly-typed approach)</h1>
<p>
Uses an AuthorizeView component to apply the policy using the policy's name.
This approach is shown for demonstration purposes and isn't recommended for
production code.
</p>
<AuthorizeView Policy="MinimumAge21">
<Authorized>
<p>You satisfy the 'MinimumAge21' policy.</p>
</Authorized>
<NotAuthorized>
<p>You <b>don't</b> satisfy the 'MinimumAge21' policy.</p>
</NotAuthorized>
</AuthorizeView>
The following component uses the strongly-typed custom MinimumAgeAuthorizeAttribute implementation described in the Custom authorization attribute section. Using a strongly-typed attribute is recommended for production apps.
Components/Pages/PassMinimumAge21PolicyWithAttribute.razor:
@page "/pass-minimumage21-policy-with-attribute"
@using BlazorWebAppAuthorization.Policies.Attributes
@attribute [MinimumAgeAuthorize(21)]
<h1>Pass 'MinimumAge21' policy (strongly-typed approach)</h1>
<p>
Applies the policy to the Razor component with a custom
[MinimumAgeAuthorize] attribute (derived from AuthorizeAttribute).
This approach is preferred for production code, as it's strongly-typed
and avoids the use of a string to set the policy and minimum age.
</p>
<p>You satisfy the 'MinimumAge21' policy.</p>
The same approach is useful for securing Minimal API endpoints:
app.MapGet("/must-be-21", [MinimumAgeAuthorize(21)] () =>
"This endpoint requires a 21-year-old birthdate claim.");
Additional resources
ASP.NET Core