'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 |