'How to write an asynchronous Policy handler, injecting a scoped service

I'm trying to write a custom policy for an ASP.NET Core 3.1 web application, using a custom Identity storage provider.

I've tried to wrap my head around the fact that policies in ASP.NET Core are designed to take user informations from an HttpContext object, when I read this in a MSDN Article:

once you hold a reference to the user, you can always find the username from the claims and run a query against any database or external service

I started writing my own policy (as of now a simple role requirement) injecting the UserManager into the constructor:

public class RoleHandler : AuthorizationHandler<RoleRequirement>
{
    private UserManager<AppUser> UserManager;

    public RoleHandler(UserManager<AppUser> usermanager)
    {
        UserManager = usermanager;
    }
}

Now I have a couple problems:

INJECTING A SCOPED SERVICE IN A SINGLETON

Policies are supposed to be lasting for the entire application life, so that would be a Singleton:

services.AddSingleton<IAuthorizationHandler, RoleHandler>();

but the UserManager injected in the policy server is a scoped service and that is not allowed. Solution was very easy, changing the configuration of the policy service from a singleton to a scoped service

services.AddScoped<IAuthorizationHandler, RoleHandler>();

but I don't know whether that cause any issue or not.

WRITING AN ASYNCHRONOUS POLICY HANDLER

This is my implementation of the HandleRequirementAsync method:

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RoleRequirement requirement)
{
    AppUser user = UserManager.FindByIdAsync(context.User.Identity.Name).Result;

    if (user != null)
    {
        bool result = UserManager.IsInRoleAsync(user, requirement.Role.ToString()).Result;
        if (result) context.Succeed(requirement);
    }

    return Task.CompletedTask;
}

I used Task.Result but it blocks the thread. I can't use await because that would make the method returning a Task<Task> instead of a Task and I can't change it. How can I solve this?



Solution 1:[1]

Don't return Task.CompletedTask.

When you declare a method as async, it implicitly returns a Task when the first await is hit:

protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, RoleRequirement requirement)
{
    AppUser user = await UserManager.FindByIdAsync(context.User.Identity.Name);

    if (user != null)
    {
        bool result = await UserManager.IsInRoleAsync(user, requirement.Role.ToString());
        if (result) context.Succeed(requirement);
    }
}

Task.CompletedTask is generally used when you need to implement a Task returning method synchronously, which you are not.

Solution 2:[2]

My HandleRequirementAsync also calls httpClient.GetAsync (Blazor server, .NET 5), adding async to the HandleRequirementAsync and execute the await hpptClient.GetAsync() breaks the authorization. With async method with delays, Try typing the route address in the browser and it will redirect to not authorized page, even though the context.Succeed(requirement) is executed.

The working solution for me is to keep the HandleRequirementAsync as it is, returning Task.CompletedTask. For the async method we need to call, just use pattern for calling async method from non async method.

The one I use is from https://stackoverflow.com/a/43148321/423356

my sample async method:

public async Task<IList<Permission>> GetGroupPermissions(int userId)
{
    HttpResponseMessage response = await _httpClient.GetAsync(string.Format("Auth/GroupPermissions/{0}", userId));

    try
    {
        var payload = await response.Content.ReadFromJsonAsync<List<Permission>>();
        response.EnsureSuccessStatusCode();

        return payload;
    }
    catch
    {
        return new List<Permission>();
    }

}

HandleRequirementAsync:

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
    var t2 = (Task.Run(() => GetGroupPermissions(userId)));
    t2.Wait();
    var userGroupPermissions = t2.Result;
    

    if (!userGroupPermissions.Contains(requirement.Permission))
    {
        //context.Fail(); //no need to fail, other requirement might success
        return Task.CompletedTask;
    }

    context.Succeed(requirement);

    return Task.CompletedTask;
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Johnathan Barclay
Solution 2