Role-Based Authorization
The simplest form of authorization in ASP.NET Core is role-based, using [Authorize(Roles = "Admin")] on a controller or action, which checks whether the current ClaimsPrincipal has a role claim matching one of the listed roles (comma-separated roles are OR'd together, while stacking multiple [Authorize(Roles=...)] attributes ANDs them). Roles typically come from IdentityRole records assigned via UserManager.AddToRoleAsync, or from role claims embedded directly in a JWT for token-based APIs, and the AuthorizationMiddleware evaluates them after authentication has already populated HttpContext.User.
Cricket analogy: [Authorize(Roles = "Captain")] is like an umpire only accepting a review request from the on-field captain — the role check is a simple membership test: are you on the list of people allowed to make this specific call?
Policy-Based Authorization
Policies decouple the authorization rule from the attribute by letting you register named requirements in AddAuthorization(options => options.AddPolicy("MinimumAge", policy => policy.Requirements.Add(new MinimumAgeRequirement(18)))), then apply them with [Authorize(Policy = "MinimumAge")]. Each policy is backed by one or more IAuthorizationRequirement objects evaluated by a matching AuthorizationHandler<TRequirement>, which inspects the ClaimsPrincipal (or even the resource being accessed, for resource-based authorization) and calls context.Succeed(requirement) or leaves it unfulfilled, giving you arbitrary logic beyond a simple role string comparison.
Cricket analogy: A custom policy is like a match referee applying the DLS method for a rain-affected chase — instead of a blunt yes/no role check, a dedicated handler runs a specific calculation (requirement logic) against the actual situation.
Claims-Based Checks and Requirement Handlers
A requirement class implements IAuthorizationRequirement and typically carries the parameters needed for the check (like a minimum age or a required permission string), while the paired handler inherits AuthorizationHandler<TRequirement> and overrides HandleRequirementAsync, inspecting context.User.Claims to find and validate the relevant claim before calling context.Succeed(requirement). You register the handler with services.AddScoped<IAuthorizationHandler, MinimumAgeHandler>() (or AddSingleton if it has no per-request dependencies), and multiple handlers for the same requirement type can each independently succeed it, since ASP.NET Core authorization is inherently OR-based per requirement but AND-based across all requirements in a policy.
Cricket analogy: A requirement handler inspecting claims is like a match referee checking a specific certification claim on a bowler's action report before allowing them to bowl — it looks for one precise fact and validates it against a threshold.
// Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("MinimumAge", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
options.AddPolicy("CanManageOrders", policy =>
policy.RequireClaim("permission", "orders.manage"));
});
builder.Services.AddScoped<IAuthorizationHandler, MinimumAgeHandler>();
// MinimumAgeRequirement.cs
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge) => MinimumAge = minimumAge;
}
// MinimumAgeHandler.cs
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
{
var dobClaim = context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);
if (dobClaim != null && DateTime.TryParse(dobClaim.Value, out var dob))
{
var age = DateTime.Today.Year - dob.Year;
if (age >= requirement.MinimumAge)
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
// OrdersController.cs
[Authorize(Policy = "CanManageOrders")]
[HttpDelete("{id}")]
public IActionResult CancelOrder(int id) => Ok();For per-resource decisions (e.g., "can this user edit THIS specific document"), use resource-based authorization via IAuthorizationService.AuthorizeAsync(User, resource, policyName) inside your action method, since attribute-based [Authorize] alone can't inspect the loaded entity.
Combining Requirements and Fallback Policies
A single policy can combine multiple requirement types — RequireClaim, RequireRole, and custom IAuthorizationRequirement instances all added to the same AuthorizationPolicyBuilder — and all of them must succeed for the policy to pass, since policy evaluation is AND-based across distinct requirements even though multiple handlers for the same requirement type are OR'd. You can also set options.FallbackPolicy to require authenticated users globally (so unauthenticated requests are rejected by default) and options.DefaultPolicy to define what a bare [Authorize] attribute without a named policy actually enforces.
Cricket analogy: Combining RequireClaim and a custom requirement in one policy is like the ICC requiring a bowler to pass BOTH a legal-action review AND a doping test before playing — every distinct condition must independently clear, not just one.
Without an explicit FallbackPolicy, endpoints that lack any [Authorize] attribute are accessible to anonymous users by default. In APIs where most endpoints should require authentication, set options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build() and opt individual endpoints out with [AllowAnonymous] instead of opting each one in.
- [Authorize(Roles = "X,Y")] performs an OR check against role claims; stacking multiple [Authorize] attributes ANDs them.
- Policies decouple authorization logic from attributes via named requirements evaluated by AuthorizationHandler<TRequirement>.
- A requirement's HandleRequirementAsync inspects claims (or resources) and calls context.Succeed(requirement) when satisfied.
- Multiple handlers for the same requirement are OR'd; distinct requirements within a policy are AND'd together.
- Resource-based authorization via IAuthorizationService.AuthorizeAsync handles per-entity decisions attributes can't express.
- FallbackPolicy sets the default behavior for endpoints with no explicit [Authorize] attribute at all.
- Prefer a global RequireAuthenticatedUser() fallback with selective [AllowAnonymous] over endpoint-by-endpoint opt-in.
Practice what you learned
1. What does [Authorize(Roles = "Admin,Manager")] evaluate to?
2. In a policy with two different IAuthorizationRequirement types, how must the requirements be satisfied for authorization to succeed?
3. When should you use resource-based authorization instead of attribute-based [Authorize]?
4. What is the risk of not configuring a FallbackPolicy in an API where most endpoints should require authentication?
5. What must a custom AuthorizationHandler call to indicate its requirement was satisfied?
Was this page helpful?
You May Also Like
Authentication with ASP.NET Core Identity
Learn how ASP.NET Core Identity manages users, passwords, and cookie-based sign-in for server-rendered and hybrid applications.
JWT Bearer Authentication
Understand how to secure ASP.NET Core Web APIs with stateless JSON Web Tokens issued and validated via the JWT bearer scheme.
Securing APIs: Best Practices
A practical checklist of defense-in-depth techniques for hardening ASP.NET Core Web APIs beyond basic authentication and authorization.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics