First some code:
builder
.Services
.AddAuthorization(options =>
{
options.AddPolicy("IsLoggedIn", policy =>
{
policy.RequireClaim(StoreApi.Properties.Resources.UserName);
policy.RequireClaim(StoreApi.Properties.Resources.UserID);
});
options.AddPolicy("IsCustomer", policy =>
{
policy.RequireClaim(StoreApi.Properties.Resources.UserType, nameof(Customer));
});
options.AddPolicy("IsAdministrator", policy =>
{
policy.RequireClaim(StoreApi.Properties.Resources.UserType, nameof(Administrator));
});
options.AddPolicy("IsApplication", policy =>
{
policy.RequireClaim(StoreApi.Properties.Resources.UserType, nameof(Application));
});
options.AddPolicy("IsSystem", policy =>
{
policy.RequireClaim(StoreApi.Properties.Resources.UserType, nameof(Application), nameof(Administrator));
});
options.AddPolicy("HasCart", policy =>
{
policy.RequireClaim(StoreApi.Properties.Resources.UserCart);
});
});
I have a bunch of claim-base policies and the claims are defined in a resource while some need to have a specific value. These claims come from a JWT token and the whole thing is a simplified web store. And these policies work well, except for one issue...
When the user fails to meet a policy, they get a 403 "Forbidden" message, but no explanation about what's wrong. And because I have multiple policies, I would like to know which policy failed.
So, how do I make sure that the 403 provides this extra data to the client?
I assume you should implement your own IAuthorizationMiddlewareResultHandler
based on this article.
Example:
Policy:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Admin",
policy =>
{
policy.Requirements.Add(new RolesAuthorizationRequirement(new[] { "Admin" }));
policy.Requirements.Add(new ClaimsAuthorizationRequirement("MyClaim", new[] { "Test" }));
});
});
[HttpGet]
[Authorize(Policy = "Admin")]
public async Task<IActionResult> Get() => Ok(true);
Custom IAuthorizationMiddlewareResultHandler
public class MyAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
private readonly AuthorizationMiddlewareResultHandler defaultHandler = new();
public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy,
PolicyAuthorizationResult authorizeResult)
{
// Fall back to the default implementation.
await defaultHandler.HandleAsync(next, context, policy, authorizeResult);
//Have to write to body after default implementation because is sets Http Code
if (!authorizeResult.Succeeded)
{
string? text = authorizeResult.AuthorizationFailure?.FailedRequirements.Aggregate(new StringBuilder(),
(builder, reason) => builder.Append(reason + Environment.NewLine), builder => builder.ToString());
if (!string.IsNullOrWhiteSpace(text))
await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(text));
}
}
}
Its registartion
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler,
MyAuthorizationMiddlewareResultHandler>();
After all these steps the result after calling endpoint would be response with status code 403:
RolesAuthorizationRequirement:User.IsInRole must be true for one of the following roles: (Admin)
ClaimsAuthorizationRequirement:Claim.Type=MyClaim and Claim.Value is one of the following values: (Test)
If you want to customize the response text you should work with FailedRequirements
and FailureReasons
properties of authorizeResult.AuthorizationFailure
.
Sources: