Authorisation Policies and Requirements in ASP.NET Core
Authentication tells you who the user is. Authorisation tells you what they can do. ASP.NET Core's policy-based authorisation system goes well beyond simple role checks, letting you express complex access rules through requirements and handlers.
Simple Policies
The simplest policies check for claims:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("PremiumUser", policy =>
policy.RequireClaim("subscription", "premium", "enterprise"));
options.AddPolicy("Over18", policy =>
policy.RequireAssertion(context =>
{
var dobClaim = context.User.FindFirst("date_of_birth");
if (dobClaim is null || !DateOnly.TryParse(dobClaim.Value, out var dob))
return false;
return dob.AddYears(18) <= DateOnly.FromDateTime(DateTime.UtcNow);
}));
});
Apply policies to endpoints:
app.MapGet("/api/admin/users", GetUsers).RequireAuthorization("AdminOnly");
// Or with controllers:
[Authorize(Policy = "PremiumUser")]
[HttpGet("premium-content")]
public IActionResult GetPremiumContent() => Ok("Premium content here");
Custom Requirements and Handlers
For complex logic, define a requirement and a handler. A requirement is a marker class; the handler contains the logic:
public class MinimumTenureRequirement : IAuthorizationRequirement
{
public int MinimumMonths { get; }
public MinimumTenureRequirement(int minimumMonths)
{
MinimumMonths = minimumMonths;
}
}
public class MinimumTenureHandler : AuthorizationHandler<MinimumTenureRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumTenureRequirement requirement)
{
var joinedClaim = context.User.FindFirst("joined_date");
if (joinedClaim is not null
&& DateTime.TryParse(joinedClaim.Value, out var joinedDate)
&& joinedDate.AddMonths(requirement.MinimumMonths) <= DateTime.UtcNow)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Register the handler and use the requirement in a policy:
builder.Services.AddSingleton<IAuthorizationHandler, MinimumTenureHandler>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("LongTermUser", policy =>
policy.AddRequirements(new MinimumTenureRequirement(12)));
});
Multiple Handlers for One Requirement
A requirement can have multiple handlers. If any handler succeeds, the requirement is met. This is useful for OR logic:
public class DocumentAccessRequirement : IAuthorizationRequirement { }
// Handler 1: Owners can always access
public class DocumentOwnerHandler : AuthorizationHandler<DocumentAccessRequirement, Document>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DocumentAccessRequirement requirement,
Document document)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (userId == document.OwnerId)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
// Handler 2: Admins can access everything
public class AdminDocumentHandler : AuthorizationHandler<DocumentAccessRequirement, Document>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DocumentAccessRequirement requirement,
Document document)
{
if (context.User.IsInRole("Admin"))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Resource-Based Authorisation
Sometimes you need the resource itself to make an authorisation decision. Inject IAuthorizationService and pass the resource:
[ApiController]
[Route("api/documents")]
public class DocumentsController : ControllerBase
{
private readonly IAuthorizationService _authService;
private readonly IDocumentRepository _repository;
public DocumentsController(IAuthorizationService authService, IDocumentRepository repository)
{
_authService = authService;
_repository = repository;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var document = await _repository.GetByIdAsync(id);
if (document is null)
return NotFound();
var result = await _authService.AuthorizeAsync(User, document, new DocumentAccessRequirement());
if (!result.Succeeded)
return Forbid();
return Ok(document);
}
}
Handlers with Dependency Injection
Handlers are registered in DI, so they can inject services:
public class FeatureFlagHandler : AuthorizationHandler<FeatureFlagRequirement>
{
private readonly IFeatureFlagService _featureFlags;
public FeatureFlagHandler(IFeatureFlagService featureFlags)
{
_featureFlags = featureFlags;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
FeatureFlagRequirement requirement)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (userId is not null && await _featureFlags.IsEnabledAsync(requirement.FeatureName, userId))
{
context.Succeed(requirement);
}
}
}
Policies with Multiple Requirements
All requirements in a policy must be satisfied (AND logic):
options.AddPolicy("CanDeleteUsers", policy =>
{
policy.RequireRole("Admin");
policy.AddRequirements(new MinimumTenureRequirement(6));
policy.RequireClaim("mfa_verified", "true");
});
This policy requires the user to be an admin, have been a member for at least 6 months, AND have completed MFA verification.
Fallback and Default Policies
Set a default policy for all [Authorize] attributes without a specified policy:
builder.Services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireClaim("email_verified", "true")
.Build();
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
The FallbackPolicy applies to endpoints that don't have any authorisation metadata — effectively making your entire application require authentication by default.
Key Points
- Policies combine requirements with AND logic; multiple handlers per requirement give you OR logic.
- Use
IAuthorizationServicefor resource-based authorisation where the decision depends on the specific resource. - Handlers participate in DI, so they can access databases, feature flags, or any other service.
- Set a
FallbackPolicyto require authentication by default — it's safer to opt out of security than opt in. - Never put authorisation logic in controllers. Express it as requirements and handlers, then apply policies declaratively.
The policy system is one of ASP.NET Core's strongest security features. It separates the what (policies on endpoints) from the how (handlers with logic), making authorisation rules testable, composable, and maintainable.