By the end of this session, you will be able to:
[Authorize(Roles="Admin")]Program.cs| Time | Segment | Type | Duration |
|---|---|---|---|
| 0:00 | Roles vs claims vs policies | Theory | 15 min |
| 0:15 | Role-based authorization | Demo | 20 min |
| 0:35 | Policy-based authorization | Demo | 25 min |
| 1:00 | Resource-based authorization | Theory + Demo | 25 min |
| 1:25 | Lab — protect admin endpoints | Lab | 30 min |
| 1:55 | Wrap-up | Discussion | 5 min |
ASP.NET Core returns different HTTP status codes depending on why access was denied.
[Authorize] attributes to restrict endpoints by roleThe role must be added as a claim when the JWT is issued (Session 19). ASP.NET Core reads ClaimTypes.Role from the token automatically.
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"
};
ClaimTypes.Role must exactly match the string you pass to [Authorize(Roles="...")] — it is case-sensitive.
Roles: only users whose role claim matches are permitted[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() { ... }
[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() { ... }
}
[Authorize] on the class locks every action by default — use [AllowAnonymous] to carve out public exceptions.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)));
});
[Authorize(Policy="...")][Authorize] attributes on the same action are AND-ed together[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");
When built-in helpers are not enough, implement IAuthorizationRequirement and a matching handler.
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;
}
}
IAuthorizationHandler implementations automatically// Program.cs
builder.Services.AddSingleton<
IAuthorizationHandler,
MinimumExperienceHandler>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("SeniorStaff", policy =>
policy.Requirements.Add(
new MinimumExperienceRequirement(5)));
});
IAuthorizationService is injected into the controller and called imperativelypublic 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;
}
}
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();
}
OperationAuthorizationRequirement to define named CRUD operationspublic 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" };
}
Work through these steps on your Task Management API project:
[Authorize(Roles = "Admin")] to the DELETE task endpoint and verify a Member JWT receives 403"CanCreateTasks" policy requiring role Member or Admin; apply it to the POST endpointTaskAuthorizationHandler and register it, then add an ownership check inside the PUT and DELETE actions[Authorize(Roles="Admin")] is the fastest way to restrict an endpoint — no configuration neededProgram.cs keep authorization logic centralized and easy to updateIAuthorizationService.AuthorizeAsyncSession 21 — API Security: Rate Limiting, CORS & HTTPS
Extend your Task Management API with a complete authorization layer.
[Authorize(Roles = "Admin")] to a dedicated admin endpoint (e.g., GET /api/admin/users)"CanManageTasks" policy in Program.cs and apply it to DELETE and PUT task endpointsTaskAuthorizationHandler so users can only edit or delete tasks they own; Admins bypass this check403 Forbidden (not 404) when ownership check fails so the client knows the resource exists but is off-limitsTaskAuthorizationHandler and write at least two Postman requests that demonstrate the difference.