One new feature of ASP.NET Identity is Role Claims. Since there’s little documentation on how to use them I thought I’d put together a quick demo.
A Role Claim is a statement about a Role. When a user is a member of a role, they automatically inherit the role’s claims.
An example of where this feature could be used is for handling application permissions. Roles provide a mechanism to group related users. Permissions determine what members of those roles can do.
Here’s the code I’m using to set up my roles, permissions and role membership (warning, it’s demo quality):
public async Task<IActionResult> Setup()
{
var user = await userManager.FindByIdAsync(User.GetUserId());
var adminRole = await roleManager.FindByNameAsync("Admin");
if (adminRole == null)
{
adminRole = new IdentityRole("Admin");
await roleManager.CreateAsync(adminRole);
await roleManager.AddClaimAsync(adminRole, new Claim(CustomClaimTypes.Permission, "projects.view"));
await roleManager.AddClaimAsync(adminRole, new Claim(CustomClaimTypes.Permission, "projects.create"));
await roleManager.AddClaimAsync(adminRole, new Claim(CustomClaimTypes.Permission, "projects.update"));
}
if (!await userManager.IsInRoleAsync(user, adminRole.Name))
{
await userManager.AddToRoleAsync(user, adminRole.Name);
}
var accountManagerRole = await roleManager.FindByNameAsync("Account Manager");
if (accountManagerRole == null)
{
accountManagerRole = new IdentityRole("Account Manager");
await roleManager.CreateAsync(accountManagerRole);
await roleManager.AddClaimAsync(accountManagerRole, new Claim(CustomClaimTypes.Permission, "account.manage"));
}
if (!await userManager.IsInRoleAsync(user, accountManagerRole.Name))
{
await userManager.AddToRoleAsync(user, accountManagerRole.Name);
}
return Ok();
}
Here I’m defining two roles with the following permissions:
- Admin - View, Create, Update Projects
- Account Manager - Manage Accounts
I’ve got an API endpoint that spits out the user’s claims:
public IActionResult Index()
{
var claims = User.Claims.Select(claim => new { claim.Type, claim.Value }).ToArray();
return Json(claims);
}
After running the role setup I can see that my user has the permissions claims we set up for both roles:
[
{
"Type":"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
"Value":"d96ec201-d984-4cf8-a226-dc58d2a92b52"
},
{
"Type":"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
"Value":"ben@example.org"
},
{
"Type":"http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
"Value":"Admin"
},
{
"Type":"http://example.org/claims/permission",
"Value":"projects.view"
},
{
"Type":"http://example.org/claims/permission",
"Value":"projects.create"
},
{
"Type":"http://example.org/claims/permission",
"Value":"projects.update"
},
{
"Type":"http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
"Value":"Account Manager"
},
{
"Type":"http://example.org/claims/permission",
"Value":"account.manage"
}
]
The code for this is the built-in UserClaimsPrincipalFactory class:
if (UserManager.SupportsUserRole)
{
var roles = await UserManager.GetRolesAsync(user);
foreach (var roleName in roles)
{
id.AddClaim(new Claim(Options.ClaimsIdentity.RoleClaimType, roleName));
if (RoleManager.SupportsRoleClaims)
{
var role = await RoleManager.FindByNameAsync(roleName);
if (role != null)
{
id.AddClaims(await RoleManager.GetClaimsAsync(role));
}
}
}
}
Note that the above code doesn’t check for duplicate claims, so if a user is a member of roles that shared the same permissions they would end up with multiple permission claims of the same value.
You could customise this behaviour by providing your own implementation of IUserClaimsPrincipalFactory
which I covered in a previous post.
Checking for permissions
To then authorise user actions based on their permissions we can create a custom policy (one of the new authorisation features in ASP.NET Core):
services.AddAuthorization(options =>
{
options.AddPolicy("View Projects",
policy => policy.RequireClaim(CustomClaimTypes.Permission, "projects.view"));
});
This is applied by decorating our controller and/or actions with the Authorize attribute.
[Authorize("View Projects")]
public IActionResult Index(int siteId)
{
return View();
}
Tip: Use constants for policy and permission names.
In a future post I’ll cover the new authorisation features in more depth.