← Open in the full interactive course (progress tracking, search & more)

Knowing exactly who someone is settles nothing about what they're allowed to do next.

The previous lesson ended with a fully authenticated request: HttpContext.User holds a real, verified identity — say, a logged-in customer named Priya. Now a request comes in: DELETE /api/orders/4471. The server knows, with certainty, that this is Priya. That fact, on its own, answers nothing about whether this specific delete should be allowed. Is order 4471 even Priya's order? Is Priya an administrator who can delete any order, or an ordinary customer who can only delete her own, and only within an hour of placing it? Authentication already did its job perfectly — it told you who's asking. It was never going to tell you what they're allowed to do. That's a completely separate question, with a completely separate answer, and answering it is this lesson's entire subject.

In this lesson: [Authorize] and [AllowAnonymous], role-based authorization with [Authorize(Roles = "...")], the more flexible policy-based authorization model built around arbitrary requirements, and claims — the underlying currency that both authentication and authorization actually read and reason about.

What Is It?

The Simple Explanation

Authorization is the process of deciding whether an already-identified caller is permitted to perform a specific action or access a specific resource. It always runs after authentication, and it always assumes authentication already did its job — authorization never re-checks "is this really who they claim to be," it only ever asks "given that this is genuinely who they are, are they allowed to do this?"

The Technical Definition

In ASP.NET Core, authorization is implemented as middleware (UseAuthorization()) plus a set of attributes and services that evaluate one or more requirements against the current ClaimsPrincipal on HttpContext.User — the exact identity authentication produced in the previous lesson. The simplest requirement is "any authenticated identity" (bare [Authorize]); more specific requirements check role membership or evaluate an arbitrary, named policy. Every one of these checks ultimately reads claims — individual facts attached to the identity — to make its decision.

Authentication (previous lesson)

Authorization (this lesson)

Why Does It Exist?

The Problem — Not Every Authenticated User Should Do Everything

Almost every real application has more than one kind of user, and different kinds of users are allowed to do genuinely different things — customers can view their own orders, but only staff can issue refunds; only an order's owner can cancel it; only an administrator can delete another user's account. If the only gate is "are you logged in at all," none of these distinctions can be expressed. Every authenticated user would be equally powerful, which is rarely the actual intent and is often a serious security gap.

The Need

What's needed is a way to express permission rules — sometimes as simple as "must be an Admin," sometimes as elaborate as "must be over 18 and from a supported region and own the resource being modified" — evaluated consistently, on every relevant request, without scattering ad-hoc if checks for role names or claim values throughout the codebase.

The Solution

ASP.NET Core's authorization system, offering three levels of expressiveness: bare [Authorize] for "any authenticated user," role-based [Authorize(Roles = "...")] for simple role membership checks, and policy-based authorization for arbitrary, named, reusable requirements — the most flexible of the three, and the one modern applications increasingly reach for as rules grow past "does this user have role X."

Big Picture

CLAIMS ARE THE CURRENCY EVERYTHING ELSE IS BUILT FROM
Authentication produces a ClaimsPrincipal, holding a set of claims
e.g. name = "Priya", role = "Admin", birthdate = "2001-03-14"

Role-based authorization reads the role claim(s) specifically
↓                          or
Policy-based authorization reads any claim(s), and can combine several into one requirement

Allow → the endpoint executes          Deny → 403 Forbidden

How It Works

FROM [Authorize] TO ROLES TO POLICIES — STEP BY STEP
1. [Authorize] — REQUIRES ANY AUTHENTICATED IDENTITY
[Authorize]
[HttpGet("me")]
public IActionResult GetMyProfile() => Ok(User.Identity?.Name);
2. [AllowAnonymous] — EXPLICITLY OPTS OUT, EVEN INSIDE A PROTECTED CONTROLLER
[Authorize]                 // every action in this controller requires authentication...
public class AccountController : ControllerBase
{
    [AllowAnonymous]         // ...except this one, which explicitly opts back out
    [HttpPost("register")]
    public IActionResult Register(RegisterRequest request) { /* ... */ }
}
3. ROLE-BASED — REQUIRES MEMBERSHIP IN A SPECIFIC ROLE (OR ONE OF SEVERAL)
[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public IActionResult DeleteUser(int id) { /* ... */ }

[Authorize(Roles = "Admin,Manager")]  // comma-separated = OR: either role is accepted
[HttpGet("reports")]
public IActionResult ViewReports() { /* ... */ }
4. POLICY-BASED — REGISTER AN ARBITRARY, NAMED REQUIREMENT
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("MustBeOver18", policy =>
        policy.RequireAssertion(context =>
        {
            var dobClaim = context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);
            return dobClaim is not null
                && DateOnly.TryParse(dobClaim.Value, out var dob)
                && DateOnly.FromDateTime(DateTime.UtcNow) >= dob.AddYears(18);
        }));
});
5. APPLY THE POLICY BY NAME
[Authorize(Policy = "MustBeOver18")]
[HttpPost("purchase-restricted-item")]
public IActionResult PurchaseRestrictedItem() { /* ... */ }

