Authentication answers "Who are you?". Authorization answers "Are you allowed to do this?". Once a user's token is authenticated, we must implement strict Authorization restrictions to ensure normal users cannot access Administrative endpoints.
If you included a ClaimTypes.Role in the JWT token (e.g., "Admin", "Manager", "User"), ASP.NET Core offers native attribute validation.
[ApiController]
[Route("api/admin")]
public class AdminController : ControllerBase
{
// Authenticated, but MUST strictly possess the "Admin" role claim
[Authorize(Roles = "Admin")]
[HttpDelete("fire-employee/{id}")]
public IActionResult FireEmployee(int id)
{ ... }
// Supports multiple roles (OR condition)
[Authorize(Roles = "Admin,Manager")]
[HttpGet("view-reports")]
public IActionResult ViewReports()
{ ... }
}
"Admin,Manager" everywhere creates brittle spaghetti code. When a company changes a job title from "Manager" to "Supervisor", you must hunt down and rewrite 50 attributes.
Instead of checking hardcoded roles, Policies allow you to define an abstract rule (e.g., "Require HR Clearance") in a central location. You then tag the controllers with the Policy. If the underlying logic changes, you only update it once.
builder.Services.AddAuthorization(options =>
{
// Simple Policy matching roles
options.AddPolicy("RequireHrClearance", policy =>
policy.RequireRole("Admin", "HR_Manager"));
// Complex Business Logic Policy evaluating specific claims
options.AddPolicy("MustBeAge21", policy =>
policy.RequireAssertion(context =>
{
var dobClaim = context.User.FindFirst("DateOfBirth");
if (dobClaim == null) return false;
var dob = DateTime.Parse(dobClaim.Value);
return DateTime.Today.Year - dob.Year >= 21;
}));
});
[Authorize(Policy = "RequireHrClearance")]
[HttpPost("hire")]
public IActionResult HireEmployee() { ... }
[Authorize(Policy = "MustBeAge21")]
[HttpGet("restricted-data")]
public IActionResult GetRestrictedData() { ... }
Sometimes Authorization logic cannot be decided via attributes because it depends on the precise data retrieved from the database. (e.g., "A user can only Edit an article if they are the original Author who wrote it.")
[HttpPut("{articleId}")]
[Authorize] // Just ensure they are logged in
public async Task<IActionResult> UpdateArticle(int articleId, ArticleDto dto)
{
var article = await _db.Articles.FindAsync(articleId);
if (article == null) return NotFound();
// Imperative check: Find who is currently logged in
var currentUserId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
// Verify ownership
if (article.AuthorId != currentUserId)
{
return Forbid(); // HTTP 403: You are authenticated, but not authorized for THIS specific record
}
article.Content = dto.Content;
await _db.SaveChangesAsync();
return Ok();
}
Q: "What is the exact HTTP Status Code difference between returning Unauthorized() and Forbid()?"
Architect Answer: "It is a massive architectural distinction. `Unauthorized()` produces an HTTP 401. It means 'I do not know who you are. Your token is missing, invalid, or expired. You must authenticate yourself first.' `Forbid()` produces an HTTP 403. It means 'I know exactly who you are. Your token is perfectly valid and authenticated. However, your specific user account lacks the necessary permissions (roles/policies) to access this specific resource.' A 401 means 'Login again.' A 403 means 'Go away, you aren't high enough rank to see this.'"