7 Types of Authorization in ASP.NET Core Web API
Master all 7 authorization types in ASP.NET Core: simple, role-based, policy-based, claims-based, custom requirement, endpoint-specific, and resource-based — with real code examples for each.
Authentication vs Authorization
Before the 7 types — the most common confusion:
- Authentication — Who are you? Validates identity. Fails with
401 Unauthorized. - Authorization — What are you allowed to do? Validates permissions. Fails with
403 Forbidden.
Authentication runs first (UseAuthentication), then authorization (UseAuthorization).
app.UseAuthentication(); // ← first: who are you?
app.UseAuthorization(); // ← second: what can you do?Type 1: Simple Authorization
The most basic form — just requires the user to be authenticated.
// Any authenticated user can access this
[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return Ok(new { userId });
}
// Or on the whole controller
[Authorize]
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
// All actions require authentication
// Override for specific action
[AllowAnonymous]
[HttpGet("public-stats")]
public IActionResult PublicStats() => Ok(/* public data */);
}When to use: Any endpoint that should only be accessed by logged-in users, regardless of their role.
Type 2: Role-Based Authorization (RBAC)
Restricts access to users with specific roles. Roles are stored as claims in the JWT.
// Single role
[Authorize(Roles = "Admin")]
[HttpDelete("orders/{id}")]
public async Task<IActionResult> DeleteOrder(Guid id) { /* ... */ }
// Multiple roles — user needs ONE of them
[Authorize(Roles = "Admin,Manager")]
[HttpGet("orders/all")]
public async Task<IActionResult> GetAllOrders() { /* ... */ }
// Stacked attributes — user needs BOTH roles
[Authorize(Roles = "Admin")]
[Authorize(Roles = "SuperUser")]
[HttpPost("system/reset")]
public IActionResult Reset() { /* ... */ }Assigning roles in JWT:
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, userId),
new(ClaimTypes.Email, email),
new(ClaimTypes.Role, "Admin"),
new(ClaimTypes.Role, "Manager"), // multiple roles
};Register roles:
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>();
// Create role
await roleManager.CreateAsync(new IdentityRole("Admin"));
// Assign role to user
await userManager.AddToRoleAsync(user, "Admin");When to use: Simple permission models where users are grouped into distinct roles (Admin, Manager, Customer, Support).
Type 3: Policy-Based Authorization (PBAC)
Named policies that group multiple requirements. More flexible than roles alone.
// Register policies
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdmin", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("AtLeast18", policy =>
policy.RequireClaim("age").MinimumAge(18)); // custom extension
options.AddPolicy("PremiumUser", policy =>
policy.RequireRole("User")
.RequireClaim("subscription", "Premium", "Enterprise"));
options.AddPolicy("VerifiedEmail", policy =>
policy.RequireAuthenticatedUser()
.RequireClaim("email_verified", "true"));
options.AddPolicy("InternalNetwork", policy =>
policy.RequireAuthenticatedUser()
.AddRequirements(new InternalNetworkRequirement()));
});
// Use on endpoints
[Authorize(Policy = "PremiumUser")]
[HttpGet("reports/advanced")]
public IActionResult AdvancedReports() { /* ... */ }
[Authorize(Policy = "VerifiedEmail")]
[HttpPost("orders")]
public async Task<IActionResult> CreateOrder() { /* ... */ }When to use: When a permission requires multiple conditions (authenticated + specific role + specific claim). Makes requirements reusable across multiple endpoints.
Type 4: Claims-Based Authorization (CBAC)
Decisions based on specific claim values in the user's token.
// Register claims-based policy
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("HR", policy =>
policy.RequireClaim("department", "HR"));
options.AddPolicy("SeniorEmployee", policy =>
policy.RequireClaim("years_experience").HasValue(v =>
int.TryParse(v, out var years) && years >= 5));
options.AddPolicy("UKEmployee", policy =>
policy.RequireClaim("country", "UK"));
});
// Use
[Authorize(Policy = "HR")]
[HttpGet("employees/salaries")]
public IActionResult GetSalaries() { /* ... */ }
// Check claims directly in code
[HttpGet("sensitive-data")]
[Authorize]
public IActionResult GetSensitiveData()
{
var employeeId = User.FindFirst("employee_id")?.Value;
var dept = User.FindFirst("department")?.Value;
if (dept != "Finance")
return Forbid();
return Ok(/* sensitive data */);
}Adding claims to JWT:
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim("department", "HR"),
new Claim("employee_id", "EMP-001"),
new Claim("years_experience", "7"),
new Claim("country", "UK"),
new Claim("email_verified", "true"),
};When to use: When permissions depend on user attributes (department, country, employee ID) rather than broad role categories.
Type 5: Custom Requirement Authorization
Implement IAuthorizationRequirement and AuthorizationHandler for complex business logic.
// 1. Define the requirement
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge) => MinimumAge = minimumAge;
}
// 2. Implement the handler
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
var dateOfBirthClaim = context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);
if (dateOfBirthClaim is null)
{
context.Fail();
return Task.CompletedTask;
}
var dob = Convert.ToDateTime(dateOfBirthClaim.Value);
var age = DateTime.Today.Year - dob.Year;
if (dob.Date > DateTime.Today.AddYears(-age)) age--;
if (age >= requirement.MinimumAge)
context.Succeed(requirement);
else
context.Fail();
return Task.CompletedTask;
}
}
// 3. Register
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Over18", policy =>
policy.AddRequirements(new MinimumAgeRequirement(18)));
options.AddPolicy("Over21", policy =>
policy.AddRequirements(new MinimumAgeRequirement(21)));
});
// 4. Use
[Authorize(Policy = "Over18")]
[HttpPost("adult-content")]
public IActionResult AdultContent() { /* ... */ }Multiple handlers for one requirement:
// A requirement can be fulfilled by any of its handlers
public class BadgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
{
// Allow users with a verified age badge regardless of DOB claim
if (context.User.HasClaim("age_badge", "verified"))
context.Succeed(requirement);
return Task.CompletedTask;
}
}When to use: Complex business rules that can't be expressed with roles or claims alone — age checks, time-based access, subscription tier checks.
Type 6: Endpoint-Specific Authorization
Apply authorization directly in the routing pipeline without attributes.
// Minimal API — inline authorization
app.MapGet("/admin/users", async (AppDbContext db) =>
await db.Users.ToListAsync())
.RequireAuthorization("RequireAdmin");
app.MapPost("/orders", async (CreateOrderRequest req) => { /* ... */ })
.RequireAuthorization(policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("email_verified", "true");
});
// Allow anonymous on specific endpoint
app.MapGet("/public/status", () => Results.Ok("Healthy"))
.AllowAnonymous();
// MapGroup with shared authorization
var adminGroup = app.MapGroup("/admin")
.RequireAuthorization("RequireAdmin");
adminGroup.MapGet("/users", GetUsers);
adminGroup.MapGet("/reports", GetReports);
adminGroup.MapDelete("/data", DeleteData);When to use: Minimal API projects where you want authorization defined alongside routing, rather than in controller attributes.
Type 7: Resource-Based Authorization
Fine-grained control — the decision depends on the specific resource being accessed.
// 1. Define resource and requirement
public class OrderOwnerRequirement : IAuthorizationRequirement { }
// 2. Handler — checks if user owns the specific order
public class OrderOwnerHandler
: AuthorizationHandler<OrderOwnerRequirement, Order>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OrderOwnerRequirement requirement,
Order resource) // ← the specific order being accessed
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (resource.CustomerId == userId)
context.Succeed(requirement);
// Admin can access any order
else if (context.User.IsInRole("Admin"))
context.Succeed(requirement);
else
context.Fail();
return Task.CompletedTask;
}
}
// 3. Register
builder.Services.AddSingleton<IAuthorizationHandler, OrderOwnerHandler>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("OrderOwner", policy =>
policy.AddRequirements(new OrderOwnerRequirement()));
});
// 4. Use with IAuthorizationService in the controller
[ApiController]
[Route("api/orders")]
[Authorize]
public class OrdersController : ControllerBase
{
private readonly IAuthorizationService _authService;
private readonly IOrderRepository _orders;
public OrdersController(IAuthorizationService authService, IOrderRepository orders)
{
_authService = authService;
_orders = orders;
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
{
var order = await _orders.GetByIdAsync(id, ct);
if (order is null) return NotFound();
// Check if this user is allowed to see THIS specific order
var result = await _authService.AuthorizeAsync(User, order, "OrderOwner");
if (!result.Succeeded) return Forbid();
return Ok(order);
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
var order = await _orders.GetByIdAsync(id, ct);
if (order is null) return NotFound();
var result = await _authService.AuthorizeAsync(User, order, "OrderOwner");
if (!result.Succeeded) return Forbid();
await _orders.DeleteAsync(order, ct);
return NoContent();
}
}When to use: When permissions depend on the data itself — a user can edit their own profile but not others', an author can edit their own articles, a customer can view their own orders.
Comparing All 7 Types
| Type | Setup | Granularity | Best For |
|---|---|---|---|
| Simple | [Authorize] | None | Any authenticated user |
| Role-Based | [Authorize(Roles="Admin")] | Role groups | Broad permission categories |
| Policy-Based | [Authorize(Policy="X")] | Multiple conditions | Reusable complex rules |
| Claims-Based | RequireClaim("dept","HR") | User attributes | Department/country/tier |
| Custom Requirement | IAuthorizationHandler | Business logic | Complex rules (age, time) |
| Endpoint-Specific | .RequireAuthorization() | Per-route | Minimal API, co-located rules |
| Resource-Based | IAuthorizationService | Per-record | "Own resource" scenarios |
Interview Questions
Q: What is the difference between role-based and policy-based authorization? Role-based is a subset of policy-based. A policy can require a role AND other conditions (claim, custom requirement). Use policy-based for anything beyond a simple role check — it's more expressive and the requirements are reusable and testable.
Q: What is resource-based authorization and when does it replace attribute-based?
When the authorization decision depends on the specific data being accessed — not just the user's identity. [Authorize] attributes can't check "does this user own this order?" because they run before the endpoint resolves the resource. IAuthorizationService.AuthorizeAsync(user, resource, policy) runs inside the action after the resource is loaded.
Q: What happens when multiple [Authorize] attributes are stacked?
The user must satisfy ALL of them — it's AND logic. Each attribute adds a policy requirement. For OR logic (any of several roles/policies), combine them in a single policy using RequireAssertion.
Q: How do you test authorization in unit tests?
Use AuthorizationService from DI or build a test service with DefaultAuthorizationService. Pass a ClaimsPrincipal with the relevant claims and the resource to AuthorizeAsync. Assert result.Succeeded or result.Failure.FailedRequirements.
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.