Skip to content

7 Types of Authorization in ASP.NET Core

Core Concept LearningJuly 5, 20266 min readUpdated July 21, 2026

Many .NET teams stop at "add [Authorize] and check roles." That covers two of seven authorization models ASP.NET Core ships with — and leaves you reaching for hacks when requirements get nuanced.

Authentication answers who the user is. Authorization answers what they can do. ASP.NET Core separates these cleanly: authentication middleware builds a ClaimsPrincipal; authorization runs afterward via policies, handlers, and filters. ASP.NET Core actually ships seven authorization types — simple, role-based, policy-based, claims-based, custom requirements, endpoint-specific, and resource-specific — each solving a different shape of what a user can do, with when to use each and minimal C# you can drop into a project.

For token validation at the API boundary, continue with the JWT security deep dive. For dependency boundaries around authorization code, see Clean Architecture in .NET.

Seven authorization models in ASP.NET Core
Seven authorization models in ASP.NET Core

1. Simple Authorization

The simplest form: mark a controller, action, or minimal API route with [Authorize] (or .RequireAuthorization()) so only authenticated users can access it. No roles, no custom rules — just "must be logged in."

Use this for endpoints that any signed-in user may call: profile page, logout, generic dashboard shell. Anonymous users get 401 Unauthorized (not authenticated) or 403 Forbidden (authenticated but not allowed), depending on the failure mode.

Register authentication first (AddAuthentication + middleware). Authorization without authentication only checks an empty principal — a common misconfiguration in new projects.

Quick reference

  • Requires: AddAuthorization() + UseAuthentication() + UseAuthorization() in pipeline order.
  • 401 when no valid credential; 403 when credential exists but rule fails (later types).
  • AllowAnonymous overrides [Authorize] on specific actions.
  • Minimal APIs: chain .RequireAuthorization() per route or on route groups.
  • Good default for any endpoint that should not be public.
  • Not enough when users have different permissions within the same app.
Anonymous — anyone can hit the endpoint
1app.MapGet("/profile", () => Results.Ok("secret"));2// No gate — crawlers and anonymous users included
Simple authorization — authenticated users only
1app.MapGet("/profile", () => Results.Ok("secret"))2    .RequireAuthorization();3 4// Or on MVC / Razor Pages:5// [Authorize]6// public IActionResult Profile() => View();

Remember this

Simple authorization is the baseline — authenticated only, no permission differentiation.

2. Role-Based Authorization (RBAC)

Role-based authorization checks membership in named roles: Admin, Editor, Viewer. In ASP.NET Core, roles are typically role claims on the user's identity (ClaimTypes.Role or "role" in JWT).

Decorate with [Authorize(Roles = "Admin")] or multiple roles (comma = OR). Works well when permissions align with job titles and stay coarse — back-office apps, internal tools, small teams.

Breaks down when one user needs exceptions ("Editor except billing") or when role names explode (AdminBilling, AdminSupport, …). That is when policies and claims take over.

Quick reference

  • Roles must be issued at login — Identity, JWT role claim, or manual ClaimsIdentity.
  • Comma-separated roles = user needs ANY listed role.
  • Role store: ASP.NET Core Identity RoleManager, or external IdP groups mapped to roles.
  • Fine for ≤5 stable roles; refactor to policies before role matrix gets unwieldy.
  • Do not encode permissions in role strings (AdminCanDeleteUsers) — use claims/policies.
  • Test with TestServer and claims principal factory in integration tests.
Single role gate
1[Authorize(Roles = "Admin")]2public IActionResult DeleteUser(int id) =>3    Ok(_users.Delete(id));
Multiple roles (OR) — Minimal API
1app.MapDelete("/users/{id}", (int id) => Results.NoContent())2    .RequireAuthorization(new AuthorizeAttribute3    {4        Roles = "Admin,SuperAdmin"5    });

Remember this

RBAC fits coarse, stable job titles — Admin, Manager, User — not fine-grained product permissions.

3. Policy-Based Authorization

Policies name a bundle of requirements. Instead of scattering role strings, you register options.AddPolicy("CanManageUsers", …) once and reference [Authorize(Policy = "CanManageUsers")] everywhere.

Policies can require roles, claims, custom requirements, or combinations via PolicyBuilder. Central registration in Program.cs keeps authorization rules discoverable and testable — one file to audit before release.

This is the recommended default for production APIs once you outgrow bare roles.

Roles vs policies — centralize rules
Roles vs policies — centralize rules

Quick reference

  • AddPolicy in AddAuthorization callback — inject services via policy builder if needed.
  • Combine RequireRole, RequireClaim, RequireAssertion, AddRequirements.
  • Same policy name on controllers, minimal APIs, and Razor Pages.
  • FallbackPolicy and DefaultPolicy set baseline for entire app.
  • Policy names are strings — use constants (AuthPolicies.CanManageUsers).
  • Document each policy in team wiki or XML comments — auditors will ask.
