'How to return HttpStatusCode 404 for incorrect url with FallbackPolicy set in AuthorizationOptions?

In the web application I'm currently working on there is a requirement for all users to be authenticated. This is currently handled by using an AuthorizeFilter.

I now need to be able to apply different authorization policies to different parts of the application, and therefore I wanted to switch from using a global authorization filter to setting the fallback policy (as described in, and recommended by, the official documentation).

This works as intended, with the exception that requests for resources which don't exist now returns HttpStatusCode 401 if not authenticated, or 403 if authenticated but some other requirement is not fulfilled (and we have a few in the default/fallback policy). Previously, with the authorization filter solution, a 404 would be returned. I would guess the reason is that the fallback policy is evaluated earlier in the pipeline, than the authorization filter is, but it is still a side-effect I would like to avoid.

(How) Can I get the application to return 404s like before while utilizing the FallbackPolicy? I guess I could use a custom IAuthorizationMiddlewareResultHandler if the application was using net5.0 (or later), but an upgrade isn't in the short term plan which means the solution must work for netcoreapp3.1.



Solution 1:[1]

I've managed to get the result I was after while utilizing netcoreapp3.1 by adding a custom authorization handler which extends DenyAnonymousAuthorizationRequirement:

public class CustomDenyAnonymousAuthorizationRequirement : DenyAnonymousAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DenyAnonymousAuthorizationRequirement requirement)
    {
        if (context.Resource != null)
        {
            return base.HandleRequirementAsync(context, requirement);
        }
        
        context.Succeed(requirement);
        return Task.CompletedTask;
    }
}

This code just marks the requirement as fulfilled if there is no resource.

Add it the service collection like so:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddSingleton<IAuthorizationHandler, CustomDenyAnonymousAuthorizationRequirement>();
}

If using net5.0+ a custom IAuthorizationMiddlewareResultHandler can be used to accomplish the same:

public class CustomAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
    private readonly AuthorizationMiddlewareResultHandler _defaultHandler = new();

    public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
    {
        if ((authorizeResult.Challenged || authorizeResult.Forbidden) && context.GetEndpoint() == null)
        {
            context.Response.StatusCode = (int)HttpStatusCode.NotFound;
            return;
        }

        await _defaultHandler.HandleAsync(next, context, policy, authorizeResult);
    }
}

And added to the service collection:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddSingleton<IAuthorizationMiddlewareResultHandler, CustomAuthorizationMiddlewareResultHandler>();
}

Solution 2:[2]

To returns 404 with fallback policy:

app.UseEndpoints(endpoints =>
{
   endpoints.MapControllers();
   endpoints.MapRazorPages();
   endpoints.MapFallbackToController("api/{**slug}", nameof(ErrorController.Error404), "Error");
   endpoints.MapFallbackToPage("{**slug}", "/Public/Errors/404");
});

And obviously the action decorated with AllowAnonymous

    [Route("api/[controller]")]
    [ApiController]
    public class ErrorController : ControllerBase
    {
        [HttpGet]
        [AllowAnonymous]
        public IActionResult Error404()
        {
            return NotFound();
        }
    }

For razor pages:

builder.AddRazorPagesOptions(options =>
{
  options.Conventions.AllowAnonymousToFolder("/Public");

Policies

options.FallbackPolicy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .RequireRole(LoginEntities.Sede.ToString())
                    .Build();

options.DefaultPolicy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();

enter image description here

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 jfiskvik
Solution 2