Backend Development with .NET
Session 20
Authorization: Roles & Policies
Eng. Seif Mansour  ·  Andalusia Academy
Week 8  ·  2 hours
Session Goals

By the end of this session, you will be able to:

  • Implement role-based authorization using [Authorize(Roles="Admin")]
  • Create custom policy-based authorization requirements in Program.cs
  • Implement resource-based authorization so users can only access their own data
  • Explain the difference between role-based and claim-based authorization
Agenda
Time Segment Type Duration
0:00Roles vs claims vs policiesTheory15 min
0:15Role-based authorizationDemo20 min
0:35Policy-based authorizationDemo25 min
1:00Resource-based authorizationTheory + Demo25 min
1:25Lab — protect admin endpointsLab30 min
1:55Wrap-upDiscussion5 min
Roles, Claims & Policies
Understanding the three authorization building blocks in ASP.NET Core
In this section
What a role is and when to use it
What a claim represents
How policies compose both
401 Unauthorized vs 403 Forbidden
Roles, Claims & Policies
Role
Coarse-grained group membership. Tells you who the user is in the system.
Admin, Member, Guest
Claim
Fine-grained fact about the user. A key-value pair stored inside the JWT.
department=Engineering
canDeleteTasks=true
Policy
Named rule that combines one or more requirements. Evaluated at runtime.
"SeniorStaff": role Admin
AND claim experience ≥ 5
Rule of thumb
Use roles for broad access tiers, claims for attribute-based decisions, and policies when you need to combine both into a reusable rule.
401 vs 403 — Know the Difference

ASP.NET Core returns different HTTP status codes depending on why access was denied.

401
Unauthorized
No valid token was provided. The request has not been authenticated. The user is anonymous.
403
Forbidden
A valid token was provided but the user lacks the required role or policy. Authentication passed; authorization failed.
Common mistake
Do not return 401 when you mean 403. Returning 401 misleads clients into thinking they need to re-authenticate, not that they are simply not permitted.
Role-Based Authorization
Using [Authorize] attributes to restrict endpoints by role
In this section
Attaching the Role claim during token generation
[Authorize] vs [Authorize(Roles="...")]
Allowing multiple roles on one endpoint
How Roles Enter the Token

The role must be added as a claim when the JWT is issued (Session 19). ASP.NET Core reads ClaimTypes.Role from the token automatically.

Services/TokenService.cs
var claims = new List<Claim>
{
    new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
    new Claim(ClaimTypes.Email, user.Email),
    new Claim(ClaimTypes.Role, user.Role)   // e.g. "Admin" or "Member"
};
Important
The value assigned to ClaimTypes.Role must exactly match the string you pass to [Authorize(Roles="...")] — it is case-sensitive.
The [Authorize] Attribute
  • Without parameters: requires any authenticated user (valid JWT, any role)
  • With Roles: only users whose role claim matches are permitted
  • Comma-separated roles: the user must have at least one of the listed roles
  • Apply to a controller class to protect all its actions at once
  • Override with [AllowAnonymous] on individual actions when needed
// Any authenticated user
[Authorize]
public IActionResult GetAll() { ... }

// Admin only
[Authorize(Roles = "Admin")]
public IActionResult Delete(int id) { ... }

// Admin or Manager
[Authorize(Roles = "Admin,Manager")]
public IActionResult Approve(int id) { ... }

// Public — no token needed
[AllowAnonymous]
public IActionResult Health() { ... }
Controller-Level Role Guard
Controllers/AdminController.cs
[ApiController]
[Authorize(Roles = "Admin")]        // applies to all actions below
[Route("api/admin")]
public class AdminController : ControllerBase
{
    [HttpGet("users")]
    public IActionResult GetAllUsers() { ... }

    [HttpDelete("users/{id}")]
    public IActionResult DeleteUser(int id) { ... }

