Dynamic Policy Claims in ASP.NET Core using JWT Tokens (and Role Claims)
Alexandru Bucur
Posted on January 27, 2019
Hi guys, as a developer I bet everybody is a little bit 'lazy' and likes optimizing their workflow.
My latest aha moment of laziness was when I was going trough my pet project and working on implementing Policy-based authorization and deciding that adding AddPolicy every single time I want to implement a new Role Claim in the database is counter intuitive.
I want to say a big thank you to Jerrie Pelser for the initial work on this. I only needed to adapt a few things here and there to match the JWT Token creation for the Role Claims.
I highly recommend reading his article to understand better the inner workings of this little 'hack'.
To make things a little more organized I've added the classes in an Auth folder.
- JWT Token setup
Here's how I'm generating the JWT Token claims based on Role Claims.
private async Task<string> BuildToken(User user)
{
var claims = new List<Claim> {
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Sub, user.Id),
};
var userRoles = await _users.UserManager.GetRolesAsync(user);
foreach (var userRole in userRoles)
{
claims.Add(new Claim("role", userRole));
var role = await _users.RoleManager.FindByNameAsync(userRole);
if (role == null)
{
continue;
}
var roleClaims = await _users.RoleManager.GetClaimsAsync(role);
foreach (Claim roleClaim in roleClaims)
{
claims.Add(roleClaim);
}
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Issuer"],
claims: claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
In the database the ClaimType for the Role is saved as scope
- HasScopeRequirement
using System;
using Microsoft.AspNetCore.Authorization;
namespace Craidd.Auth
{
public class HasScopeRequirement : IAuthorizationRequirement
{
public string Issuer { get; }
public string Scope { get; }
public HasScopeRequirement(string scope, string issuer)
{
Scope = scope ?? throw new ArgumentNullException(nameof(scope));
Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer));
}
}
}
- AuthorizationPolicyProvider
This is where the automatic policy rezolver does it's job. It's checking if there are any policies already added with the name and then it adds the requirements.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace Craidd.Auth
{
public class AuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
{
private readonly AuthorizationOptions _options;
private readonly IConfiguration _config;
public AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options, IConfiguration config) : base(options)
{
_options = options.Value;
_config = config;
}
public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
// Check static policies first
var policy = await base.GetPolicyAsync(policyName);
if (policy == null)
{
policy = new AuthorizationPolicyBuilder()
.AddRequirements(new HasScopeRequirement(policyName, _config["Jwt:Issuer"]))
.Build();
// Add policy to the AuthorizationOptions, so we don't have to re-create it each time
_options.AddPolicy(policyName, policy);
}
return policy;
}
}
}
- HasScopeHandler
This is where we are handling the mapping. Our JWT Token is generating the scopes as an array already so we only need to do the basic checks.
The most important thing here to keep in mind is that you can inject your dbContext into HasScopeHandler and do more advanced checks.
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
namespace Craidd.Auth
{
public class HasScopeHandler : AuthorizationHandler<HasScopeRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement)
{
// If user does not have the scope claim, get out of here
if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer)) {
return Task.CompletedTask;
}
var scopes = context.User.FindAll(c => c.Type == "scope" && c.Issuer == requirement.Issuer).ToList();
// // Succeed if the scope array contains the required scope
if (scopes.Any(s => s.Value == requirement.Scope)) {
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
- Add the classes to your Startup.cs
services.AddAuthorization();
// register the scope authorization handler
services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
services.AddScoped<IAuthorizationHandler, HasScopeHandler>(); // AddScoped allows you to inject the dbContext
Posted on January 27, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.