Authorization is the process of deciding whether an identity can perform an action based on claims, roles, or policies.
In the previous article, we enabled users to log in and obtain their JWT. That token is like their passport: it contains data about their identity.
Having a passport doesn’t mean you can enter the pilot’s cabin.
This is where Granular Authorization comes into play. Knowing the user is logged in ([Authorize]) is not enough; we need to know if they have permission to delete that product or view that invoice.
For this, ASP.NET Core uses three concepts that are often confused: Claims, Roles, and Policies. Today we will master them.
What is a Claim
A Claim is simply a piece of data: a key-value pair that describes something about the user. Think of your ID card or Driver’s License. It has several “Claims”:
- Name: John
- Date of Birth: 01/01/1985
- Can Drive Motorcycles: Yes
In the JWT world, this data travels inside the token’s Payload.
{
"sub": "123",
"email": "[email protected]",
"role": "Admin",
"vip": "true"
}When ASP.NET validates the token, it extracts this data and builds a User object (of type ClaimsPrincipal) that is available in all your controllers.
Reading Claims in the Controller
Do you need to know the current user’s ID to filter their orders? That data usually comes in the token, and you can read it from User.
[Authorize]
[HttpGet("my-data")]
public IActionResult GetMyData()
{
// User is a property available in ControllerBase
// Method 1: Search by standard type
var id = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var email = User.FindFirst(ClaimTypes.Email)?.Value;
// Method 2: Search for a custom claim
var isVip = User.FindFirst("vip")?.Value;
return Ok($"Hello user {id}, your email is {email}");
}Using claims avoids unnecessary database trips, but only for stable and non-sensitive data. If the data changes frequently or affects a critical decision, validate against your permission system.
Role-Based Authorization
The most traditional approach is to assign each user a label: “Admin”, “User”, “Manager”. In ASP.NET Core, this is extremely easy to implement.
You just need to ensure that, when generating the token (in the previous article), you added the Role type claim:
new Claim(ClaimTypes.Role, "Admin")Now, you can protect your routes like this:
// Only users with the Claim Role = "Admin"
[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public IActionResult DeleteProduct(int id)
{
return Ok("Product deleted");
}
// Users who are Admin OR Manager
[Authorize(Roles = "Admin,Manager")]
[HttpPut("{id}")]
public IActionResult EditProduct(int id) { ... }If a user with the role “User” tries to call DeleteProduct, they will automatically receive a 403 Forbidden.
Policy-Based Authorization
Roles are fine to start with, but they quickly become limiting. What if you want a user to delete products only if they are of legal age? Or if they have a “Pro” subscription? Are you going to create a role “AdminOfLegalAgePro”? That doesn’t scale.
That’s why Policies exist.
A policy is a logical rule defined in Program.cs and then applied in controllers. It decouples the rule (“CanDelete”) from the implementation (“IsAdmin”).
In Program.cs, when configuring services:
builder.Services.AddAuthorization(options =>
{
// Policy 1: Admins only (same as Roles, but encapsulated)
options.AddPolicy("IsAdmin", policy => policy.RequireRole("Admin"));
// Policy 2: VIP Users (Custom claim 'vip' == 'true')
options.AddPolicy("IsVip", policy => policy.RequireClaim("vip", "true"));
// Policy 3: More complex logic (e.g., Seniority > 2 years)
// This requires custom 'Requirements' which we will see in advanced courses
});In the controller, instead of Roles, use Policy.
[Authorize(Policy = "IsAdmin")]
[HttpDelete]
public IActionResult Delete() { ... }
[Authorize(Policy = "IsVip")]
[HttpGet("special-offers")]
public IActionResult GetOffers() { ... }The advantage is that if tomorrow you decide that to “Delete” you don’t need to be an Admin, but being a “Supervisor” is enough, you only change the policy definition in Program.cs. You don’t have to touch the 50 controllers where you used the attribute.
When to Use Each Option
| Tool | Use | Example |
|---|---|---|
| Claims | To read user data | User.FindFirst("email") |
| Roles | For simple and hierarchical permissions | [Authorize(Roles="Admin")] |
| Policies | For business rules or complex logic | [Authorize(Policy="CanExportExcel")] |
For small APIs, direct roles are sufficient. When authorization starts involving business rules, policies usually scale better because they concentrate the decision in a single place.