    // Override: allow any authenticated user to see stats
    [AllowAnonymous]
    [HttpGet("stats/public")]
    public IActionResult PublicStats() { ... }
}
Placing [Authorize] on the class locks every action by default — use [AllowAnonymous] to carve out public exceptions.
Policy-Based Authorization
Composing reusable named rules from roles, claims, and custom requirements
In this section
Defining policies in Program.cs
RequireRole, RequireClaim, RequireAssertion
Custom IAuthorizationRequirement
Using [Authorize(Policy="...")] on endpoints
Defining Policies
Program.cs
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CanManageTasks", policy =>
        policy.RequireAuthenticatedUser()
              .RequireRole("Admin", "Manager"));

    options.AddPolicy("PremiumUser", policy =>
        policy.RequireClaim("subscription", "premium"));

    options.AddPolicy("AtLeast18", policy =>
        policy.RequireAssertion(ctx =>
            ctx.User.HasClaim(c =>
                c.Type == "age" && int.Parse(c.Value) >= 18)));
});
Policies are registered once and referenced by name anywhere in the application — no repetition needed.
Using Policies on Endpoints
  • Reference a policy by its registered name in [Authorize(Policy="...")]
  • Works on controllers, actions, and Minimal API route handlers
  • Policies are evaluated after authentication — a missing token still returns 401
  • Multiple [Authorize] attributes on the same action are AND-ed together
Advantage over roles
Policies keep authorization logic in one place. When requirements change, update the policy definition — not every controller.
[Authorize(Policy = "CanManageTasks")]
[HttpDelete("{id}")]
public IActionResult Delete(int id) { ... }

[Authorize(Policy = "PremiumUser")]
[HttpGet("export")]
public IActionResult Export() { ... }

// Minimal API style
app.MapGet("/reports", () => Results.Ok())
   .RequireAuthorization("PremiumUser");
Custom Authorization Requirement

When built-in helpers are not enough, implement IAuthorizationRequirement and a matching handler.

Authorization/MinimumExperienceRequirement.cs
public class MinimumExperienceRequirement : IAuthorizationRequirement
{
    public int Years { get; }
    public MinimumExperienceRequirement(int years) => Years = years;
}

public class MinimumExperienceHandler
    : AuthorizationHandler<MinimumExperienceRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumExperienceRequirement requirement)
    {
        var claim = context.User.FindFirst("yearsOfExperience");
        if (claim != null && int.Parse(claim.Value) >= requirement.Years)
            context.Succeed(requirement);
        return Task.CompletedTask;
    }
}
Registering a Custom Requirement
  • Register the handler with the DI container as a singleton or scoped service
  • Add the policy referencing the requirement instance
  • ASP.NET Core discovers all registered IAuthorizationHandler implementations automatically
  • A single requirement can have multiple handlers — any one succeeding is enough
// Program.cs
builder.Services.AddSingleton<
    IAuthorizationHandler,
    MinimumExperienceHandler>();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("SeniorStaff", policy =>
        policy.Requirements.Add(
            new MinimumExperienceRequirement(5)));
});
Resource-Based Authorization
"Can this specific user access this specific record?"
In this section
Why role-based auth is not enough for ownership
AuthorizationHandler with a resource type
Injecting IAuthorizationService
Returning 403 when ownership check fails
Why Role-Based Auth Is Not Enough
  • Role-based auth answers: "can Admins delete tasks?" — yes or no for a whole group
  • Resource-based auth answers: "can Ahmed delete this specific task?" — depends on ownership
  • Without it, any authenticated Member can delete another Member's tasks by guessing the ID
  • The resource (the task record) must be loaded first, then the authorization check runs against it
  • IAuthorizationService is injected into the controller and called imperatively