Simple Example

Why policies are more flexible than roles alone — a rule that role-based authorization simply cannot express, because it isn't about role membership at all:

// "Must have completed onboarding AND be from a supported country" —
// this has nothing to do with role membership, so [Authorize(Roles = "...")] cannot express it at all.
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CanPlaceOrders", policy =>
        policy.RequireClaim("onboarding_complete", "true")
              .RequireClaim("country", "US", "CA", "GB")); // any of these values satisfies the claim
});

Meaning: A role check can only ever ask "does this user have role X (or Y, or Z)?" A policy can ask anything expressible against the current ClaimsPrincipal — multiple claims combined together, custom logic via RequireAssertion, even injected services consulted mid-check via a custom IAuthorizationHandler. That's precisely why policies are described as more flexible: roles are really just one narrow, common special case of "a rule based on claims," and policies are the general mechanism roles happen to be built on top of internally.

Real-World Example

A support ticketing system with three genuinely distinct authorization needs on one controller:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("OwnsTicketOrIsSupportStaff", policy =>
        policy.RequireAssertion(context =>
        {
            if (context.User.IsInRole("Support") || context.User.IsInRole("Admin"))
                return true;

            // Otherwise, the caller must be the ticket's own creator — a rule tied to a specific
            // resource, which no role by itself could ever express.
            var ticketId = ((HttpContext)context.Resource!).Request.RouteValues["id"]?.ToString();
            var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
            return TicketOwnershipChecker.UserOwnsTicket(userId, ticketId);
        }));
});

[ApiController]
[Route("api/tickets")]
public class TicketsController : ControllerBase
{
    [AllowAnonymous]
    [HttpGet("public-status")]
    public IActionResult PublicSystemStatus() => Ok("All systems operational");

    [Authorize] // any authenticated user can create a ticket
    [HttpPost]
    public IActionResult CreateTicket(CreateTicketRequest request) { /* ... */ }

    [Authorize(Roles = "Support,Admin")] // simple role check — closing a ticket is support/admin only
    [HttpPost("{id}/close")]
    public IActionResult CloseTicket(int id) { /* ... */ }

    [Authorize(Policy = "OwnsTicketOrIsSupportStaff")] // needs richer, resource-aware logic than roles alone can express
    [HttpGet("{id}")]
    public IActionResult GetTicket(int id) { /* ... */ }
}

Notice all four authorization shapes side by side, on one controller: fully public ([AllowAnonymous]), any authenticated user ([Authorize]), simple role membership (Roles = "Support,Admin"), and a genuinely custom, resource-aware policy that no combination of role names alone could express — because the rule depends on which specific ticket is being requested, not just who the caller is in general.

Analogy

The ID proved who you are; the wristband decides where you can go

Continuing straight from the previous lesson's ID-check analogy: showing your photo ID at the front desk was authentication. Authorization is the colored wristband or access badge you're given after that check — and different wristbands let you into different areas. A general-admission wristband (bare [Authorize]) gets you past the front door and nothing more. A staff badge with "Backstage" printed on it (a role check) gets you into areas general admission can't reach. And a badge that's individually programmed to open your specific assigned locker, but no one else's (a policy checking resource ownership) is a rule no simple badge color could ever express — it depends on exactly which locker you're standing in front of, not just what category of badge-holder you are.

The security guard at the door already knows exactly who you are by this point — the wristband color, not your face, is what they're actually checking now.

Under the Hood

CLAIMS, REQUIREMENTS, AND HANDLERS
HOW A ClaimsPrincipal ACTUALLY GETS EVALUATED

Common Confusion

1. "Authorization is just another word for authentication" — no; this is the same conflation from the previous lesson, now from the other side

By this point in the pipeline, authentication has already run and already succeeded — the identity is settled. Authorization takes that settled identity as a given and asks a genuinely different question: not "is this real," but "is this allowed." If a request fails authorization (403), that never means the identity was invalid — it means the identity was perfectly valid and simply isn't permitted to do the thing it asked for.

2. "Roles and claims are different systems" — roles are just one specific kind of claim

Beginners sometimes treat "role-based" and "claims-based" as two competing authorization models. They're not — role membership is stored and read as an ordinary claim (typically ClaimTypes.Role), and [Authorize(Roles = "Admin")] is really a convenient, narrower shorthand over the same claims mechanism that policies use in general. Claims are the one underlying currency; roles are a specific, very common pattern built from that currency.

Common Mistakes

Mistake 1 — Using Roles = "Admin,Manager" and expecting an AND, not an OR

Assuming a comma-separated role list requires the caller to hold all listed roles at once.

It's an OR — any one of the listed roles is sufficient. If you genuinely need "must have both Role A and Role B," stack two separate [Authorize] attributes (ASP.NET Core requires all applied [Authorize] attributes to individually succeed), or express it explicitly as a policy.

Mistake 2 — Reaching for ever-more role names instead of a policy, as rules grow complex

Inventing roles like "AdminOver18" or "PremiumUSCustomer" purely to encode a rule that's really a combination of unrelated facts, bloating the role list with one-off, single-purpose entries.

Once a rule depends on more than plain role membership — age, region, resource ownership, a combination of several facts — express it as a policy instead of contorting the role system to fit.

Mistake 3 — Forgetting that [AllowAnonymous] is needed inside a controller-level [Authorize]

Applying [Authorize] at the controller level, then adding a login or registration action that has no way to reach an authenticated state yet — locking users out of the very endpoint that would authenticate them.

Explicitly mark those specific actions [AllowAnonymous] — it overrides the controller-level requirement for that action only.

When Should I Use It?

Rule of thumb: If you catch yourself about to invent a new role name that only exists to encode one narrow combination of conditions, that's usually a sign the rule belongs in a policy instead.

Mental Model

Claim = one fact about the identity (name, role, birthdate, permission).
Role check = a shorthand for "does the role claim match?" — one narrow special case.
Policy = an arbitrary, named, reusable rule over any claim(s) — the general mechanism roles are built from.
[AllowAnonymous] = the explicit escape hatch, even inside an otherwise-locked-down controller.

Remember: Authentication answers "who." Authorization — everything in this lesson — only ever answers "what are they allowed to do, now that we already know who they are."

Key Takeaway


Check Your Understanding

You've seen how authorization builds on authentication's identity, and the three levels of expressiveness available. Let's check your understanding.

1. An already-authenticated user requests an action they don't have permission for. What HTTP status code should the server return?

Show answer

Correct: B

Why B is correct: The identity is genuinely valid — authentication already succeeded. What failed is authorization: this specific, known identity isn't permitted to do this specific thing. That's precisely what 403 means.

Why A is incorrect: 401 means no valid identity was established at all — the opposite of this scenario, where the identity is fully valid.

Why C is incorrect: The request is well-formed; the problem is a permission decision, not malformed input.

Why D is incorrect: While some applications deliberately use 404 to avoid confirming a resource's existence to unauthorized callers, that's a specific design choice, not the standard meaning of an authorization failure — 403 is the standard, correct answer here.

Reinforcement: 401 = no valid identity. 403 = valid identity, insufficient permission. Keep these mapped precisely to authentication vs. authorization.

2. What does [Authorize(Roles = "Admin,Manager")] actually require?

Show answer

Correct: B

Why B is correct: A comma-separated role list in [Authorize(Roles = "...")] is an OR — the user needs to be in at least one of the listed roles to pass.

Why A is incorrect: This is the common mistake called out in the lesson — an AND would require stacking two separate [Authorize] attributes or writing an explicit policy, not a comma-separated list.

Why C is incorrect: This describes an exclusion rule, which is not what a comma-separated role list expresses at all.

Why D is incorrect: The check is about the current user's role membership, not whether roles exist somewhere in a database.

Reinforcement: Comma-separated roles = "any one of these" — a frequent source of confusion worth memorizing precisely.

3. Why is policy-based authorization considered more flexible than role-based authorization?

Show answer

Correct: B

Why B is correct: A policy can combine multiple claims, add custom assertion logic, and even reason about the specific resource being accessed — none of which "does this role name match" can express on its own.

Why A is incorrect: The lesson makes no performance claim, and flexibility — not speed — is the actual reason policies are described as more powerful.

Why C is incorrect: Authorization, policy-based or otherwise, always depends on authentication having already established an identity to evaluate.

Why D is incorrect: Role-based authorization is a fully supported, current, commonly used mechanism — it's just narrower in what it can express than a policy.

Reinforcement: Flexibility, not deprecation or speed, is the real reason to prefer policies once a rule outgrows plain role membership.

4. A controller is marked [Authorize] at the class level. One action inside it is a public registration endpoint that unauthenticated users must be able to reach. What's the correct fix?

Show answer

Correct: B

Why B is correct: [AllowAnonymous] is the explicit, scoped escape hatch — applying it to one action opts that action out of the controller-level [Authorize] requirement without affecting any other action in the controller.

Why A is incorrect: That would remove protection from every other action in the controller too — far broader than the fix actually needed.

Why C is incorrect: A controller-level [Authorize] does apply to every action inside it by default — that's exactly why an explicit opt-out is needed for the one exception.

Why D is incorrect: There's no built-in "Anonymous" role — [AllowAnonymous] is the correct, purpose-built mechanism, not a role name trick.

Reinforcement: [AllowAnonymous] exists precisely for this common shape — a mostly-protected controller with one or two genuinely public actions.

You now understand exactly how authentication and authorization divide the work — and how claims, roles, and policies fit together to express real permission rules. Next up: JWT, the concrete mechanism behind most modern token-based authentication.


dotnetmadeeasy.com — Learn C# and .NET, the right way.