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

Viewed 126

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.

3 Answers

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

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>();
}

To solve this problem where by applying FallBackPolicy we are no longer receiving 404,, we can make use of Middlewares.

 public class TestMiddleware
    {
        private readonly RequestDelegate next;

        public TestMiddleware(RequestDelegate requestDelegate)
        {
            next = requestDelegate;
        }

        public async Task Invoke(HttpContext httpContext)
        {
            await next(httpContext);
            var ep = httpContext.GetEndpoint();
            if(ep == null)
            {
                httpContext.Response.StatusCode = 404;
                await httpContext.Response.WriteAsync("Not Found!!!");
            }
        }
    }

Above is just a sample implementation of such middleware. We can add this middleware after UseEndpoints middleware in the pipeline.

 app.UseEndpoints(endpoints =>
                {
                 endpoints.MapControllers();
                });
app.UseMiddleware<TestMiddleware>();

In the middleware code we are using GetEndPoints method on HttContext to understand whether the call actually mapped to any ActionMethod if not we can return 404. So we can have fallback policy and yet we can return 404 for Not found resources

Related