LFN
LFN

Reputation: 97

Why does AuthenticationHandler.HandleForbiddenAsync Totally Ignore Code That Runs?

Thanks for your time folks. This implementation intentionally doesn't use built in cookies, JWT token or any other Auth mechanism. Please do not suggest using any of the built in Auth stuff to bypass the issue. This is about an entirely bespoke Authentication/Authorization implementation to diagnose and understand what bonkersness is going on and why. The key word here is understanding.

public class CustomAuthenticationHandler : AuthenticationHandler<CustomAuthenticationOptions> { 

    protected override Task<AuthenticateResult> HandleAuthenticateAsync() {

        // Do stuff and return appropriately after choosing to set the
        // authenticated principle or not.
    }

    protected override Task<bool> HandleUnauthorizedAsync(ChallengeContext context) {

        // This method name suggests that you can choose what happens in the event
        // that Authentication fails. Common sense suggests we should expect a 401
        // which we do see - but something in the internals ignores anything we
        // do here - instead returning a blank page with a 401 result.

        // Note that the event ALWAYS fires and code ALWAYS runs without errors when
        // Authentication fails but the code is entirely ignored (so our redirect 
        // doesnt happen).

        // Adding a specific "ActiveAuthenticationSchemes" proerty to the Controller Action gets the redirect to be honoured.

        // Why offer this override (or even invoke the event) if it will be totally ignored by default???
        // Why REQUIRE that every Authorization attribute must specify an ActiveAuthenticationSchemes
        // property when there is only 1 scheme running? Its madness for large complex applications.

        // This will be ignored by default
        Context.Response.StatusCode = (int)HttpStatusCode.Redirect;
        Context.Response.Redirect("/login?returnUrl=" + Options.UrlEncoder.Encode(httpRequest.Path + httpRequest.QueryString));

         // Return true to stop internals overriding our code? Nope - still ignored.
        return Task.FromResult(true);
    }

    protected override Task<bool> HandleForbiddenAsync(ChallengeContext context) {

        // This method name suggests that you can choose what happens in the event
        // that Authentication succeeds and Authorisation fails. We expect to see
        // a 403 Forbidden here. But again what we see is a blank page and a 401 Unathorized.
        // Something in the internals is ignoring this code and CHANGING what would
        // have been a sensible 403 result into a 401.

        // Like the HandleUnauthorizedAsync method, it is possible to get this
        // method code to be honoured if we explicitly set an ActiveAuthenticationSchemes
        // property on every Authorize attribute.

        // Isnt this again a bonkers requirement for complex applications?

        // This will be ignored in favour of a blank page and a 401
        // We can set either the StatusCode or do the redirect, both will be duly ignored
        Context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
        Context.Response.Redirect("/_403");

        // Return true to stop internals overriding our code? Nope - still ignored.
        return Task.FromResult(true);
    }
}

For completeness, the startup looks like this:

public class Startup {

    public void ConfigureServices(IServiceCollection services) {

        services.AddMvc();
        services.AddDataProtection();
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {

        app.UseCustomAuthenticationMiddleware(

            new CustomAuthenticationOptions() {
                //AuthenticationScheme = "CustomAuthenticationScheme",
                AutomaticAuthenticate = true,
                AutomaticChallenge = true
            }
        );

        // -----

        app.UseMvc(routes => {
            routes.MapRoute(name: "default", template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

And our Middleware has a simple implimentation that invokes the handler with:

protected override AuthenticationHandler<CustomAuthenticationOptions> CreateHandler() {
    return new CustomAuthenticationHandler();
}

So, Is there some kind of built-in internal fail safe to stop people wiring up custom auth without explicitly setting ActiveAuthenticationSchemes on every Authorize attribute on every Action (to stop us doing it)?

I'm confused that the code runs without errors but then gets ignored.

Suggestions of how to diagnose this myself are as welcome as explanations - it's driving me nuts :) !!!

* Edit / Update

Possible clue. It seems that the [Authorize] attribute also has some infuriatingly odd behaviour. When applying "ActiveAuthenticationSchemes" at the Action level things start working as you might expect for both Unauthorised and Forbidden results. Move that attribute to the controller level though and it only has effect for Unauthorised. Forbidden handler code gets ignored again (resulting in a blank page and 401 instead of the coded redirect). Something in the internals here is stomping all over sensible or even coded results. Isn't it? :) ... looking at the MVC source code it looks like MVC might be forcing a 401 instead of a challenge result. I hope that's not true...

* Edit 2

It seems it is, at least in part, true. I created a new project that had only MVC running (no additional authentication or authorization) and for sure MVC will return a 401 Unauthorized regardless of whether or not you are authenticated. It feels like this should be a 403 Forbidden if Authorised but with insuffcient permissions. And there's clearly something not wired up between Authentication and MVC that results in code being ignored and behaviour overriden by the MVC 401 result. Maybe... still investigating...

Upvotes: 0

Views: 4418

Answers (1)

TeChaiMail
TeChaiMail

Reputation: 71

To solve it use the HandleChallengeAsync method instead:

protected override Task HandleChallengeAsync(AuthenticationProperties properties)       {           
    string redirectURL;


    if (ReadConfig.LoginURL_Exists && Request.AcceptHTML ())            
    {
        redirectURL = TextHelper.AppendVarValueToHTTPURL (ReadConfig.LoginURL, GeneralConstants.RETURN_URL, Request.GetDisplayUrl());

        Response.Redirect (redirectURL);

        return Task.CompletedTask;          
    }

    Response.StatusCode = (int) HttpStatusCode.Unauthorized;

    return Task.CompletedTask;      
}

Upvotes: 2

Related Questions