Role Based JWT Authentication in ASP.NET Core Web API

Role-Based JWT Authentication in ASP.NET Core Web API

In this article, I will discuss how to implement Role-Based JWT Authentication in ASP.NET Core Web API Applications. Please read our previous article on Implementing Refresh Token in ASP.NET Core Web API Applications using JWT. We will work with the same applications that we have worked on so far in our previous two articles.

Role-Based JWT Authentication in ASP.NET Core Web API

JWT (JSON Web Token) Role-Based Token Authentication is a powerful approach to secure ASP.NET Core Web API services. This approach allows us to authorize users based on their roles, providing or restricting access to various resources within our API.

Role-Based Access Control (RBAC) is a method of restricting system access to authorized users based on their role within an organization. In ASP.NET Core Web API, roles are often used to determine what actions a user can or cannot perform within the API.

In ASP.NET Core, we can use the [Authorize] attribute to secure individual controllers or actions. We can specify roles as a parameter to this attribute, like [Authorize(Roles = “Admin”)], which restricts access to only users who have the “Admin” role.

How Do We Implement Role-Based Authentication Using JWT in ASP.NET Core Web API?

Implementing role-based authentication using JWT in ASP.NET Core Web API applications involves several steps, such as generating tokens that include role claims, configuring the API to enforce role-based access, and modifying API controllers to use these role claims. Let us proceed and implement Role-Based JWT Token Authentication in an ASP.NET Core Web API application step by step.

Define Roles

We first need to define the roles that will be used in our application. Common roles include Admin, User, Guest, etc. These roles will be included in the JWT as claims when the token is generated.

Generate JWT with Role Claims

Next, we need to modify our token generation method to include role information in the claims. This is done when we authenticate the user and issue the token. In our JWTAuthServer application, please modify the AuthController as follows. Here, as you can see, we have already added the roles as claims while generating the JWT Token.

