How to handle array claim values in ASP.net Core using OIDC

Im running Skorubas implementation of IdentityServer4 https://github.com/skoruba/IdentityServer4.Admin

For some reason I end up receiving a single role-claim with the claim-type "role" and a value of an array with all roles for the current user: ["SkorubaIdentityAdminAdministrator","MyRole"]

Now if I would to protect a "page" using the Authorize-attribute: [Authorize(Role="MyRole")]

This would always end up with an access denied, since ASP.net Core expects multiple claims with the same claim-type, so in this case the claims would be

type | value

role:"SkorubaIdentityAdminAdministrator"

role:"MyRole"

is there any "best practice" to either parse the claims received and reformat them before they are processed by ASP.net core, or to tell the OpenIdConnect extension to handle the array-format as multiple claims?


Solution 1:

Generally claims received in JWTs can be arrays or objects as well as simple types. The way to deal with this when using .NET attributes for authorization is via policies.

They are pretty simple and this Curity tutorial has some examples. This code snippet shows that the entire ClaimsPrincipal is available to policies so you could work with array claims easily in your use case:

options.AddPolicy("lowRisk", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(claim => 
                claim.Type == "risk" && Int32.Parse(claim.Value) < 50
            )
        )
);

[HttpGet("lowrisk")]
[Authorize( Policy = "lowRisk")]
public IActionResult LowRisk()
{
    return Ok();
}

Solution 2:

Turns out that you can create your own ClaimActions, in the example above I had to do the following:

Firts of all.. create a new class:

public class RoleClaimAction : ClaimAction
{
    private const string RoleClaimType = "role";

    public RoleClaimAction() : base(RoleClaimType, ClaimValueTypes.String)
    {
    }
    
    public override void Run(JsonElement userData, ClaimsIdentity identity, string issuer)
    {
        //Map array of roles to separate role claims
        var roles = userData.TryGetStringArray(RoleClaimType)?.ToList();
        if (roles!.Any())
        {
            foreach (var role in roles!)
            {
                AddRoleClaim(identity, role, issuer);
            }

            return; 
        }

        //If we only have one role (not an array), add it as a single role claim
        var singleRole = userData.TryGetString(RoleClaimType);
        if(!string.IsNullOrEmpty(singleRole))
            AddRoleClaim(identity, singleRole, issuer);
    }

    private void AddRoleClaim(ClaimsIdentity identity, string role, string issuer)
    {
        identity.AddClaim(new Claim(JwtClaimTypes.Role, role, ClaimValueTypes.String, issuer));
    }
}

This will simply validate that the user has a claim called roles, and the re-map the array-values to separate role-claims, which then "hooks" into the auth-framework.

To add your ClaimAction, simply add it as the following to your OpenIdConnectOptions:

options.ClaimActions.Add(new RoleClaimAction())

Now Authorize-attributes with roles, and the User.IsInRole(string) should work properly.