ASP.NET Core  

JWT Session 8: Implementing Role-Based Authorization in ASP.NET Core Web API Using JWT

In the previous article, we learned how ASP.NET Core stores user information as Claims after validating a JWT. We also saw how to access the current user's details using HttpContext.User.

But knowing who the user is is only half of the authentication process.

The next question is:

What is this user allowed to do?

For example:

  • A customer should be able to place an order.

  • An administrator should be able to manage products.

  • A manager should be able to view sales reports.

This is where Role-Based Authorization comes in.

In this article, you'll learn how roles work in ASP.NET Core, how to include them in a JWT, and how to protect API endpoints based on user roles.

Implementing Role-Based Authorization in ASP.NET Core Web API Using JWT

What Is Role-Based Authorization?

Role-Based Authorization is a way of controlling access based on the user's role.

Instead of giving every authenticated user the same permissions, the application checks the user's assigned role before allowing access to an endpoint.

For example:

UserRole
JohnCustomer
SarahAdmin
DavidManager

Although all three users are authenticated, they should not have access to the same resources.

Why Do We Need Roles?

Let's continue with our SecureShop API.

Suppose we have the following endpoints.

EndpointWho Can Access?
GET /api/productsEveryone
POST /api/ordersCustomer
POST /api/productsAdmin
DELETE /api/products/{id}Admin
GET /api/reportsManager

Without roles, every authenticated user would have identical permissions.

That would allow customers to delete products or administrators to perform actions they shouldn't.

Roles help prevent unauthorized access.

How Roles Work

The role is stored as a Claim inside the JWT.

For example:

User Id : 101
Name    : John
Role    : Customer

When ASP.NET Core validates the JWT, it automatically creates a user identity containing all claims, including the role.

Later, when a protected endpoint requires a specific role, ASP.NET Core compares the required role with the role stored in the authenticated user's claims.

Adding the Role Claim

In Session 4, we generated the JWT.

One of the claims looked like this:

new Claim(ClaimTypes.Role, user.Role)

If the user's role is:

Admin

Then the generated JWT contains:

Role → Admin

Similarly,

Customer

or

Manager

becomes part of the JWT.

This role travels with every authenticated request.

Protecting an Endpoint by Role

ASP.NET Core allows you to specify which role can access an endpoint.

Example:

[Authorize(Roles = "Admin")]
[HttpPost]
public IActionResult AddProduct()
{
    return Ok("Product Added");
}

Now only users whose JWT contains:

Role = Admin

can access this endpoint.

If any other user calls this API, access is denied.

Allowing Multiple Roles

Sometimes more than one role should access the same endpoint.

For example, both Managers and Admins can view reports.

[Authorize(Roles = "Admin,Manager")]
[HttpGet]
public IActionResult GetSalesReport()
{
    return Ok();
}

ASP.NET Core treats this as an OR condition.

The request succeeds if the user belongs to either role.

Applying Roles to an Entire Controller

Instead of protecting individual actions, you can protect the whole controller.

[Authorize(Roles = "Admin")]
[ApiController]
[Route("api/admin")]
public class AdminController : ControllerBase
{
    [HttpGet("dashboard")]
    public IActionResult Dashboard()
    {
        return Ok();
    }

    [HttpPost("product")]
    public IActionResult AddProduct()
    {
        return Ok();
    }
}

Now every action inside the controller requires the Admin role.

This keeps your code cleaner and avoids repeating the same attribute.

Internal Authorization Flow

Let's see what happens when a request reaches a role-protected endpoint.

Client
   │
   │ JWT
   ▼
Authentication Middleware
   │
   ▼
Validate JWT
   │
   ▼
Create ClaimsPrincipal
   │
   ▼
[Authorize(Roles="Admin")]
   │
   ▼
Does User Have Admin Role?
      │
 Yes  │  No
      │
      ▼
Controller Executes

Notice that the role check happens after authentication.

A user must first be authenticated before ASP.NET Core checks their role.

Practical Example

Let's continue with the SecureShop API.

Suppose two users log in.

Customer

Name : John
Role : Customer

Admin

Name : Sarah
Role : Admin

Now both users call:

POST /api/products

The endpoint is protected like this:

[Authorize(Roles = "Admin")]

John's Request

John is authenticated.

Role = Customer.

Required Role = Admin.

ASP.NET Core rejects the request.

403 Forbidden

Sarah's Request

Sarah is authenticated.

Role = Admin.

Required Role = Admin.

The controller executes successfully.

200 OK

Understanding 401 vs 403

Many beginners confuse these two responses.

They have different meanings.

Status CodeMeaning
401 UnauthorizedThe user is not authenticated.
403 ForbiddenThe user is authenticated but does not have permission.

Example:

  • No JWT → 401 Unauthorized

  • Customer accessing an Admin endpoint → 403 Forbidden

Understanding this difference makes debugging authentication and authorization issues much easier.

Common Mistakes

Mistake 1: Forgetting to Add the Role Claim

If the JWT doesn't contain a role claim, ASP.NET Core cannot perform role-based authorization.

Always include the role while generating the token.

Mistake 2: Confusing Authentication with Authorization

Authentication answers:

  • Who are you?

Authorization answers:

  • What are you allowed to do?

Both are required to secure an application.

Mistake 3: Using Roles for Every Permission

Roles work well for broad permissions such as:

  • Admin

  • Customer

  • Manager

However, they become difficult to manage when permissions become very detailed.

For fine-grained access control, ASP.NET Core provides Policy-Based Authorization, which we'll cover in a later article.

Mistake 4: Expecting Roles to Update Automatically

A JWT contains the user's role at the time the token is generated.

If a user's role changes in the database, existing tokens continue to use the old role until a new JWT is issued.

Key Takeaways

  • Role-Based Authorization controls what authenticated users are allowed to access.

  • The user's role is stored as a claim inside the JWT.

  • Use Authorize(Roles = "...") to restrict access to specific roles.

  • Multiple roles can be specified in a single attribute.

  • 401 Unauthorized means the user is not authenticated.

  • 403 Forbidden means the user is authenticated but does not have permission.

Summary

Role-Based Authorization builds on authentication by determining what an authenticated user is allowed to do. In ASP.NET Core, roles are stored as claims inside the JWT and are automatically available after the token is validated. By using the Authorize(Roles = "...") attribute, you can secure controllers and endpoints for specific user roles, support multiple roles, and clearly distinguish between authentication (401 Unauthorized) and authorization (403 Forbidden). Our SecureShop API can now identify users and restrict endpoints based on their roles. In the next article, we'll move beyond roles and explore Policy-Based Authorization, which provides a more flexible way to implement complex business rules such as age restrictions, membership levels, or custom access requirements.