DEV Community

Cover image for Asp Net Core - Rest API Authorization with JWT (Roles Vs Claims Vs Policy) - Step by Step
Mohamad Lawand
Mohamad Lawand

Posted on

Asp Net Core - Rest API Authorization with JWT (Roles Vs Claims Vs Policy) - Step by Step

In this article we will go through AspNet Core Authorisation (Roles, Claims and Policies). When do you want to use each and give you a better understanding on they fit together.

So what we will cover today:

  • Authentication vs Authorisation
    • What is Authentication
    • What is Authorisation
  • Authorisation type
    • What is a Role
    • What is a Claim
    • What is a Policy
  • Ingredients
  • Code and Implementations

You can watch the full video on YouTube

You can find the source code on GitHub
https://github.com/mohamadlawand087/v48-AspNetCore-Authorisation

This is Part 4 of API dev series you can check the different parts by following the links:
Part 1:https://dev.to/moe23/asp-net-core-5-rest-api-step-by-step-2mb6
Part 2: https://dev.to/moe23/asp-net-core-5-rest-api-authentication-with-jwt-step-by-step-140d
Part 3: https://dev.to/moe23/refresh-jwt-with-refresh-tokens-in-asp-net-core-5-rest-api-step-by-step-3en5

Authentication vs Authorisation

Before we dive into this topic too deep, despite the similar-sounding terms, authentication and authorisation are separate steps in the login process.

Authentication

Authentication is the act of validating that users are whom they claim to be. This is the first step in any security process.

Logging into your email or unlocking your phone is a form of authenticaiton, where you are required to give some sort of credentials so the system will let you in and you can view your information.

Authentication can take many forms:

  • Passwords. Usernames and passwords ****are the most common authentication factors. If a user enters the correct data, the system assumes the identity is valid and grants access.
  • One-time pins. Grant access for only one session or transaction.
  • Authentication apps. Generate security codes via an outside party that grants access.
  • Biometrics. A user presents a fingerprint or eye scan to gain access to the system.

In some instances, systems require the successful verification of more than one factor before granting access. This multi-factor authentication (MFA) requirement is often deployed to increase security beyond what passwords alone can provide.

Image description

Authorisation:

we first need to define what authentication actually is, and more importantly, what it’s not. Refers to the process that determines what a user is able to do.

In other words, Authorization proves you have the right to make a request. When you try to go backstage at a concert or an event, you don’t necessarily have to prove that you are who you say you are – you show the ticket, which is the proof that you have the right to be where you’re trying to get into.

Authorization is independent from authentication. However, authorization requires an authentication mechanism.

Roles:

They are a set of permissions to do certain activities in the application. We can think of a role as if its a boolean wether we have this role or not, true or false.

Image description

So what we do with roles is we attach functionality to a role and once we assign a user to a role those set of functionalities are set to the user. Once we remove the role these functionalities are removed.

Image description

A role will protect access to the funciton, without the user having that correct role the user will not be able to execute that function

Claims:

They are completely different from Roles, Claim based is more flexible then roles they are key value pair. The claim belong to a user or an entity and claim is used to describe the user or the entity. Claims are essentially user properties and they inform the authorisation about the user.

Image description

To illustrate it more let us check the driver license example again

Image description

We can see here that there is 11 claims on this licesne which basically mean there is 11 pieces of information about the user. So if we want to translate this into a code based structure it will be something like this

{
    "dl":"123456789",
    "exp":"07/11/2025",
    "ln":"DOE",
    "fn":"John",
  "dob":"09/05/1993",
  "sex":"M",
    "hair":"brn",
    "eyes":"blue",
    "hgt":"6.0"
    "wgt":"183lb",
  "class":"C"
}
Enter fullscreen mode Exit fullscreen mode

So these claims will be given to the user once they log in.

Claims can work with roles or with out roles, based on how we want to implement the authorisation process.

Policy:

They are functions or rules which are used to check the user information and check if permission is granted or denied.

Image description

Policies which basically starts with the context which checks the user against a policy list and based on the list it will either grant or deny permision to the requested resource.

Role based authrisation and Claims based authorisation use requirements, a requirements handler and a pre-configured policy. Policy consist of one or more requirements

Image description

Roles vs Claims vs Policy

A role is a symbolic category that collects together users who share the same levels of security privileges. Role-based authorization requires first identifying the user, then ascertaining the roles to which the user is assigned, and finally comparing those roles to the roles that are authorized to access a resource.

In contrast, a claim is not group based, rather it is identity based.

Code

We will continue building on the last project that we used authorisation with JWT token you can find the source code on github

https://github.com/mohamadlawand087/v8-refreshtokenswithJWT

Once we clone this repo we can start building our authorisation

The first thing we need to do is to update the startup class to include Roles in our identity providers. Inside the ConfigureServices in the Startup class we need to update the following

