C# – ASP.NET Core 2.0 authentication middleware

asp.net-coreasp.net-core-2.0authenticationc++

With Core 1.1 followed @blowdart's advice and implemented a custom middleware:

https://stackoverflow.com/a/31465227/29821

It worked like this:

  1. Middleware ran. Picked up a token from the request headers.
  2. Verified the token and if valid built an identity (ClaimsIdentity) that contained multiple claims which then it added via HttpContext.User.AddIdentity();
  3. In ConfigureServices using services.AddAuthorization I've added a policy to require the claim that is provided by the middleware.
  4. In the controllers/actions I would then use [Authorize(Roles = "some role that the middleware added")]

This somewhat works with 2.0, except that if the token is not valid (step 2 above) and the claim is never added I get "No authenticationScheme was specified, and there was no DefaultChallengeScheme found."

So now I'm reading that auth changed in 2.0:

https://docs.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x

What's the right path for me to do the same thing in ASP.NET Core 2.0? I don't see an example to do truly custom authentication.

Best Solution

So, after a long day of trying to solve this problem, I've finally figured out how Microsoft wants us to make custom authentication handlers for their new single-middleware setup in core 2.0.

After looking through some of the documentation on MSDN, I found a class called AuthenticationHandler<TOption> that implements the IAuthenticationHandler interface.

From there, I found an entire codebase with the existing authentication schemes located at https://github.com/aspnet/Security

Inside of one of these, it shows how Microsoft implements the JwtBearer authentication scheme. (https://github.com/aspnet/Security/tree/master/src/Microsoft.AspNetCore.Authentication.JwtBearer)

I copied most of that code over into a new folder, and cleared out all the things having to do with JwtBearer.

In the JwtBearerHandler class (which extends AuthenticationHandler<>), there's an override for Task<AuthenticateResult> HandleAuthenticateAsync()

I added in our old middleware for setting up claims through a custom token server, and was still encountering some issues with permissions, just spitting out a 200 OK instead of a 401 Unauthorized when a token was invalid and no claims were set up.

I realized that I had overridden Task HandleChallengeAsync(AuthenticationProperties properties) which for whatever reason is used to set permissions via [Authorize(Roles="")] in a controller.

After removing this override, the code had worked, and had successfully thrown a 401 when the permissions didn't match up.

The main takeaway from this is that now you can't use a custom middleware, you have to implement it via AuthenticationHandler<> and you have to set the DefaultAuthenticateScheme and DefaultChallengeScheme when using services.AddAuthentication(...).

Here's an example of what this should all look like:

In Startup.cs / ConfigureServices() add:

services.AddAuthentication(options =>
{
    // the scheme name has to match the value we're going to use in AuthenticationBuilder.AddScheme(...)
    options.DefaultAuthenticateScheme = "Custom Scheme";
    options.DefaultChallengeScheme = "Custom Scheme";
})
.AddCustomAuth(o => { });

In Startup.cs / Configure() add:

app.UseAuthentication();

Create a new file CustomAuthExtensions.cs

public static class CustomAuthExtensions
{
    public static AuthenticationBuilder AddCustomAuth(this AuthenticationBuilder builder, Action<CustomAuthOptions> configureOptions)
    {
        return builder.AddScheme<CustomAuthOptions, CustomAuthHandler>("Custom Scheme", "Custom Auth", configureOptions);
    }
}

Create a new file CustomAuthOptions.cs

public class CustomAuthOptions: AuthenticationSchemeOptions
{
    public CustomAuthOptions()
    {

    }
}

Create a new file CustomAuthHandler.cs

internal class CustomAuthHandler : AuthenticationHandler<CustomAuthOptions>
{
    public CustomAuthHandler(IOptionsMonitor<CustomAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
    {
        // store custom services here...
    }
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        // build the claims and put them in "Context"; you need to import the Microsoft.AspNetCore.Authentication package
        return AuthenticateResult.NoResult();
    }
}