Role- and Policy-Based Authorization: Deciding Who Is Allowed to Do What
We deepen authorization: what role-based permission is, where it falls short, how the policy and claim-based approach offers a more flexible solution, and how we set these up in .NET.
In the previous article we verified the user's identity with JWT and put a "role" piece of information inside the token. In the nineteenth article we saw a short permission example with [Authorize(Roles = "Admin")]. Today we combine those pieces and take on authorization seriously. We'll talk about two basic approaches: role-based and policy-based. Both answer the "who can access what?" question, but at different levels of flexibility.
Role-Based Authorization
This is the simplest and most common approach. You divide users into roles: like Admin, Editor, User. Then you specify which role each endpoint is open to. The role information inside the token was one of the claims we saw in the twentieth article; and authorization uses exactly this information.
[HttpDelete("{id}")]
[Authorize(Roles = "Admin")]
public IActionResult Delete(int id)
{
return Ok($"Record number {id} was deleted.");
}
This endpoint is open only to users with the "Admin" role. If someone authenticated but not an Admin arrives, they get a 403 Forbidden, as we discussed in the nineteenth article. Allowing more than one role is also possible; you separate the roles with commas:
[Authorize(Roles = "Admin,Editor")]
In this case, users in both the Admin and Editor roles can access. The role-based approach is intuitive, easy to read, and covers the needs of most applications. It's a perfect fit for clear distinctions like "administrators can delete, editors can edit, users can read."
Where Does the Role-Based Approach Fall Short?
Roles are great in simple scenarios, but the real world is often more complex. Let's say your rule is: "A user can access this content only if they're over 18." Or "Only users with a premium subscription can use this feature." These don't fit into a role; they're rules that look not at a role but at a specific attribute of the user.
If you try to solve everything with roles, role names get out of hand: you start deriving strange combinations like "Admin", "AdminOver18", "PremiumEditor". This leads to an unmanageable mess. And exactly at this point a more powerful tool comes in: policy-based authorization.
Policy-Based Authorization
A policy is the idea of giving a name to a permission rule and defining it in a single place. You can put in a policy not just a simple rule like "the role must be Admin," but any logic like "the user's age must be over 18." Then you use this policy's name in the endpoints. The rule itself stays in one place, while the endpoints just reference its name.
You define a policy in Program.cs. A simple example, a policy containing the "being at least 18 years old" rule:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Over18", policy =>
policy.RequireClaim("Age", "18", "19", "20", "21"));
});
This policy looks at the "Age" claim inside the token and grants permission if it carries one of the specified values. In real projects there are ways to write this rule more flexibly (instead of listing an age range one by one), but this example is enough for you to see the logic. Now you can apply this policy to an endpoint:
[HttpGet("adult-content")]
[Authorize(Policy = "Over18")]
public IActionResult GetAdultContent()
{
return Ok("This content is open only to users over 18.");
}
Notice: the attribute now takes Policy, not Roles. The rule itself lives not in the endpoint, but in the definition in Program.cs. This distinction brings a powerful advantage: you can use the same policy in dozens of endpoints, and when you need to change the rule, you fix a single place. While in the role-based approach the rule is embedded in the endpoint, in a policy the rule is centralized.
Thinking in Terms of Claims
The real idea behind policies is claims. In the twentieth article we defined claims as user information carried inside the token: their name, role, maybe age, subscription status. In fact, even a role is a type of claim; it's just information specially named "role."
This perspective is important because it pushes you to think more flexibly. Instead of asking "which role is this user in?", you ask "which attributes does this user have?" Their age, their country, their subscription level, whether their account is verified... All of these can be claims, and policies decide based on these claims. Roles are just one part of this picture; claims let you see the whole picture.
So Which One Should I Choose?
The two aren't rivals but complements. For simple and clear permission distinctions, the role-based approach is more than enough and is the easiest to read; use roles directly for classic distinctions like "administrator / editor / user." If your rule doesn't fit into a role, looks at a specific attribute of the user (like age, subscription, location), or you'll reuse the same rule in many places, move to a policy.
A practical piece of advice: start with roles, because they cover most needs and are the easiest to understand. When your rules start pushing the limits of roles, that is, when you have to derive strange role names, that's the sign you need to move to a policy. There's nothing wrong with using the two together in the same project; most real applications do exactly that.
A Small Experiment
Recall the login endpoint from the twentieth article. While producing the token, you were adding the user's role; now add one more claim, for example an "Age" piece of information. Then define a policy like "Over18" in Program.cs and protect an endpoint with this policy. Produce tokens for two different users: one over 18, one under. Make a request to the same endpoint with both tokens, and observe that one can enter and the other gets a 403. This experiment clearly shows how a policy is based on claims and how it goes beyond roles.
In the next article we'll complete the security block with a few important topics that harden the application against external threats: CORS, rate limiting, and basic security headers. Identity and permission are now in place; next is protecting the API against misuse and unwanted access.