7 Types of Authorization in ASP.NET Core
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. This guide walks through all seven authorization types — simple, role-based, policy-based, claims-based, custom requirements, endpoint-specific, and resource-specific — with when to use each and minimal C# you can drop into a project.
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.
1app.MapGet("/profile", () => Results.Ok("secret"));2// No gate — crawlers and anonymous users included1app.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.
1[Authorize(Roles = "Admin")]2public IActionResult DeleteUser(int id) =>3 Ok(_users.Delete(id));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.
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.
1[Authorize(Roles = "Admin")]2public IActionResult ManageUsers() { ... }3 4[Authorize(Roles = "Admin")]5public IActionResult ManageRoles() { ... }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.
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 tenant = ctx.User.FindFirst("tenant_id")?.Value;10 var routeTenant = ctx.Resource as HttpContext;11 // match route tenant to claim — see resource-based section12 return tenant is not null;13 }));14});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(new MustOwnSubscriptionRequirement())`. ASP.NET Core runs all handlers for the policy; all must succeed (by default) unless you configure otherwise.
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 handlers on one policy — understand AND vs OR (UseRequireAll).
- •Keep handlers fast — cache permission lookups; avoid N+1 per request.
- •Unit test handlers in isolation with AuthorizationHandlerContext.
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 var age = DateTime.Today.Year - DateTime.Parse(dob).Year;18 if (age >= requirement.MinimumAge)19 context.Succeed(requirement);20 21 return Task.CompletedTask;22 }23}24 25// Register:26options.AddPolicy("AtLeast18", p =>27 p.AddRequirements(new MinimumAgeRequirement(18)));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.
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 publicRemember 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).
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.
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.
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.
If you only use `[Authorize(Roles = "Admin")]`, you are leaving six tools in the box. Register policies in one place, test handlers with real claims, and call `AuthorizeAsync` before mutating shared data — that is how .NET APIs stay secure as requirements grow.
Related Articles
JSON Web Tokens are everywhere — issued by every OAuth 2.0 provider, sent in every Authorization: Bearer header, decoded…
ReadMost API security checklists stop at "use HTTPS and check the JWT." That covers the front door, but real API breaches ra…
ReadAuthentication in modern apps spans three distinct models. Sessions store user state on the server and send a session ID…
ReadExplore this topic