Search code examples
c#asp.net-web-apipolicy

Policy based authorization needs to return some reason when it fails


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?


Solution

  • 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: