C# Custom API Key Authentication Handler 🔒

As part of a recent project we realized some form of API key auth was going to be required, and we ended up rolling a custom, fairly simple version by implementing our own version of the Microsoft.AspNetCore.Authentication.AuthenticationHandler

Ahh, authentication, the word that all developers dread. While in C# over the last few years, it has become a bit easier to work with, it's still sometimes not all that straightforward.
As part of a recent project we realized some form of API key auth was going to be required, and we ended up rolling a custom, fairly simple version by implementing our own version of the  Microsoft.AspNetCore.Authentication.AuthenticationHandler.

So, how does it work?

When we call a method on a controller, we can use the Authorize attribute to tell the application a user must be authenticated to use said method or controller. If the user is not authenticated then a HTTP 403 is normally sent back. There are various way to wire up what this Authorize attribute actually does. In our case, we are going to create a new handler for the authentication that is going to pull a HTTP header from the request, check it's valid then return the status from our authentication handler.

Our new handler

So first, we need to scaffold our handler up. Create a new class that inherits the AuthenticationHandler class, and brings in all the boilerplate code

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        
        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock)
            : base(options, logger, encoder, clock)
        {           
        }

        protected override async Task<AuthenticateResult> HandleAuthenticateAsync(){}

Pretty standard stuff, we're just overriding the HandleAuthenticateAsync method with our own code. Next, we need to have a list of valid API keys injected into our method. In this example, we are just going to drop them into our appsettings.config file as a dictionary and bring them in using IOptions (don't forget to register them in DI). We also need to set up a couple of string constants for our auth scheme name and api http header name. So now our boilerplate code will look like this:

public class ApiKeyAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
 	public static readonly string SchemeName = "APIKeyAuthentication";
    public static readonly string ApiKeyHeaderName = "X-API-Key";
    private readonly IDictionary<string, string> _apiKeys;
        
	public ApiKeyAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock,
            IOptions<IDictionary<string, string>> apiKeys)
            : base(options, logger, encoder, clock)
        {
            _apiKeys = apiKeys.Value;
        }
 protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
        }
}

Cool, so once we have that we can start to actually go and see if we have the header, and if we do then pull the payload out and go and see if it exists in our allowed list of clients. We're going to wrap that up in a try/catch block just in case anything goes wrong and returns a fail (unauthorized result). So let's build that logic up

 try
    {
        if (!Request.Headers.ContainsKey(ApiKeyHeaderName))
            {
                Logger.LogWarning($"Missing {ApiKeyHeaderName} Header", Request.Headers);
                return Task.FromResult(AuthenticateResult.Fail($"Missing {ApiKeyHeaderName} Header"));
            }

        var apiKeyHeader = AuthenticationHeaderValue.Parse(Request.Headers[ApiKeyHeaderName]);
        if(!_apiKeys.ContainsKey(apiKeyHeader))
        {
            Logger.LogWarning("Invalid API Key passed", apiKeyHeader);
            return Task.FromResult(AuthenticateResult.Fail("API Key not found"));
        }

        
    }
    catch (Exception e)
    {
        Logger.LogError("Basic authentication failed", Request.Headers[ApiKeyHeaderName], e);
        return Task.FromResult(AuthenticateResult.Fail("API authentication failed. Unable to authenticate"));
    }

So most of that should be pretty self-explanatory, we are checking to make sure we have the expected http header, and if we don't then return a fail result.

If we do then we try and find the key in our dictionary of known API keys, if it doesn't exist then at this point we again send a failed result back. If it does then we need to move on to the next part, creating our AuthenticationTicket.

First, we can create some claims for the user, in this example we are just going to add one that creates the customer's name, this will come from the information we hold in our API keys settings in config. Update this with any claims you might need.

 private static IEnumerable<Claim> CreateClaimsForCustomer(APICustomer customer)
        {
            return new List<Claim>
            {
                new Claim(ClaimTypes.NameIdentifier, customer.Name)
            };
        }

Now we can create claims, we just need to create our ticket, add the claims, and return the ticket along with a Success result

var identity = new ClaimsIdentity(CreateClaimsForCustomer(customer), Scheme.Name);
                var principal = new ClaimsPrincipal(identity);
                var ticket = new AuthenticationTicket(principal, Scheme.Name);
                return Task.FromResult(AuthenticateResult.Success(ticket));

That should be it for our handler

Wire It into the Pipeline

Now it's just a case of plugging it into our Startup file. Under ConfigureServices add the following section somewhere

 services.AddAuthentication(ApiKeyAuthenticationHandler.SchemeName)
                .AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationHandler.SchemeName, null);

This tells the application to add our handler as an authentication method. Then again in Startup under Configure we need to add the line

 app.UseAuthentication();

And we should be ready to go! Now in your controllers, above the class just add the [Authorize] attribute and your controller will require authentication using our handler. Simple but a good start to understanding and controlling access to simple applications.