using JWTAuthServer.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace JWTAuthServer.Controllers
{
[Route("api/[controller]")]  
[ApiController]  
public class AuthController : ControllerBase
{
private readonly IConfiguration _configuration; // Configuration object to access application settings.
private readonly TokenService _tokenService; // Service for managing refresh tokens.
// Constructor with dependency injection for configuration and the token service.
public AuthController(IConfiguration configuration, TokenService tokenService)
{
_configuration = configuration; // Initialize configuration.
_tokenService = tokenService; // Initialize token service.
}
// Action method to handle login requests. It expects a Login model in the request body.
[HttpPost("Login")]
public async Task<IActionResult> Login([FromBody] Login request)
{
// Validate the model state (input validation).
if (!ModelState.IsValid)
{
return BadRequest("Invalid request body.");  // Return bad request if the model state is invalid.
}
try
{
// Attempt to find a user that matches the provided username and password.
var user = UserStore.Users.FirstOrDefault(u => u.Username == request.Username && u.Password == request.Password);
// Check if user was not found.
if (user == null)
{
return Unauthorized("Invalid user credentials.");  // Return unauthorized if no user is found.
}
// Issue a new access token for the user.
var accessToken = IssueAccessToken(user);
// Generate a new refresh token.
var refreshToken = GenerateRefreshToken();
// Save the new refresh token with associated user information.
await _tokenService.SaveRefreshToken(user.Username, refreshToken);
// Return the generated access and refresh tokens.
return Ok(new { AccessToken = accessToken, RefreshToken = refreshToken });
}
catch (Exception ex)
{
// Handle any exceptions that occur during the login process.
return StatusCode(500, $"Internal server error: {ex.Message}");  // Return a 500 internal server error on exception.
}
}
// Generates an access token for the specified user.
private string IssueAccessToken(User user)
{
// Create a new symmetric security key from the configured JWT key.
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
// Setup the signing credentials to use for the token.
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
// Prepare claims to be included in the token.
var claims = new List<Claim>
{
new Claim("Myapp_User_Id", user.Id.ToString()),  // Custom claim for user ID.
new Claim(ClaimTypes.NameIdentifier, user.Username),  // Standard claim for user identifier.
new Claim(ClaimTypes.Email, user.Email),  // Standard claim for user's email.
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString())  // JWT standard claim for subject.
};
// Add role claims for the user.
user.Roles.ForEach(role => claims.Add(new Claim(ClaimTypes.Role, role)));
// Create the JWT token using the claims, issuer, audience, etc., and set its expiration.
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.Now.AddMinutes(30),  // Set access token to expire in 30 minutes.
signingCredentials: credentials);
// Return the serialized token.
return new JwtSecurityTokenHandler().WriteToken(token);
}
// Generates a new refresh token using a cryptographic random number generator.
private string GenerateRefreshToken()
{
var randomNumber = new byte[32];  // Prepare a buffer to hold the random bytes.
using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);  // Fill the buffer with cryptographically strong random bytes.
return Convert.ToBase64String(randomNumber);  // Convert the bytes to a Base64 string and return.
}
}
// Refreshes an access token using a valid refresh token.
[HttpPost("RefreshToken")]
public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequest request)
{
// Validate the refresh token request.
if (request == null || string.IsNullOrWhiteSpace(request.RefreshToken))
{
return BadRequest("Refresh token is required.");  // Return bad request if no refresh token is provided.
}
try
{
// Retrieve the username associated with the provided refresh token.
var username = await _tokenService.RetrieveUsernameByRefreshToken(request.RefreshToken);
if (string.IsNullOrEmpty(username))
{
return Unauthorized("Invalid refresh token.");  // Return unauthorized if no username is found (invalid or expired token).
}
// Retrieve the user by username.
var user = UserStore.Users.FirstOrDefault(u => u.Username == username);
if (user == null)
{
return Unauthorized("Invalid user.");  // Return unauthorized if no user is found.
}
// Issue a new access token and refresh token for the user.
var accessToken = IssueAccessToken(user);
var newRefreshToken = GenerateRefreshToken();
// Save the new refresh token.
await _tokenService.SaveRefreshToken(user.Username, newRefreshToken);
// Return the new access and refresh tokens.
return Ok(new { AccessToken = accessToken, RefreshToken = newRefreshToken });
}
catch (Exception ex)
{
// Handle any exceptions during the refresh process.
return StatusCode(500, $"Internal server error: {ex.Message}");  // Return a 500 internal server error on exception.
}
}
// Revokes a refresh token to prevent its future use.
[HttpPost("RevokeToken")]
public async Task<IActionResult> RevokeToken([FromBody] RefreshTokenRequest request)
{
// Validate the revocation request.
if (request == null || string.IsNullOrWhiteSpace(request.RefreshToken))
{
return BadRequest("Refresh token is required.");  // Return bad request if no refresh token is provided.
}
try
{
// Attempt to revoke the refresh token.
var result = await _tokenService.RevokeRefreshToken(request.RefreshToken);
if (!result)
{
return NotFound("Refresh token not found.");  // Return not found if the token does not exist.
}
return Ok("Token revoked.");  // Return success message if the token is successfully revoked.
}
catch (Exception ex)
{
// Handle any exceptions during the revocation process.
return StatusCode(500, $"Internal server error: {ex.Message}");  // Return a 500 internal server error on exception.
}
}
}
}
Modifying UserStore:

Please make sure to initialize the Roles property of each user. So, please modify the UserStore class as follows:

namespace JWTAuthServer.Models
{
public class UserStore
{
// Declare a public static list of User objects named 'Users'.
// Being static, this list is shared across all instances of the UserStore class and accessible without creating an instance of the class.
// This list is initialized with predefined user data.
public static List<User> Users = new List<User>
{
new User { Id=1, Username = "admin", Password = "password", Email="admin@Example.com", Roles = new List<string> { "Admin" } },
new User { Id=2, Username = "user", Password = "password", Email="user@Example.com", Roles = new List<string> { "User" } },
new User { Id=3, Username = "test", Password = "password", Email="test@Example.com", Roles = new List<string> { "Admin", "User" } }
};
}
}
Configure Authentication and Authorization Services

Ensure the authentication service is configured to validate Role claims. This needs to be checked in the APIResourceServer application. So, modify the Program class file of the APIResourceServer application as follows:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
using System.Text;
namespace APIResourceServer
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
// This will use the property names as defined in the C# model
options.JsonSerializerOptions.PropertyNamingPolicy = null;
});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Configure JWT authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
RoleClaimType = ClaimTypes.Role // Ensure the JWT token role claim is recognized correctly
};
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// Registers the authentication middleware that uses the default authentication scheme set by AddAuthentication.
app.UseAuthentication();
// Registers the authorization middleware to enforce authorization policies on the app's routes.
app.UseAuthorization();
// Maps controller routes.
app.MapControllers();
app.Run();
}
}
}
Enforce Role-Based Authorization

