The approach you describe seems correct. Everything depends upon your requirements.
Imagine you have several features in your application, if you choose to use roles, the code belonging to the feature must check everytime if the user is in a particular set of roles to use the feature. This approach becomes quite unmanageable when the features and roles grow, because you must take into account the combination of roles into every single feature. In this example, a user can perform the management operation X
only if it is PowerUser
or Administrator
. Now, this seems easy and strightforward, but, what happens if you add a new role, ALittleBitMorePowerful
, which is a User
who can also perform the X
operation. To achieve this result you have to review everything and change the checks (this implies retesting the whole thing).
If you designed the feature X
with a claim CanPerformX
, your introduce a layer of abstraction: your code will not care about the role of the user, but will check only for its own claim. If you ever rework how the claims are associated to the users, your effective code will not change (which in the end means no formal regressions have been introduced).
Roles are designed to be broad while Claims have been designed to be fine grain. However, as you read in the link, you may think a role as "big claim", or a claim as a "small role".
I post a small excerpt of a code of mine which supports custom roles but fixed claims.
Define the claims
internal static class PolicyClaims
{
public const string AdministratorClaim = @"http://myorganization/2019/administrator";
public const string Operation1Claim = @"http://myorganization/2019/op1";
public const string Operation2Claim = @"http://myorganization/2019/op2";
public const string ObtainedClaim = @"true";
}
Define the policies
internal static class Policies
{
public const string RequireAdministrator = "RequireAdministrator";
public const string RequireOp1 = "RequireOp1";
public const string RequireOp2 = "RequireOp2";
public const string AlwaysDeny = "AlwaysDeny";
public static void ConfigurePolicies(IServiceCollection services)
{
services.AddAuthorization(options => options.AddPolicy(RequireAdministrator, policy => policy.RequireClaim(PolicyClaims.AdministratorClaim)));
services.AddAuthorization(options => options.AddPolicy(RequireOp1, policy => policy.RequireClaim(PolicyClaims.Operation1Claim)));
services.AddAuthorization(options => options.AddPolicy(RequireOp2, policy => policy.RequireClaim(PolicyClaims.Operation2Claim)));
services.AddAuthorization(options => options.AddPolicy(AlwaysDeny, policy => policy.RequireUserName("THIS$USER\n\r\t\0cannot be created")));
}
}
Register the policies in Startup.RegisterServices
Policies.ConfigurePolicies(services);
Where you authenticate the user, decide which claims you need to add based upon your logic (omitted some parts to focus on the concepts)
[AllowAnonymous]
[Route("api/authentication/authenticate")]
[HttpPost()]
public async Task<IActionResult> Authenticate([FromBody] LoginModel model)
{
if (ModelState.IsValid)
{
var user = m_UserManager.Users.FirstOrDefault(x => x.UserName == model.UserName);
if (user == null)
{
...
}
else
{
var result = await m_SignInManager.CheckPasswordSignInAsync(user, model.Password, false);
if (result.Succeeded)
{
var handler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.Name, model.UserName)
}),
Expires = DateTime.UtcNow.AddHours(2),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(InstanceSettings.JWTKey), SecurityAlgorithms.HmacSha256Signature)
};
var roles = await m_UserManager.GetRolesAsync(user);
AddClaims(tokenDescriptor, roles);
var token = handler.CreateToken(tokenDescriptor);
var tokenString = handler.WriteToken(token);
return ...
}
else
{
...
}
}
}
return ...
}
private static void AddClaims(SecurityTokenDescriptor tokenDescriptor, IList<string> roles)
{
if (roles.Any(x => string.Equals(Constants.AdministratorRoleName, x, StringComparison.OrdinalIgnoreCase)))
{
tokenDescriptor.Subject.AddClaim(new Claim(PolicyClaims.AdministratorClaim, PolicyClaims.ObtainedClaim));
tokenDescriptor.Subject.AddClaim(new Claim(PolicyClaims.Operation1Claim, PolicyClaims.ObtainedClaim));
tokenDescriptor.Subject.AddClaim(new Claim(PolicyClaims.Operation2Claim, PolicyClaims.ObtainedClaim));
}
... query the database and add each claim with value PolicyClaims.ObtainedClaim ...
}
Finally, you can use the policies to protect your code:
[Authorize(Policy = Policies.RequireAdministrator)]
[HttpPost("execute")]
public async Task<IActionResult> ExecuteOperation([FromBody] CommandModel model)
{
...
}
Note that in this approach, I hardcoded certain claims to the administrator because I'd like to prevent the administrator removing certain claims. However, this is not mandatory.