简体   繁体   中英

ASP.NET Core AuthorizationHandler not being called

I am trying to add some custom role based authorisation , but I am unable to get the Startup configured to call my AuthorizationHandler .

I found some related information on GitHub: here . Is this a bug or not?

I am using ASP.NET Core 3.1 and my initializaion is as follows:

1: This retrieves the url/roles from the database using Dapper ORM:

private List<UrlRole> GetRolesRoutes()
{
    var urlRole = DapperORM.ReturnList<UrlRole>("user_url_role_all");
    return urlRole.Result.ToList();
}

2: In my Startup, I get the url/roles and store the result in a global variable:

public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
    this.environment = env;
    UrlRoles = GetRolesRoutes();
}

3: My Configuration is: Note the UrlRoles which is passed along

 public void ConfigureServices(IServiceCollection services)
 {
     // .. snip   
     services.AddAuthorization(o =>
     o.AddPolicy(_RequireAuthenticatedUserPolicy,
            builder => builder.RequireAuthenticatedUser()));

     services.AddAuthorization(options =>
     {
         options.AddPolicy("Roles", policy =>
         policy.Requirements.Add(new UrlRolesRequirement(UrlRoles)));
     });


    services.AddSingleton<AuthorizationHandler<UrlRolesRequirement>, PermissionHandler>();
}

5: My Handler: which is not being called

public class PermissionHandler : AuthorizationHandler<UrlRolesRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UrlRolesRequirement urlRolesRequirement)
    {
        var pendingRequirements = context.PendingRequirements.ToList();
        foreach (var requirement in pendingRequirements)
        {
        }
        return Task.CompletedTask;
    }
}

6: My Requirement class:

public class UrlRolesRequirement : IAuthorizationRequirement
{
    private List<UrlRole> UrlRoles { get; }

    public UrlRolesRequirement(List<UrlRole> urlRoles)
    {
        UrlRoles = urlRoles;
    }      
}

When I debug the ASP.NET Core AuthorizationHandler , I never see that my custom Requirement as being a requirement, which I configured in the Startup. I expected to see the requirement, and if the requirement is present then the "callback" will happen I assume. But for some reason my configuration fails to add the requirement.

public virtual async Task HandleAsync(AuthorizationHandlerContext context)
{
    if (context.Resource is TResource)
    {
        foreach (var req in context.Requirements.OfType<TRequirement>())
        {
            await HandleRequirementAsync(context, req, (TResource)context.Resource);
        }
    }
}

Without telling ASP.NET Core to do so, it will not use your configured policy to authorize anything. Authorization policies are so that you can predefine complex authorization conditions so that you can reuse this behavior when you need it. It however does not apply by default, and it couldn't considering that you already configure two policies: Which of those should apply? All of them? Then why configure separate policies?

So instead, no policy is being used to authorize a user unless you explicitly tell the framework that. One common method is to use the [Authorize] attribute with the policy name. You can put that on controller actions but also on controllers themselves to make all its actions authorize with this policy:

[Authorize("Roles")] // ← this is the policy name
public class ExampleController : Controller
{
    // …
}

If you have a policy that you want to use most of the times to authorize users, then you can configure this policy as the default:

services.AddAuthorization(options =>
{
    options.DefaultPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
}

This for example will define a policy that requires an authenticated users as the default. So whenever you use the [Authorize] attribute without specificing an explicit policy, then it will use that default policy.

This all will still require you to mark your routes somehow that you require authorization. Besides using the [Authorize] attribute, you can also do this in a more central location: The app.UseEndpoints() call in your Startup class.

endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}")
    .RequireAuthorization("Roles");

This is the default route template for controllers but with a call to RequireAuthorization which will basically require the Roles authorization policy on all routes that match this route template.

You can use also use this place to configure different default authorization policies for your different routes: By splitting up your route template, you can have multiple calls to MapControllerRoute with different route templates that all specify their own authorization policy.

I was thinking that instead of decorating each and every controller or action, I rather wanted to have some pre-configuration map in a DB, and then in the pipeline verify the users role or roles which are allocated when the user authenticates. When the user then tries to access a url, the users role gets verified and access is granted or rejected.

You could move the logic how exactly a user is authorized into the authorization handler that verifies your requirement. You would still enable the policy that has this requirement for all the routes you want to test though.

However, I would generally advise against this: Authorization requirements are meant to be simple, and you usually, you want to be able to verify them without hitting a database or something other external resource. You want to use the user's claims directly to make a quick decision whether or not the user is authorized to access something. After all, these checks run on every request, so you want to make this fast. One major benefit of claims based authorization is that you do not need to hit the database on every request, so you should keep that benefit by making sure everything you need to authorize a user is available in their claims.

Here is a tested solution, which enables runtime configuration changes . Also relieving the burden of decorating each class or action.

In the Startup Add the Role Authorization Requirement , and also register the RoleService which will be responsible for ensuring a particular role is authorised to access a particular URL.

Here is the Startup.cs where we configure the requirement and also the role service:

services.AddAuthorization(options =>
{
    options.DefaultPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddRequirements(new UrlRolesRequirement())
        .Build();
});

services.AddSingleton<IUserService, UserService>(); // authenticate
services.AddSingleton<IUserRoleService, UserRoleService>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IAuthorizationHandler, PermissionHandler>(); // authorise

The role IUserRoleService: - the UserRoleService implementation validates a users claimed role (a JWT claim) against a configuration map consisting of url/role entries that are allowed, by looking in either in cached map or retrieving the data from the database.

A typical url(path) to role map has the following format, retrieve from a database, then cached ( if lookups fail, the data is retrieved from the database ):

 /path/to/resource ROLE
public interface IUserRoleService
{
    public bool UserHasAccess(ClaimsPrincipal user, string path);
}

The Permission handler:

public class PermissionHandler : IAuthorizationHandler
{
    private readonly IUserRoleService userRoleService;
    private readonly IHttpContextAccessor contextAccessor;

    public PermissionHandler(IUserRoleService userRoleService, IHttpContextAccessor contextAccessor)
    {
        this.userRoleService = userRoleService;
        this.contextAccessor = contextAccessor;
    }

    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();
        foreach (var requirement in pendingRequirements)
        {
            if (!(requirement is UrlRolesRequirement)) continue;

            var httpContext = contextAccessor.HttpContext;
            var path = httpContext.Request.Path;
            if (userRoleService.UserHasAccess(context.User, path))
            {
                context.Succeed(requirement);
                break;
            }
        }
        return Task.CompletedTask;
    }
}

The RolesRequirement - just a POCO

public class UrlRolesRequirement : IAuthorizationRequirement
{           
}

Here is a partial implementation of the UserRoleService which validates the JWT role claimed.

private bool ValidateUser(ClaimsPrincipal user, string path)
{
    foreach (var userClaim in user.Claims)
    {
        if (!userClaim.Type.Contains("claims/role")) continue;

        var role = userClaim.Value;
        var key = role + SEPARATOR + path;

        if (urlRoles.ContainsKey(key))
        {
            var entry = urlRoles[key];
            if (entry.Url.Equals(path) && entry.Role.Equals(role))
            {
                return true;
            }
        }

    }
    Console.WriteLine("Access denied: " + path);
    return false;
}

I had an immediate 403 response, my custom authorization handler code was never reached. Turns out I forgot to inject them (Scoped is fine). That solved the issue for me.

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