Use the role claims in the JWT to enforce access control on API endpoints. We can do this using the [Authorize] attribute with roles specified. So, modify the UsersController of our APIResourceServer application as follows:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace APIResourceServer.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
// Static list to simulate database of users.
private static List<User> _users = new List<User>
{
new User { Id = 1, Username = "admin", Email = "admin@example.com" },
new User { Id = 2, Username = "user", Email = "user@example.com" },
new User { Id = 3, Username = "test", Email="test@example.com" }
};
// GET method to retrieve all users.
[HttpGet]
//To secure the endpoints with JWT authentication, we need to apply the [Authorize] attribute
[Authorize(Roles ="Admin")]
public ActionResult<List<User>> GetAllUsers()
{
// Return the static list of users.
return _users;
}
// GET method with a parameter to retrieve a specific user by their ID.
[HttpGet("{id}")]
[Authorize(Roles = "User")]
public ActionResult<User> GetUser(int id)
{
// Searches the list for a user with the specified ID.
var user = _users.FirstOrDefault(u => u.Id == id);
// If no user is found, return a 404 Not Found response.
if (user == null)
return NotFound();
// If found, return the user.
return user;
}
// POST method to create a new user.
[HttpPost]
[Authorize(Roles = "Admin,User")]
public ActionResult<User> CreateUser([FromBody] User user)
{
// Adds the new user to the list.
_users.Add(user);
// Return a response with the location of the newly created user.
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}
// PUT method to update an existing user.
[HttpPut("{id}")]
[Authorize(Roles = "Admin")]
[Authorize(Roles = "User")]
public ActionResult<User> UpdateUser(int id, [FromBody] User user)
{
// Find the index of the user in the list.
var index = _users.FindIndex(u => u.Id == id);
// If no user is found at that index, return a 404 Not Found.
if (index == -1)
return NotFound();
// Update the user at the specific index.
_users[index] = user;
// Return a 204 No Content response, indicating the update was successful.
return NoContent();
}
// DELETE method to remove a user by ID.
[HttpDelete("{id}")]
[Authorize]
public IActionResult DeleteUser(int id)
{
// Find the index of the user to be deleted.
var index = _users.FindIndex(u => u.Id == id);
// If no user is found at that index, return a 404 Not Found.
if (index == -1)
return NotFound();
// Remove the user from the list.
_users.RemoveAt(index);
// Return a 204 No Content response, indicating the deletion was successful.
return NoContent();
}
}
// Definition of the User model used in the controller.
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
}
}

Let us understand which endpoints in the UsersController are accessible by which roles based on the [Authorize] attributes applied:

GetAllUsers (GET /api/Users)
  • Role Allowed: Admin
  • This endpoint retrieves all users and is restricted to users with the “Admin” role.
GetUser (GET /api/Users/{id})
  • Role Allowed: User
  • This endpoint retrieves a specific user by their ID and is restricted to users with the “User” role.
CreateUser (POST /api/Users)
  • Roles Allowed: Admin, User
  • This endpoint allows the creation of a new user and can be accessed by users with either “Admin” or “User” roles.
UpdateUser (PUT /api/Users/{id})
  • Roles Allowed: Admin, User
  • This endpoint updates an existing user. There are two separate [Authorize] attributes with “Admin” and “User” roles, which means that users with both roles can access this endpoint.
DeleteUser (DELETE /api/Users/{id})
  • Role Allowed: Not specified
  • This endpoint deletes a user by ID. The [Authorize] attribute is used without specifying roles, which means any authenticated user can access this endpoint regardless of their role.

Note: No Changes are required in the client Application. Now, run the application and test the functionalities with different users and roles. It should work as expected.

In the next article, I will discuss how to implement Client Validation using JWT Authentication in the ASP.NET Core Web API Application. In this article, I explain How to Implement Role-Based JWT Authentication in ASP.NET Core Web API Application with an Example. I hope you enjoy this article.

Leave a Reply

Your email address will not be published. Required fields are marked *