ASP.NET Core custom AuthenticationHandler in combination with Cookies

Viewed 520

In .NET Core 1 we could use

app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
    AutomaticAuthenticate = true,
    ...

to ensure the cookie was evaluated first. On top of this we had either OAuth2 or a custom authentication module using the Basic authentication header. This made the following flow trivial:

  1. User reach website, gets a WWW-Authorize challenge (Bearer or Basic depending on settings)
  2. User responds to the challenge (so for Basic, provide username/password)
  3. As soon as the relevant middleware would validate the Bearer or Basic token on the next request, it would sign in the user - and as a result the cookie middleware would add its cookie to the respond.
  4. On the next request the incoming cookie would be validated. If it is still valid, the user is signed in and as a result the following OAuth2 or custom middleware simply backs off and do not perform any action.

We are now trying to update that to ASP.NET Core 6 (or whatever it is called these days).

In the event handler of our custom AuthenticationHandler the following code:

context.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, context.Principal)

does result in the cookies being set.

With

services.AddAuthorization(options =>
{
    options.DefaultPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme, "MyCustomScheme")
        .Build();
});

it will first validate the cookie, but it will always run "MyCustomScheme" as well. In some environments validating a username/password can add 30 seconds due to timeouts in their way too complicated infrastructure setup, and I do not see a way for my custom handler to access any previous identities allowing it to shortcut (so no, "oh, already validated by cookie, so I do not need to do anything" unless I start seriously hacking).

I could also write a "ForwardDefaultSelector" (from https://docs.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-6.0)

options.ForwardDefaultSelector = context =>
{
    string authorization = context.Request.Headers[HeaderNames.Authorization];
    if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
    {
        var token = authorization.Substring("Bearer ".Length).Trim();
        var jwtHandler = new JwtSecurityTokenHandler();

        return (jwtHandler.CanReadToken(token) && jwtHandler.ReadJwtToken(token).Issuer.Equals("B2C-Authority"))
            ? "B2C" : "AAD";
    }
    return "AAD";
};

but checking the cookie exists is of course not enough - it might be present but expired in which case setting the only authentication schema to be the cookie would result in a failed login - even if the correct token is being passed in as well.

Am I missing some obvious way to get the cookie to authenticate if able, but if not able let another handler have a go? It seems like a simple task: Try to authenticate with Cookie, if fail, try X. 2 lines of code if I could just find a place to put those lines.

And yes, I am aware we should skip this and only support OAuth2. Unfortunately the world moves slow.

0 Answers
Related