ASP.NET Core makes it extremely easy to configure authentication right out of the box with a choice from a plethora of different built-in authentication handlers. Everything from Single Sign On with Facebook to JWT to simple cookie authentication is available right out of the box. Where I found the ASP.NET Core documentation lacking was when attempting to use multiple authentication handlers at the same time. I was already using the JWT handler in my application, but I wanted to have my custom API key authentication handler run and handle authentication if no Authorization header was supplied as part of the request. Finding the answer to how to accomplish that took longer than it should have, so hopefully this blog post will save others from that same fate.

Getting Started

We’ll start off by configuring our ASP.NET Core application to use JWT authentication, then we’ll move on to building our own customer authentication handler that we’ll want to have handle authentication for a request if a JWT isn’t provided.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors();
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

        var appSettings = Configuration.GetSection("AppSettings");
        var jwtKeyBytes = Encoding.UTF8.GetBytes(appSettings.Get<string>("JwtSigningKey"));
        var jwtKey = new SymmetricSecurityKey(jwtKeyBytes);

        services.AddAuthentication(x =>
        {
            x.DefaultAuthenticationScheme = JwtBearerDefaults.AuthenticationScheme),
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(x =>
        {
            x.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = jwtKey,
                ValidateIssuer = false,
                ValidateAudience = false
            };
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseCors(x =>
        {
            x.AllowAnyOrigin();
            x.AllowAnyMethod();
            x.AllowAnyHeader();
        });

        app.UseAuthentication();
        app.UseMvc();
    }
}

Creating a Custom Authentication Handler

We’ll start off by creating our own simple handler that we’ll use for example purposes:

public class SharedSecretAuthenticationHandler : AuthenticationHandler<SharedSecretAuthenticationOptions>
{
    private readonly IUserClaimsPrincipalFactory<User> _claimsPrincipalFactory;

    public SharedSecretAuthenticationHandler(
        IOptionsMonitor<SharedSecretAuthenticationOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock,
        IUserClaimsPrincipalFactory<User> claimsPrincipalFactory) : base(options, logger, encoder, clock) 
    { 
        _claimsPrincipalFactory = claimsPrincipalFactory;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var headerExists = Context.Request.Headers.TryGetValue(Options.SharedSecretHeaderName, out var headers);
        if (!headerExists)
        {
            // Returning NoResult will allow other handlers to potentially authenticate the request
            return AuthenticateResult.NoResult();
        }

        var headerValue = headers.FirstOrDefault();
        if (HashUtils.Sha256(headerValue) != Options.SharedSecret)
        {
            return AuthenticateResult.Failed("Shared secret was invalid");
        }

        var user = new User {UserName = "SharedSecret"};
        var principal = await _claimsPrincipalFactory.CreateAsync(user);
        var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), Options.AuthenticationScheme);

        return AuthenticateResult.Success(ticket);
    }

    public static class SharedSecretAuthenticationDefaults
    {
        public const string AuthenticationScheme = "SharedSecret";
        public const string SharedSecretHeaderName = "X-Shared-Secret";
    }

    public static class HashUtils
    {
        public static string Sha256(string input)
        {
            using (var hashAlgo = SHA256.Create())
            {
                var bytes = hashAlgo.ComputeHash(Encoding.UTF8.GetBytes(input));
                return Convert.ToBase64String(bytes);
            }
        }
    }

    public class SharedSecretOptions : AuthenticationSchemeOptions
    {
        public string AuthenticationScheme {get; set;} = SharedSecretAuthenticationDefaults.AuthenticationScheme;
        public string SharedSecretHeaderName {get; set;} = SharedSecretAuthenticationDefaults.SharedSecretHeaderName;
        public string SharedSecret 
        {
            get { return value; }
            set { value = HashUtils.Sha256(value); }
        }
    }

    public static class SharedSecretAuthenticationExtensions
    {
        public static AuthenticationBuilder AddSharedSecretAuth(this AuthenticationBuilder builder,
            Action<SharedSecretOptions> configureOptions)
        {
            builder.AddScheme<SharedSecretOptions, SharedSecretAuthenticationHandler>(
                SharedSecretAuthenticationDefaults.AuthenticationScheme, "Shared Secret Auth", configureOptions);

            return builder;
        }
    }
}

Wiring Everything Up

We’ll now need to configure ASP.NET Core to use our authentication handler. We’ll update the AddAuthentication call in the ConfigureServices method of Startup as follows:

var sharedSecret = appSettings.Get<string>("SharedSecret");

services.AddAuthentication(x =>
{
    x.DefaultAuthenticationScheme = JwtBearerDefaults.AuthenticationScheme),
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
    x.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = jwtKey,
        ValidateIssuer = false,
        ValidateAudience = false
    };
})
.AddSharedSecretAuth(x => x.SharedSecret = sharedSecret);

If we run our application now and attempt to authenticate using only our shared secret handler, we’ll find that the JWT Bearer handler runs but our own handler doesn’t get run afterwards. What we’ll need to do is create our own AuthorizationPolicy and pass it to a global AuthorizeFilter instance that runs on each request:

var defaultSchemes = new[] { JwtBearerDefaults.AuthenticationScheme, SharedSecretAuthenticationDefaults.AuthenticationScheme};
var defaultPolicy = new AuthorizationPolicyBuilder(defaultSchemes)
    .RequireAuthenticatedUser()
    .Build();

var mvc = services.AddMvc(x => x.Filters.Add(new AuthorizeFilter(defaultPolicy)))
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Putting it all together, our ConfigureServices method now looks like this:

public void ConfigureServices(IServiceCollection services)
{
    var defaultSchemes = new[] { JwtBearerDefaults.AuthenticationScheme, SharedSecretAuthenticationDefaults.AuthenticationScheme};
    var defaultPolicy = new AuthorizationPolicyBuilder(defaultSchemes)
        .RequireAuthenticatedUser()
        .Build();

    var mvc = services.AddMvc(x => x.Filters.Add(new AuthorizeFilter(defaultPolicy)))
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    var appSettings = Configuration.GetSection("AppSettings");
    var jwtKeyBytes = Encoding.UTF8.GetBytes(appSettings.Get<string>("JwtSigningKey"));
    var jwtKey = new SymmetricSecurityKey(jwtKeyBytes);
    var sharedSecret = appSettings.Get<string>("SharedSecret");

    services.AddAuthentication(x =>
    {
        x.DefaultAuthenticationScheme = JwtBearerDefaults.AuthenticationScheme),
        x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(x =>
    {
        x.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = jwtKey,
            ValidateIssuer = false,
            ValidateAudience = false
        };
    })
    .AddSharedSecretAuth(x => x.SharedSecret = sharedSecret);
}

Conclusion

I hope this helps out anyone that finds themselves in a similar situation to this and saves you some time tracking down this information.