services.AddIdentity<IdentityUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
                        .AddEntityFrameworkStores<ApiDbContext>();
Enter fullscreen mode Exit fullscreen mode

Then we need to do is to create a new controller called SetupController inside the controller folder and add the following

[Route("api/[controller]")] // api/setup
    [ApiController]
    public class SetupController: ControllerBase
    {
        private readonly ApiDbContext _context;
        private readonly RoleManager<IdentityRole> _roleManager;
        private readonly UserManager<IdentityUser> _userManager;
        protected readonly ILogger<SetupController> _logger;

        public SetupController(
            ApiDbContext context,
            RoleManager<IdentityRole> roleManager,
            UserManager<IdentityUser> userManager,
            ILogger<SetupController> logger)
        {   
            _logger = logger;
            _roleManager = roleManager;
            _userManager = userManager;
            _context = context;
        }

        [HttpGet]
        public IActionResult GetAllRoles()
        {
            var roles = _roleManager.Roles.ToList();
            return Ok(roles);
        }

        [HttpPost]
        public async Task<IActionResult> CreateRole(string roleName)
        {
            var roleExist = await _roleManager.RoleExistsAsync(roleName);
            if (!roleExist) {
                //create the roles and seed them to the database: Question 1
                var roleResult = await _roleManager.CreateAsync (new IdentityRole (roleName));

                if (roleResult.Succeeded) {
                    _logger.LogInformation (1, "Roles Added");
                    return Ok(new {result = $"Role {roleName} added successfully"});
                } else {
                    _logger.LogInformation (2, "Error");
                    return BadRequest(new {error = $"Issue adding the new {roleName} role"});
                }
            }

            return BadRequest(new {error = "Role already exist"});
        }

        // Get all users
        [HttpGet]
        [Route("GetAllUsers")]
        public async Task<IActionResult> GetAllUsers()
        {
            var users = await _userManager.Users.ToListAsync();
            return Ok(users);
        }

        // Add User to role
        [HttpPost]
        [Route("AddUserToRole")]
        public async Task<IActionResult> AddUserToRole(string email, string roleName)
        {
            var user = await _userManager.FindByEmailAsync(email);

            if(user != null)
            {
                var result = await _userManager.AddToRoleAsync(user, roleName);

                if(result.Succeeded)
                {
                    _logger.LogInformation (1, $"User {user.Email} added to the {roleName} role");
                    return Ok(new {result = $"User {user.Email} added to the {roleName} role"});
                }
                else
                {
                    _logger.LogInformation (1, $"Error: Unable to add user {user.Email} to the {roleName} role");
                    return BadRequest(new {error = $"Error: Unable to add user {user.Email} to the {roleName} role"});
                }
            }

            // User doesn't exist
            return BadRequest(new {error = "Unable to find user"});
        }

        // Get specific user role
        [HttpGet]
        [Route("GetUserRoles")]
        public async Task<IActionResult> GetUserRoles(string email)
        {
            // Resolve the user via their email
            var user = await _userManager.FindByEmailAsync(email);
            // Get the roles for the user
            var roles = await _userManager.GetRolesAsync(user);
            return Ok(roles);
        }

        // Remove User to role
        [HttpPost]
        [Route("RemoveUserFromRole")]
        public async Task<IActionResult> RemoveUserFromRole(string email, string roleName)
        {
            var user = await _userManager.FindByEmailAsync(email);

            if(user != null)
            {
                var result = await _userManager.RemoveFromRoleAsync(user, roleName);

                if(result.Succeeded)
                {
                    _logger.LogInformation (1, $"User {user.Email} removed from the {roleName} role");
                    return Ok(new {result = $"User {user.Email} removed from the {roleName} role"});
                }
                else
                {
                    _logger.LogInformation (1, $"Error: Unable to removed user {user.Email} from the {roleName} role");
                    return BadRequest(new {error = $"Error: Unable to removed user {user.Email} from the {roleName} role"});
                }
            }

            // User doesn't exist
            return BadRequest(new {error = "Unable to find user"});
        }
    }
Enter fullscreen mode Exit fullscreen mode

Once we finished with the SetupController let us move to the AuthManagement Controller and update the following

// We need to add the following before the constructor
private readonly RoleManager<IdentityRole> _roleManager;
protected readonly ILogger<AuthManagementController> _logger;

// We need to update the constructor to the following
public AuthManagementController(
          UserManager<IdentityUser> userManager,
          RoleManager<IdentityRole> roleManager,
          IOptionsMonitor<JwtConfig> optionsMonitor,
          TokenValidationParameters tokenValidationParams,
          ILogger<AuthManagementController> logger,
          ApiDbContext apiDbContext)
{
    _logger = logger;
    _userManager = userManager;
    _roleManager = roleManager;
    _jwtConfig = optionsMonitor.CurrentValue;
    _tokenValidationParams = tokenValidationParams;
    _apiDbContext = apiDbContext;
}

