Policies and Requirements

Name policies with AddAuthorizationBuilder, combine claim and role checks, and write custom requirements and handlers for rules the built-ins cannot express.

Syntaxbuilder.Services.AddSingleton<IAuthorizationHandler, ScopeHandler>(); builder.Services.AddAuthorizationBuilder() .AddPolicy("orders:refund", p => p .AddRequirements(new ScopeRequirement("orders.write")) .RequireRole("finance"));

A policy is an AuthorizationPolicy: a list of IAuthorizationRequirement objects plus, optionally, the authentication schemes to use. Requirements are plain data; IAuthorizationHandler implementations decide whether each requirement is met. The built-in builder methods add built-in requirements that handle themselves: RequireAuthenticatedUser, RequireClaim, RequireRole, RequireUserName and RequireAssertion for a one-off lambda.

Registering policies

builder.Services.AddAuthorizationBuilder() returns an AuthorizationBuilder with AddPolicy, SetDefaultPolicy, SetFallbackPolicy and SetInvokeHandlersAfterFailure. It is shorthand for AddAuthorization(o => o.AddPolicy(...)); both write to AuthorizationOptions. Keep policy names in constants: a typo in RequireAuthorization("orders:refnd") compiles, then throws InvalidOperationException on the first request to that endpoint, which the client sees as a 500.

How evaluation decides

IAuthorizationService creates an AuthorizationHandlerContext holding the user, the pending requirements and an optional resource, then runs every registered handler. AuthorizationHandler<TRequirement> only sees requirements of its type.

Handler doesEffect
context.Succeed(requirement)That requirement is met
NothingNo opinion; another handler may still succeed it
context.Fail()Veto: the policy fails whatever other handlers do

The policy passes when every requirement has succeeded and nobody called Fail. Requirements therefore combine with AND, and several handlers for one requirement combine with OR. InvokeHandlersAfterFailure is true by default, so all handlers run even after a veto; turn it off when handlers are expensive. Pass an AuthorizationFailureReason to Fail to explain the decision in logs and to a custom result handler.

Why RequireClaim fails on scopes

RequireClaim(type, values) compares the whole claim value, ordinal and case-sensitive. OAuth access tokens usually carry scopes as one space-separated string, "scope": "orders.read orders.write" (Entra ID uses scp), so RequireClaim("scope", "orders.read") rejects a token that has the scope. Write a small requirement and handler that splits the value, as the demo does. Likewise RequireRole calls ClaimsPrincipal.IsInRole, which only looks at claims whose type equals the identity's RoleClaimType.

Requirements without named policies

An attribute that implements IAuthorizationRequirementData carries its own requirements: [RequireScope("orders.cancel")] on a handler or action needs no policy registered per scope. For truly dynamic names, such as perm:orders.refund, implement IAuthorizationPolicyProvider and fall back to DefaultAuthorizationPolicyProvider for everything else.

Handler lifetime and failure modes

Handlers are resolved from DI each time authorization runs. Register pure logic as a singleton and a handler that needs a DbContext as scoped. A handler that throws turns into a 500, a handler that queries the database runs on every request it guards, and a handler that calls Fail where it meant "not my case" silently blocks another handler's legitimate path.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Accept a token whose scope claim is "orders.read orders.write" for GET /orders, which RequireClaim("scope", "orders.read") would reject
  • Let either the finance team or a store manager with a current delegation approve a refund over 500.00 EUR
  • Block every action for a staff account flagged as suspended, regardless of the roles in its token

More examples

A handler that needs a scoped service

A handler that depends on a scoped service must itself be registered as scoped; as a singleton it would capture the first request's <code>DbContext</code> and scope validation would reject it in Development. When authorization runs from the middleware, <code>context.Resource</code> is the <code>HttpContext</code>, which gives the handler <code>RequestAborted</code>. This check costs a query per request, so cache the credit-hold flag for a short time if the endpoint is hot.

Example · csharp
using Microsoft.AspNetCore.Authorization;

namespace Shop.Api.Authorization;

public interface ICustomerAccounts
{
    Task<bool> IsOnCreditHoldAsync(string customerId, CancellationToken cancellationToken);
}

public sealed class NotOnCreditHoldRequirement : IAuthorizationRequirement;

public sealed class NotOnCreditHoldHandler(ICustomerAccounts accounts) : AuthorizationHandler<NotOnCreditHoldRequirement>
{
    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, NotOnCreditHoldRequirement requirement)
    {
        var customerId = context.User.FindFirst("sub")?.Value;
        if (customerId is null) return;

        // For endpoint authorization the resource is the HttpContext.
        var cancellationToken = (context.Resource as HttpContext)?.RequestAborted ?? CancellationToken.None;
        if (!await accounts.IsOnCreditHoldAsync(customerId, cancellationToken))
            context.Succeed(requirement);
    }
}