Security note
Attribute-based checks run before the action executes. Resource-based checks run inside the action, after loading the record. Both are needed for complete protection.
Resource Authorization Handler
Authorization/TaskAuthorizationHandler.cs
public class TaskAuthorizationHandler
    : AuthorizationHandler<OperationAuthorizationRequirement, TaskItem>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        OperationAuthorizationRequirement requirement,
        TaskItem resource)
    {
        var userId = context.User
            .FindFirst(ClaimTypes.NameIdentifier)?.Value;

        if (resource.UserId.ToString() == userId
            || context.User.IsInRole("Admin"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
Admins bypass the ownership check. Everyone else must own the resource to proceed.
Calling the Handler from a Controller
Controllers/TasksController.cs
private readonly IAuthorizationService _authz;

public TasksController(IAuthorizationService authz) =>
    _authz = authz;

[Authorize]
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
    var task = await _repo.GetByIdAsync(id);
    if (task is null) return NotFound();

    var result = await _authz.AuthorizeAsync(
        User, task, Operations.Delete);
    if (!result.Succeeded) return Forbid();

    await _repo.DeleteAsync(task);
    return NoContent();
}
Named Operations
  • Use OperationAuthorizationRequirement to define named CRUD operations
  • Each operation is a singleton requirement — reuse them across multiple handlers
  • The same handler can branch on the operation name to apply different rules per action
  • Kept in a static class for easy, typo-free referencing throughout the codebase
public static class Operations
{
    public static readonly
        OperationAuthorizationRequirement
        Create = new() { Name = "Create" };

    public static readonly
        OperationAuthorizationRequirement
        Read = new() { Name = "Read" };

    public static readonly
        OperationAuthorizationRequirement
        Update = new() { Name = "Update" };

    public static readonly
        OperationAuthorizationRequirement
        Delete = new() { Name = "Delete" };
}
Key Concept
"Authentication asks who you are. Authorization asks what you are allowed to do — and to whom."
— Session 20
Lab — Protect Admin Endpoints

Work through these steps on your Task Management API project:

01
Add [Authorize(Roles = "Admin")] to the DELETE task endpoint and verify a Member JWT receives 403
02
Create a "CanCreateTasks" policy requiring role Member or Admin; apply it to the POST endpoint
03
Implement TaskAuthorizationHandler and register it, then add an ownership check inside the PUT and DELETE actions
04
Test in Postman: log in as a Member and try to delete another user's task — expect 403 Forbidden
Summary
1
Roles are coarse-grained; claims are fine-grained; policies compose both into reusable, named rules
2
[Authorize(Roles="Admin")] is the fastest way to restrict an endpoint — no configuration needed
3
Policies defined once in Program.cs keep authorization logic centralized and easy to update
4
Resource-based authorization is essential for ownership checks — load the record first, then call IAuthorizationService.AuthorizeAsync
5
401 means unauthenticated; 403 means authenticated but not permitted — never confuse the two
What's Next

Session 21 — API Security: Rate Limiting, CORS & HTTPS

  • Configuring rate limiting to prevent abuse and denial-of-service
  • Understanding CORS and setting up allowed origins for your frontend
  • Enforcing HTTPS and configuring HSTS in production
  • Security headers and what each one protects against
Before next session
Ensure your Task Management API has all three authorization layers working: role-based on admin endpoints, a custom policy on create, and ownership checks on update/delete.
Assignment

Extend your Task Management API with a complete authorization layer.

  • Add [Authorize(Roles = "Admin")] to a dedicated admin endpoint (e.g., GET /api/admin/users)
  • Define a "CanManageTasks" policy in Program.cs and apply it to DELETE and PUT task endpoints
  • Implement TaskAuthorizationHandler so users can only edit or delete tasks they own; Admins bypass this check
  • Return 403 Forbidden (not 404) when ownership check fails so the client knows the resource exists but is off-limits
Bonus
Create a second role — Moderator — that can edit any task but cannot delete tasks owned by Admins. Add the necessary logic to TaskAuthorizationHandler and write at least two Postman requests that demonstrate the difference.
Questions?
Session 20 — Authorization: Roles & Policies