// We need to create a GetValidClaims method
private async Task<List<Claim>> GetValidClaims(IdentityUser user)
{
    IdentityOptions _options = new IdentityOptions();
    var claims = new List<Claim>
    {
        new Claim("Id", user.Id), 
        new Claim(JwtRegisteredClaimNames.Email, user.Email),
        new Claim(JwtRegisteredClaimNames.Sub, user.Email),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new Claim(_options.ClaimsIdentity.UserIdClaimType, user.Id.ToString()),
        new Claim(_options.ClaimsIdentity.UserNameClaimType, user.UserName),
    };

    var userClaims = await _userManager.GetClaimsAsync(user);
    var userRoles = await _userManager.GetRolesAsync(user);
    claims.AddRange(userClaims);
    foreach (var userRole in userRoles)
    {
        claims.Add(new Claim(ClaimTypes.Role, userRole));
        var role = await _roleManager.FindByNameAsync(userRole);
        if(role != null)
        {
            var roleClaims = await _roleManager.GetClaimsAsync(role);
            foreach(Claim roleClaim in roleClaims)
            {
                claims.Add(roleClaim);
            }
        }
    }
    return claims;
}

// We need to update the GenerateJwtToken method
var claims = await GetValidClaims(user);

var tokenDescriptor = new SecurityTokenDescriptor
{
    Subject = new ClaimsIdentity(claims),
    Expires = DateTime.UtcNow.AddMinutes(5), // 5-10 
    SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
Enter fullscreen mode Exit fullscreen mode

Next we need to update the TodoController attribute to add the roles to it

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "AppUser")]
Enter fullscreen mode Exit fullscreen mode

Now let us give this a try

  • Will create a new user
  • Will create a role called AppUser
  • Will assign the role to the user
  • Will login and get a JWT token
  • Will try to access GetItems endpoint

Now we start by adding our ClaimSetup Controller, inside the controller folder will add a new class called ClaimSetupController and will add the following

[Route("api/[controller]")] // api/ClaimSetup
[ApiController]
public class ClaimSetupController : ControllerBase
{
    private readonly ApiDbContext _context;
    private readonly RoleManager<IdentityRole> _roleManager;
    private readonly UserManager<IdentityUser> _userManager;
    protected readonly ILogger<ClaimSetupController> _logger;

    public ClaimSetupController(
        ApiDbContext context,
        RoleManager<IdentityRole> roleManager,
        UserManager<IdentityUser> userManager,
        ILogger<ClaimSetupController> logger)
    {   
        _logger = logger;
        _roleManager = roleManager;
        _userManager = userManager;
        _context = context;
    }

    [HttpGet]
    public async Task<IActionResult> GetAllClaims(string email)
    {
        var user = await _userManager.FindByEmailAsync(email);

        var claims = await _userManager.GetClaimsAsync(user);

        return Ok(claims);
    }

    // Add Claim to user
    [HttpPost]
    [Route("AddClaimToUser")]
    public async Task<IActionResult> AddClaimToUser(string email, string claimName, string value)
    {
        var user = await _userManager.FindByEmailAsync(email);

        var userClaim = new Claim(claimName, value);

        if(user != null)
        {
            var result = await _userManager.AddClaimAsync(user, userClaim);

            if(result.Succeeded)
            {
                _logger.LogInformation (1, $"the claim {claimName} add to the  User {user.Email}");
                return Ok(new {result = $"the claim {claimName} add to the  User {user.Email}"});
            }
            else
            {
                _logger.LogInformation (1, $"Error: Unable to add the claim {claimName} to the  User {user.Email}");
                return BadRequest(new {error = $"Error: Unable to add the claim {claimName} to the  User {user.Email}"});
            }
        }

        // User doesn't exist
        return BadRequest(new {error = "Unable to find user"});
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we need to update the Startup class to create a Claims Policy, inside the Startup.cs in the root directoty we need to add the following in the ConfigureServices method

services.AddAuthorization(options =>
  {
      options.AddPolicy("ViewItemsPolicy",
          policy => policy.RequireClaim("ViewItems"));
  });
Enter fullscreen mode Exit fullscreen mode

Next we need to update the TodoController with the following on any action we want

[HttpGet]
[Authorize(Policy = "ViewItemsPolicy")]
public async Task<IActionResult> GetItems()
{
    var items = await _context.Items.ToListAsync();
    return Ok(items);
}
Enter fullscreen mode Exit fullscreen mode

Now let us test this

Discussion (1)

Collapse
manniealfaro profile image
Manuel

Hi Mohamad! VERY THANKS for this series! It is very usefull for me! But I've one doubt. I've evrything configured in my API, but how I can configure my client to send the tokens in the requests to the API? So once I'm logued in I can retrive any data from the API? Thanks a lot!