public static class CheckoutAuthorization
{
    public const string PlaceOrder = "orders:place";

    public static IServiceCollection AddCheckoutAuthorization(this IServiceCollection services)
    {
        // Scoped, because ICustomerAccounts wraps the request's DbContext.
        services.AddScoped<IAuthorizationHandler, NotOnCreditHoldHandler>();
        services.AddAuthorizationBuilder()
            .AddPolicy(PlaceOrder, p => p
                .RequireAuthenticatedUser()
                .AddRequirements(new NotOnCreditHoldRequirement()));
        return services;
    }
}

Several handlers for one requirement, and a veto

Either of the first two handlers is enough, because each only calls <code>Succeed</code> and stays silent otherwise. <code>SuspendedStaffHandler</code> implements <code>IAuthorizationHandler</code> directly, so it runs for every policy evaluation, and its <code>Fail</code> vetoes all of them. Injecting <code>TimeProvider</code> keeps the expiry check testable with a fake clock.

Example · csharp
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;

namespace Shop.Api.Authorization;

public sealed class RefundApproverRequirement : IAuthorizationRequirement;

// Path 1: members of the finance team may approve refunds.
public sealed class FinanceTeamHandler : AuthorizationHandler<RefundApproverRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RefundApproverRequirement requirement)
    {
        if (context.User.IsInRole("finance")) context.Succeed(requirement);
        return Task.CompletedTask;
    }
}

// Path 2: a store manager holding a time-limited delegation.
public sealed class DelegatedApproverHandler(TimeProvider clock) : AuthorizationHandler<RefundApproverRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RefundApproverRequirement requirement)
    {
        var until = context.User.FindFirst("refund_delegation_until")?.Value;
        if (DateTimeOffset.TryParse(until, out var expires) && expires > clock.GetUtcNow())
            context.Succeed(requirement);
        return Task.CompletedTask;
    }
}

// Veto: a suspended staff account fails the policy whatever the other handlers decided.
public sealed class SuspendedStaffHandler : IAuthorizationHandler
{
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        if (context.User.HasClaim("account_status", "suspended"))
            context.Fail(new AuthorizationFailureReason(this, "Staff account is suspended."));
        return Task.CompletedTask;
    }
}

public static class RefundAuthorization
{
    public static IServiceCollection AddRefundAuthorization(this IServiceCollection services)
    {
        services.AddSingleton(TimeProvider.System);
        services.AddSingleton<IAuthorizationHandler, FinanceTeamHandler>();
        services.AddSingleton<IAuthorizationHandler, DelegatedApproverHandler>();
        services.AddSingleton<IAuthorizationHandler, SuspendedStaffHandler>();
        services.AddAuthorizationBuilder()
            .AddPolicy("RefundApprover", p => p.AddRequirements(new RefundApproverRequirement()));
        return services;
    }
}

Dynamic permission policies with a policy provider

There is only one <code>IAuthorizationPolicyProvider</code>, so a custom one must delegate everything it does not recognise to <code>DefaultAuthorizationPolicyProvider</code>, or named policies, the default policy and the fallback policy all disappear. Build the policy from the name alone and keep it cheap: the middleware may ask for it on every request.

Example · csharp
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;

namespace Shop.Api.Authorization;

// Handles RequireAuthorization("perm:orders.refund") without registering a policy per permission.
public sealed class PermissionPolicyProvider(IOptions<AuthorizationOptions> options) : IAuthorizationPolicyProvider
{
    public const string Prefix = "perm:";
    private readonly DefaultAuthorizationPolicyProvider _fallback = new(options);

    public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
    {
        if (!policyName.StartsWith(Prefix, StringComparison.Ordinal))
            return _fallback.GetPolicyAsync(policyName);

        var policy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .RequireClaim("permission", policyName[Prefix.Length..])
            .Build();
        return Task.FromResult<AuthorizationPolicy?>(policy);
    }

    public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => _fallback.GetDefaultPolicyAsync();

    public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() => _fallback.GetFallbackPolicyAsync();
}

public static class PermissionAuthorization
{
    public static IServiceCollection AddPermissionPolicies(this IServiceCollection services)
    {
        services.AddAuthorization();
        services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
        return services;
    }
}

Discussion

  • Be the first to comment on this lesson.