简体   繁体   中英

ASP.NET Core RequireClaim "scope" with multiple scopes

I'm developing an authorization system on ASP.NET Core 2.1 which requires both resource level and scope adherence before granting access. That is, I must be an author of a book (there can be multiple), and a must have the necessary scopes ("write.book", "read.book", "delete.book", etc). I successfully configured JWT in Startup.cs , and receive 401s when invalid tokens are passed. The issue I'm running into is enforcing the scopes. policy.RequireClaim("scope", "write.book") works when the access token only requires the one required scope, but always fails the access token contains multiple scopes "write.book delete.book" . How can I configure a policy to require a list of scopes which could be a subset of scopes an access token contains? I'm not seeing any Policy methods which accepts a list of scopes, so I'm assuming the framework is just performing a string comparison, which is why the authorization is failing. write.book != write.book delete.book . To clarify, if the policy only requires one scope write.book , but multiple are present in the access token write.book delete.book , the authorization fails.

The code below only works if the access token contains one scope, and fails if multiple are present.

authorization.AddPolicy("writeBookPolicy", policy => {
    policy.RequireAuthenticatedUser().AddAuthenticationSchemes("Bearer")
      .RequireClaim("scope", "write.book").Build();
});

{"scope": "write.book"} // Works
{"scope": "write.book delete.book"} //Fails

You have to use a more complicated claim check. Use RequireAssertion() instead and parse out the scope claim:

var scopes = new[] { "write.book", "delete.book" };
builder.RequireAssertion(context => {
   var claim = context.User.FindFirst("scope");
   if(claim == null) { return false; }
   return claim.Value.Split(' ').Any(s =>
      scopes.Contains(s, StringComparer.OrdinalIgnoreCase)
   );
});

I wrote a little extension method that, while less easy to read, is easier to use:

private static readonly IEnumerable<string> _scopeClaimTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
    "http://schemas.microsoft.com/identity/claims/scope",
    "scope"
};
public static AuthorizationPolicyBuilder RequireScope(this AuthorizationPolicyBuilder builder, params string[] scopes) {
    return builder.RequireAssertion(context =>
        context.User
            .Claims
            .Where(c => _scopeClaimTypes.Contains(c.Type))
            .SelectMany(c => c.Value.Split(' '))
            .Any(s => scopes.Contains(s, StringComparer.OrdinalIgnoreCase))
    );
}

Another option for this is spliting scope into multiple claims

        services.AddAuthentication("Bearer")
            .AddJwtBearer("Bearer", options =>
            {
                options.Authority = "https://localhost:5001";
                options.TokenValidationParameters = new() { ValidateAudience = false };

                options.Events = new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents()
                {
                    OnTokenValidated = async (context) =>
                    {
                        if (context.Principal?.Identity is ClaimsIdentity claimsIdentity)
                        {
                            var scopeClaims = claimsIdentity.FindFirst("scope");
                            if (scopeClaims is not null)
                            {
                                claimsIdentity.RemoveClaim(scopeClaims);
                                claimsIdentity.AddClaims(scopeClaims.Value.Split(' ').Select(scope => new Claim("scope", scope)));
                            }
                        }
                        await Task.CompletedTask;
                    }
                };
            });

These extension methods I've created allows you to add scope policy where all scopes are required or any of the scopes

    using Microsoft.AspNetCore.Authorization;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;

    public static class AuthorizationPolicyBuilder_Extentions
    {
        public static void AddPolicyScope_AllowedScopes(this AuthorizationOptions authorizationOptions, string policyName, params string[] allowedScopes) =>
            authorizationOptions.AddPolicy(policyName, policyBuilder => policyBuilder.RequireClaim("scope", allowedScopes));


        public static void AddPolicyScope_AllRequiredScopes(this AuthorizationOptions authorizationOptions, string policyName, params string[] requiredScopes) =>
            authorizationOptions.AddPolicy(policyName, policyBuilder => policyBuilder.RequireScopes(requiredScopes));

        private static AuthorizationPolicyBuilder RequireScopes(this AuthorizationPolicyBuilder builder, params string[] requiredScopes) =>
             builder.RequireAssertion(context => {
                 var userScopes = GetUserScopes(context);
                 return requiredScopes.All(scope => userScopes.Contains(scope, StringComparer.CurrentCulture));
             });

        private static IEnumerable<string> GetUserScopes(this AuthorizationHandlerContext context) =>
            context?.User?
                    .Claims
                    .Where(c => c.Type.Equals("scope"))
                    .SelectMany(c => c.Value.Split(' ')) ?? new List<string>();
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM