Authorization in ASP.NET Core: Roles, Claims, Policies and Resource-Based Access
Learn ASP.NET Core authorization in depth, covering roles, claims, policy requirements and handlers, resource-based checks, permissions and testing.
IntermediateUpdated 13 min read
On this page
Once ASP.NET Core knows who is calling, authorization decides what that caller may do. The framework gives you four building blocks that compose into everything from a simple admin flag to a fully dynamic, multi-tenant permission system: roles, claims, policies backed by requirements and handlers, and resource-based checks that look at the specific object being accessed. This guide walks through all four, shows how they apply in Minimal APIs and Blazor, and covers testing authorization logic in isolation from HTTP.
Authorization runs after authentication, against the ClaimsPrincipal that the authentication middleware already attached to HttpContext.User. At its center is IAuthorizationService, a framework-registered service with two AuthorizeAsync overloads: one that evaluates a named policy, one that evaluates an explicit list of requirements, both optionally against a specific resource object. Everything else β [Authorize], RequireAuthorization(), roles and claims shortcuts β is sugar that ultimately builds an AuthorizationPolicy and hands it to that same service. Understanding IAuthorizationService first makes the rest of the system easier to reason about, because declarative attributes and imperative service calls are two doors into the same evaluation engine.
The fallback and default policies are not the same thing and are never combined: an endpoint with a bare [Authorize] uses the default policy, and only an endpoint with no authorization metadata at all falls back to the fallback policy. Static files served before the authorization middleware runs are never covered by either. For how the credential itself gets onto HttpContext.User in the first place, see authentication in ASP.NET Core.
Getting Started: [Authorize] and RequireAuthorization#
Controllers and Razor components use the [Authorize] attribute; Minimal API endpoints use the fluent RequireAuthorization() extension, and both accept a policy name:
var orders = app.MapGroup("/orders").RequireAuthorization();
orders.MapDelete("/{id:int}", (int id) => Results.NoContent())
.RequireAuthorization("CanManageOrders");
// A single endpoint can also use the attribute directly:
app.MapGet("/orders/mine", [Authorize] (ClaimsPrincipal user) => user.Identity?.Name);
RequireAuthorization() on a MapGroup applies to every endpoint in the group, and a more specific call on an individual endpoint adds to, rather than replaces, the group's requirements β all of them must pass.
Roles are the simplest model: a claim of type role (or whatever RoleClaimType is configured to) whose values the framework checks with ClaimsPrincipal.IsInRole. [Authorize(Roles = "Admin,SuperUser")] is an OR: any one of the listed roles is enough. Stacking multiple [Authorize(Roles = "...")] attributes on the same target is an AND: the user must satisfy every attribute.
C#
[Authorize(Roles = "Admin,SuperUser")] // Admin OR SuperUser
[Authorize(Roles = "EmployeeOfTheMonth")] // AND EmployeeOfTheMonthpublicIActionResultManagerLounge() => Ok();
The same OR/AND rules apply to the policy-based equivalent, RequireRole: a single call with multiple arguments is an OR, and chaining .RequireRole("Admin").RequireRole("SuperUser") on the same policy builder is an AND, because each call adds a separate requirement. If you use ASP.NET Core Identity, call AddRoles<IdentityRole>() when configuring Identity to register the role store and RoleManager<TRole> that back this model; without it, IsInRole still works against any role claims you issue yourself, for example from an external OIDC provider's token.
Roles are really a special case of the more general claims model: any key/value pair on the principal, such as department = "Finance" or subscription-tier = "Enterprise", can drive an authorization decision. Register a claims policy with RequireClaim, which checks for the claim's presence and, optionally, one of a set of allowed values:
Claims usually come from the identity provider's token, but you can add or reshape them after authentication with an IClaimsTransformation implementation, which runs once per request before authorization evaluates. That is the right place to turn an external user ID into an internal permission claim looked up from your own database, rather than re-querying the database inside every authorization handler.
Policy-Based Authorization: Requirements and Handlers#
Policies exist for logic that a role or claim check cannot express on its own. A policy is one or more requirements β classes implementing the marker interface IAuthorizationRequirement β each evaluated by an authorization handler:
A handler indicates success by calling context.Succeed(requirement); it should generally not call context.Fail() for a simple "no" answer, because other handlers for the same requirement may still succeed β a requirement can have multiple handlers evaluated on an OR basis (any one succeeding satisfies the requirement), while multiple requirements on one policy are always evaluated on an AND basis. AuthorizationOptions .InvokeHandlersAfterFailure, true by default, keeps running every handler even after one calls context.Fail(), which lets handlers log side effects consistently. For simple inline logic, RequireAssertion skips the requirement/handler ceremony entirely with a Func<AuthorizationHandlerContext, bool>.
Attribute-Driven Policies with IAuthorizationRequirementData#
IAuthorizationRequirementData, added in .NET 8, lets an attribute carry its own requirement instead of you registering a separate named policy:
In ASP.NET Core 8.0 through 10.0, IAuthorizationRequirementData attributes are enforced only on Minimal API and other routed endpoints; .NET 11 extends support to SignalR hubs and hub methods, MVC controllers and actions, and Blazor's AuthorizeView and AuthorizeRouteView components. If your app targets an earlier release and needs the same pattern on a controller action, register a conventional named policy instead.
Resource-Based Authorization with IAuthorizationService#
Attribute-based checks run before model binding, so they cannot see the specific entity a request is about β you cannot decide from [Authorize] alone whether this user may edit this order. That requires imperative, resource-based authorization through IAuthorizationService, injected wherever you need it:
C#
publicclassOrderService(IAuthorizationService authorizationService)
{
publicasyncTask<IResult> UpdateAsync(Order order, ClaimsPrincipal user)
{
var result = await authorizationService.AuthorizeAsync(user, order, "SameOwnerPolicy");
if (!result.Succeeded)
{
returnResults.Forbid();
}
// Apply the update.returnResults.NoContent();
}
}
The handler for a resource-based requirement declares both the requirement and resource types:
For CRUD-shaped checks, the built-in OperationAuthorizationRequirement helper avoids writing one requirement class per verb: define Operations.Create, .Read, .Update and .Delete once, branch on requirement.Name inside a single handler, and call AuthorizeAsync(user, resource, Operations.Delete) at each call site.
Role and claim checks hardcode a name at compile time. Larger systems usually want permissions β fine-grained, data-driven strings such as "orders.delete" β checked without redeploying every time a permission is added. Combine a permission claim (populated at sign-in from your own store) with a generic requirement and handler:
For a catalog large enough that you don't want to register a named policy per permission, implement a custom IAuthorizationPolicyProvider that builds an AuthorizationPolicy on demand from the policy name itself (for example, treating any policy named "Permission:orders.delete" as a request for that permission), instead of pre-registering every combination at startup.
In a multi-tenant system, a permission is not enough on its own β a support agent with orders.delete for tenant A must not delete an order that belongs to tenant B. Fold the tenant check into a resource-based handler alongside the permission check, comparing a tenant_id claim against the resource's own tenant, never a client-supplied value:
Keep the tenant identifier out of the request body or route entirely where you can, and derive it only from the validated token or session, so a manipulated payload cannot widen access across tenants.
Minimal APIs use the same policy engine as MVC: RequireAuthorization() on a route or MapGroup, or the [Authorize] attribute applied directly to a route handler delegate, as shown earlier. Blazor applies authorization at the component level. AddCascadingAuthenticationState() (registered automatically by the Blazor Web App template) makes the current AuthenticationState available to every component; <AuthorizeView> renders different markup for authorized, unauthorized and authenticating states, and [Authorize] on a routable @page component blocks navigation to it entirely:
Razor
@attribute [Authorize(Policy = "CanManageOrders")]
<AuthorizeView><Authorized><p>Welcome, @context.User.Identity?.Name.</p></Authorized><NotAuthorized><p>You don't have access to this page.</p></NotAuthorized></AuthorizeView>
Resource-based checks work identically in Blazor: inject IAuthorizationService into a component and call AuthorizeAsync with the loaded entity, exactly as you would from a controller or Minimal API handler. See the Blazor guide for the broader render-mode and hosting model context this fits into.
Handlers are plain classes with no HTTP dependency, so unit test them directly by constructing an AuthorizationHandlerContext with a fabricated ClaimsPrincipal, the requirement under test, and an optional resource:
C#
[Fact]
publicasyncTaskHandler_Succeeds_For_Matching_Owner()
{
var user = newClaimsPrincipal(newClaimsIdentity(
[newClaim(ClaimTypes.Name, "alice")], "TestAuth"));
var requirement = newSameOwnerRequirement();
var context = newAuthorizationHandlerContext([requirement], user, newOrder { OwnerName = "alice" });
awaitnewOrderAuthorizationHandler().HandleAsync(context);
Assert.True(context.HasSucceeded);
}
For integration tests, register a lightweight test AuthenticationHandler in a WebApplicationFactory-based fixture that issues a predictable ClaimsPrincipal for every request, so you exercise the real authorization pipeline β including policy selection and [Authorize] metadata β without depending on a live identity provider.
Start from a fallback policy that requires authentication, and use roles or claims only where they truly map to your domain, not as a stand-in for permissions.
Prefer policies over role strings once you have more than a handful of roles. Policies centralize the rule; role strings scatter it across every attribute.
Never trust a client-supplied tenant or resource ID for authorization. Compare it against a value derived from the authenticated principal or loaded from the database.
Keep handlers side-effect-light and fast. They can run multiple times per request and should not perform expensive I/O without caching.
Use resource-based authorization for anything ownership-shaped.[Authorize] alone cannot see the entity.
Unit test handlers directly. They have no HTTP dependency, so there is no reason to test them only through full integration tests.
Confusing the default and fallback policies. A bare [Authorize] always uses the default policy, never the fallback, even on the same endpoint.
Forgetting that multiple [Authorize] attributes are ANDed. Stacking them by accident makes an endpoint stricter than intended.
Calling context.Fail() for an ordinary "not met" result. That forces the whole policy to fail even if another handler for the same requirement would have succeeded; reserve Fail() for cases that must veto every other handler.
Doing resource-based checks inside the resource-loading action only after the data is already fetched and returned to the client, instead of before assembling the response.
Relying on IAuthorizationRequirementData attributes on MVC controllers or SignalR hubs before .NET 11. They are silently not enforced there on earlier target frameworks.
Hardcoding tenant or organization IDs from route parameters instead of validating them against the caller's own claims.
Roles vs Claims vs Policies: Choosing an Approach#
Approach
Granularity
Where it's defined
Best for
Roles
Coarse (one name per group of users)
[Authorize(Roles = "...")] or RequireRole
Small, stable sets of job functions (Admin, Editor)
Claims
Fine (any key/value pair)
RequireClaim
Attributes issued by the identity provider (department, tier)
Policies with requirements
As fine as your handler logic
AddPolicy + AuthorizationHandler<T>
Reusable business rules, especially with external data
What is the difference between the default policy and the fallback policy?#
The default policy applies when an endpoint opts in with a bare [Authorize] or RequireAuthorization() and no policy name; it requires an authenticated user unless you change it. The fallback policy applies only to endpoints that carry no authorization metadata at all, and the two are never combined for the same endpoint.
Roles are fine for a small, stable set of broad job functions where the check really is "is this user an Admin?" Reach for policies as soon as the rule involves more than membership in a named group β combining claims, checking a resource, or applying business logic that might change independently of who has which role.
How do I authorize access to a specific record, not just an endpoint?#
Use resource-based authorization: inject IAuthorizationService and call AuthorizeAsync(user, resource, policyName) after loading the entity, inside a handler that implements AuthorizationHandler<TRequirement, TResource>. Attribute-based checks such as [Authorize] run before the entity is loaded, so they cannot express this by themselves.
Can Blazor WebAssembly enforce authorization on its own?#
Only as a UI convenience. <AuthorizeView> and [Authorize] in a WebAssembly app hide or block navigation to markup, but the compiled client code still ships to the browser, so any API the app calls must independently enforce authorization server-side; never treat client-side checks as the real security boundary.
How do I unit test a custom authorization handler?#
Construct an AuthorizationHandlerContext directly with the requirement, a ClaimsPrincipal you build in the test, and an optional resource object, call the handler's HandleAsync (or HandleRequirementAsync if you invoke it directly), and assert on context.HasSucceeded. No web host or HTTP call is required.
IAuthorizationService is the engine behind every authorization feature; [Authorize], roles, claims and policies all end up calling it.
Roles and claims cover simple, static checks; policies with requirements and handlers cover reusable business rules; resource-based authorization covers per-entity ownership decisions.
IAuthorizationRequirementData lets an attribute carry its own requirement, with broader framework support (SignalR, MVC, Blazor) arriving in .NET 11.
Multi-tenant and permission-based designs both build on the same requirement/handler pattern β the logic just reads from claims and resources instead of a hardcoded role name.
Handlers are plain classes: test them directly without spinning up a web host.
A practical guide to ASP.NET Core authentication: schemes and handlers, cookies, JWT bearer validation, OpenID Connect with Entra ID, and the BFF pattern.
Master ASP.NET Core middleware: the RequestDelegate pipeline, Use, Run, Map and UseWhen, correct ordering, custom and IMiddleware components, errors and tests.