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.
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.
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.
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.
To illustrate it more let us check the driver license example again
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"
}
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.
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
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>();
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"});
}
}
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)
};
Next we need to update the TodoController attribute to add the roles to it
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "AppUser")]
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"});
}
}
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"));
});
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);
}
Now let us test this
- Using the same account that we created earlier we need to add the claim to it
- We utilise the new endpoint we created http://localhost:8090/api/ClaimSetup/AddClaimToUser and we add the claim to the user account
- We try to access the http://localhost:8090/api/todo any other user who doesnt have the claim should not be able to access this.
Top comments (7)
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!
check if you have added the AllowAnyOrigin of your client app in your asp.net core project
// global cors policy
app.UseCors(x => x
.SetIsOriginAllowed(origin => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
Hi Mohamad! When i clicked on source code URL, it's show 404 Not found.
@moe23 Please update on source code url
Hi Mohamad! It is very usefull for me! But I've one doubt, i can't update Role and Users using PUT method. Can you explain how to do this. By the way very very thanks for this!
It is nice article , very usefull,THANKS