Inline role checks scattered across controllers
1[Authorize(Roles = "Admin")]2public IActionResult ManageUsers() { ... }3 4[Authorize(Roles = "Admin")]5public IActionResult ManageRoles() { ... }
Named policy registered once
1builder.Services.AddAuthorization(options =>2{3    options.AddPolicy("CanManageUsers", policy =>4        policy.RequireRole("Admin")5              .RequireClaim("department", "IT", "HR"));6});7 8[Authorize(Policy = "CanManageUsers")]9public IActionResult ManageUsers() => Ok();

Remember this

Policies name reusable authorization rules — register centrally, apply with [Authorize(Policy = "…")].

4. Claims-Based Authorization

Claims are key-value facts about the user: user ID, email, permission=invoice.approve, tenant_id=42. Claims-based authorization checks those facts instead of roles alone.

Use RequireClaim("permission", "reports.export") inside a policy, or [Authorize(Policy = "ExportReports")] where the policy requires that claim. JWT access tokens commonly carry permissions as claims; Identity adds them at sign-in.

This maps cleanly to permission tables in the database without creating a new role per combination.

Quick reference

  • ClaimTypes and custom claim names — be consistent across token issuer and API.
  • Map IdP groups → application permissions at login, not on every request.
  • Prefer short-lived tokens when permissions change frequently.
  • RequireClaim supports multiple allowed values (OR).
  • Avoid putting sensitive data in claims clients can read (JWT payload is base64, not secret).
  • Combine with IClaimsTransformation to normalize legacy claim shapes.
Diagram
1builder.Services.AddAuthorization(options =>2{3    options.AddPolicy("ExportReports", policy =>4        policy.RequireClaim("permission", "reports.export"));5 6    options.AddPolicy("TenantMember", policy =>7        policy.RequireAssertion(ctx =>8        {9            var claimTenant = ctx.User.FindFirst("tenant_id")?.Value;10            var http = ctx.Resource as HttpContext;11            var routeTenant = http?.Request.RouteValues["tenantId"]?.ToString();12            return claimTenant is not null13                && routeTenant is not null14                && StringComparer.Ordinal.Equals(claimTenant, routeTenant);15        }));16});

Remember this

Claims-based authorization checks facts on the identity — permissions, tenant, department — not just role names.

5. Custom-Requirement Authorization

When built-in rules are not enough, implement IAuthorizationRequirement and AuthorizationHandler<TRequirement>. The handler contains your logic: database lookups, business rules, time windows, subscription tier checks.

Register the requirement in a policy with policy.AddRequirements(...), and register its handler with dependency injection. Every distinct requirement in a policy must succeed (AND). If one requirement has multiple handlers, any handler may satisfy that requirement (OR); there is no UseRequireAll switch.

This is how authorization stays domain-aware without bloating controllers.

Quick reference

  • Handlers are DI-enabled — inject DbContext, HTTP context, options.
  • context.Succeed(requirement) marks pass; no Succeed = fail.
  • context.Fail() with reason for debugging (logged at Information).
  • Multiple requirements are AND; multiple handlers for the same requirement provide alternative OR paths.
  • Keep handlers fast — cache permission lookups; avoid N+1 per request.
  • Unit test handlers in isolation with AuthorizationHandlerContext.
Diagram
1public sealed class MinimumAgeRequirement : IAuthorizationRequirement2{3    public int MinimumAge { get; }4    public MinimumAgeRequirement(int age) => MinimumAge = age;5}6 7public sealed class MinimumAgeHandler8    : AuthorizationHandler<MinimumAgeRequirement>9{10    protected override Task HandleRequirementAsync(11        AuthorizationHandlerContext context,12        MinimumAgeRequirement requirement)13    {14        var dob = context.User.FindFirst("birthdate")?.Value;15        if (dob is null) return Task.CompletedTask;16 17        if (!DateOnly.TryParse(dob, out var birthDate))18            return Task.CompletedTask;19 20        var today = DateOnly.FromDateTime(DateTime.UtcNow);21        var age = today.Year - birthDate.Year;22        if (birthDate > today.AddYears(-age))23            age--;24 25        if (age >= requirement.MinimumAge)26            context.Succeed(requirement);27 28        return Task.CompletedTask;29    }30}31 32// Register:33builder.Services.AddAuthorization(options =>34    options.AddPolicy("AtLeast18", p =>35        p.AddRequirements(new MinimumAgeRequirement(18))));36builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

Remember this

Custom requirements + handlers encode business rules in authorization — keep controllers thin.

6. Endpoint-Specific Authorization

