Evaluate multi value scope claim in ASP.NET Core authorization policy

How to evaluate a multi valued scope claim in the authorization process of a ASP.NET core application?

To add a scoped based authorization policy in an ASP.NET Core application you normally add code like this in the  ConfigureServices(IServiceCollection services) method of the Startup class:

services.AddAuthorization(
    options =>
    {
        options.AddPolicy(
            "HasReadScope",
            builder.RequireClaim("scope", "read"));
    });

And to secure the specific endpoint you decorate the controller (or method) with a Authorize attribute:

[Authorize(Policy = "HasReadScope")]
public IActionResult Get()
{
    return Ok();
}

So when you access the decorated API controller with a JWT Token which contains this single scope the request will be authorized and executed.

However the downside of this approach is that the internal ClaimsAuthorizationRequirement class compares the value of the token with your required claim. In this case "read". So when your access token contains a scope claim with a multi value like "read write delete" the authorization will fail because both values are not equal.

The scope claim is a space delimited list so the RequireClaim() helper will not work in this case.

What we want is an authorization check that evaluates that the scope value from the token contains one or more required scopes.

To do this I implemented a ScopeAuthorizationRequirement class which handles the required logic:

public class ScopeAuthorizationRequirement : AuthorizationHandler<ScopeAuthorizationRequirement>, IAuthorizationRequirement
{
    public IEnumerable<string> RequiredScopes { get; }

    public ScopeAuthorizationRequirement(IEnumerable<string> requiredScopes)
    {
        if (requiredScopes == null || !requiredScopes.Any())
        {
            throw new ArgumentException($"{nameof(requiredScopes)} must contain at least one value.", nameof(requiredScopes));
        }

        RequiredScopes = requiredScopes;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ScopeAuthorizationRequirement requirement)
    {
        if (context.User != null)
        {
            var scopeClaim = context.User.Claims.FirstOrDefault(
                c => string.Equals(c.Type, "scope", StringComparison.OrdinalIgnoreCase));

            if (scopeClaim != null)
            {
                var scopes = scopeClaim.Value.Split(" ", StringSplitOptions.RemoveEmptyEntries);
                if (requirement.RequiredScopes.All(requiredScope => scopes.Contains(requiredScope)))
                {
                    context.Succeed(requirement);
                }
            }
        }

        return Task.CompletedTask;
    }
}

The authorization requirement hereby splits the scope value into separate values and evaluates that all required scopes (one or more) are provided through the user token.

To use the requirement in a comfortable way I further implemented the following extensions:

public static class ScopeAuthorizationRequirementExtensions
{
    public static AuthorizationPolicyBuilder RequireScope(
        this AuthorizationPolicyBuilder authorizationPolicyBuilder,
        params string[] requiredScopes)
    {
        authorizationPolicyBuilder.RequireScope((IEnumerable<string>) requiredScopes);
        return authorizationPolicyBuilder;
    }

    public static AuthorizationPolicyBuilder RequireScope(
        this AuthorizationPolicyBuilder authorizationPolicyBuilder,
        IEnumerable<string> requiredScopes)
    {
        authorizationPolicyBuilder.AddRequirements(new ScopeAuthorizationRequirement(requiredScopes));
        return authorizationPolicyBuilder;
    }
}

So finally in the Startup class it is possible to configure the authorization policies like this:

services.AddAuthorization(
    options =>
    {
        options.AddPolicy(
            "HasReadScope",
            builder.RequireScope("read"));
        options.AddPolicy(
            "HasReadWriteScope",
            builder.RequireScope("read", "write"));
    });

Leave a Reply

Your email address will not be published. Required fields are marked *

two × three =