Learnixo
Back to blog
Backend Systemsintermediate

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.

LearnixoJune 4, 20268 min read
.NETC#AuthorizationSecurityASP.NET CoreJWTRBAC
Share:𝕏

Authentication vs Authorization

Before the 7 types — the most common confusion:

  • AuthenticationWho are you? Validates identity. Fails with 401 Unauthorized.
  • AuthorizationWhat are you allowed to do? Validates permissions. Fails with 403 Forbidden.

Authentication runs first (UseAuthentication), then authorization (UseAuthorization).

C#
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.

C#
// 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.

C#
// 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:

C#
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:

C#
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.

C#
// 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.

C#
// 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:

C#
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.

C#
// 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:

C#
// 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.

C#
// 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.

C#
// 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?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.