DEV Community

Alexandru Bucur
Alexandru Bucur

Posted on • Originally published at alexandrubucur.com on

Dynamic Policy Claims in ASP.NET Core using JWT Tokens (and Role Claims)

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);
}
Enter fullscreen mode Exit fullscreen mode

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));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)