Endpoint-specific authorization applies different rules per route — critical for Minimal APIs and for mixing public, authenticated, and admin routes in one app.

Patterns: [Authorize] on one controller action but not another; .RequireAuthorization("PolicyName") on a single MapGet; route groups with app.MapGroup("/admin").RequireAuthorization(). You can also use IEndpointConventionBuilder metadata and endpoint filters in .NET 7+.

Fine-grained HTTP method control: GET public, POST requires Editor — implement via separate endpoints or policy per verb.

Quick reference

  • MapGroup reduces duplicate RequireAuthorization calls.
  • AllowAnonymous on endpoint overrides group-level [Authorize].
  • Endpoint metadata visible in OpenAPI — document security schemes.
  • FallbackPolicy: require auth globally, opt-out with AllowAnonymous.
  • Use naming conventions: /internal/* always requires VPN + policy.
  • Integration tests: hit endpoint with/without token, assert 401/403/200.
Diagram
1var api = app.MapGroup("/api");2 3api.MapGet("/products", () => Results.Ok(products)); // public4 5var admin = api.MapGroup("/admin")6    .RequireAuthorization("CanManageUsers");7 8admin.MapPost("/products", (Product p) => Results.Created(...));9 10app.MapGet("/health", () => Results.Ok())11    .AllowAnonymous(); // explicit public

Remember this

Endpoint-specific authorization maps rules to routes — groups, policies, and AllowAnonymous per path.

7. Resource-Specific Authorization

Resource-based authorization asks: "Can this user perform this action on this document?" Roles and claims alone cannot answer that — ownership and row-level rules depend on the resource instance.

Call IAuthorizationService.AuthorizeAsync(user, resource, policyName) in your handler after loading the entity. Common pattern: user may edit only their own orders; managers may edit team orders; admins may edit all.

Implement OperationAuthorizationRequirement or a custom requirement where the handler receives the resource object (Order, Document, Project).

Zoom into one ASP.NET Core resource authorization decision
Zoom into one ASP.NET Core resource authorization decision

Quick reference

  • Resource can be any object passed to AuthorizeAsync — entity, DTO, tuple.
  • Handler: OperationAuthorizationRequirement for CRUD-style ops (Create, Read, Update, Delete).
  • Do not rely on hidden IDs from client — load resource server-side, then authorize.
  • 403 Forbid() vs 404 NotFound — hide existence of resources user cannot see (product choice).
  • Row-level security in DB is another layer — authorization at app + DB for defense in depth.
  • Blazor, MVC, and Minimal APIs all use the same IAuthorizationService.

Remember this

Resource-specific authorization evaluates the actual object — ownership and row-level rules live here.

Which Type Should You Use?

Use the simplest model that fits — then upgrade when pain appears.

Start: [Authorize] on everything except login/register.Add RBAC when job titles map cleanly to access.Move to policies + claims when permissions multiply.Add custom handlers when rules need database or domain logic.Apply per-endpoint when public and protected routes mix in one API.Add resource checks when users share a system but own different rows.

Most production ASP.NET Core APIs combine types: policies with claim requirements on endpoints, plus resource authorization in command handlers for update/delete.

Pick the simplest model that fits
Pick the simplest model that fits

Quick reference

  • Monolith admin panel: RBAC + a few policies.
  • Multi-tenant SaaS: claims (tenant_id) + resource handlers.
  • Public API + admin API: endpoint groups + different policies.
  • Microservices: same policy names + shared claim schema from IdP.
  • Always authenticate first — authorization never replaces login.
  • Related: JWT validation, OAuth, and REST auth articles on this site.

Remember this

Combine simple, policy, claim, endpoint, and resource authorization — most real apps use at least three.

Key takeaway

ASP.NET Core authorization is not one switch. Simple gates login. Roles gate coarse access. Policies name reusable rules. Claims carry permissions. Custom handlers encode domain logic. Endpoint metadata scopes routes. Resource checks enforce ownership.

Practice (25 min): create a Minimal API with dotnet new web -n AuthzLab, add /orders/{id} behind a CanEditOrder policy, and use a fake authenticated user with sub=user-1. Seed order 42 with owner user-1 and order 43 with owner user-2; run two integration requests. Pass only when 42 returns 200 and 43 returns 403 (or your documented 404 concealment policy), and neither denied request mutates the order. Then temporarily remove the resource check, prove the deny test fails, restore it, and rerun dotnet test to green.

Share:

Related Articles

JSON Web Tokens are a common access-token format, but OAuth 2.0 does not require them: providers may issue opaque bearer

Read

Selecting the correct authorization flow is essential for securing modern applications. The OAuth 2.1 specification cons

Read

A human who logs into a payments dashboard gets a session, a timeout, and a moment of hesitation before clicking submit.

Read

Keep learning

Follow a structured path or browse all courses to go deeper.