• Camilo Terevinto

Simple and secure custom API Keys using ASP.NET Core

Updated: Jul 14

A common issue when exposing endpoints to the outside world, is that you have no idea what is calling your application, and there's a clear need to have some control. While there are protocols like OAuth 2.0 and OpenID Connect that provide a great deal of security and configuration, they tend to be hard to work with, as the setup and maintenance is not trivial. Besides, for simple solutions where there's only one or two clients, such setup would likely become overkill.

A nice solution, that is secure enough if correctly implemented, is to implement API keys, which is a fancy way of saying client password, as, really, it's nothing more than that.


There are 3 important security aspects to take into account when doing this type of implementations:

  1. Generating secure keys,

  2. Securely validating the keys, and,

  3. Having a fail-safe mechanism to immediately block a key if (when) the need arises.


Besides these, there are also some extra aspects to take into account:

  • Validating the keys should be unobtrusive,

  • Validating the keys should be fast,

  • The keys should be URL-safe, in case they need to be sent in HTTP GET requests from a browser,

  • And, optionally, they should look a bit custom! This makes it easier to identify issues besides adding a bit of branding to the solution.

\\ The code for this blog has been refined for configurability and extensibility and published to NuGet: https://www.nuget.org/packages/TerevintoSoftware.AspNetCore.Authentication.ApiKeys/

1. Generating secure keys

It's easy to find tutorials online about API keys that fail from the beginning: they generate keys that are insecure, or are based on algorithms that are not cryptographically secure, like GUIDs.

Fortunately, there's an easy solution, using the RNGCryptoServiceProvider class:

public string GenerateApiKey()
{
    using var provider = new RNGCryptoServiceProvider();
    var bytes = new byte[32];
    provider.GetBytes(bytes);

    return Convert.ToBase64String(bytes);
}

The above, however, has the problem of not being URL-safe, which can solved quite easily:

public string GenerateApiKey()
{
    using var provider = new RNGCryptoServiceProvider();
    var bytes = new byte[32];
    provider.GetBytes(bytes);

    return Convert.ToBase64String(bytes)
        .Replace("/", "")
        .Replace("+", "")
        .Replace("=", "");
}

Now, while the above works perfectly fine, we can take it 2 steps further to have a static length as well as some customization:

public string GenerateApiKey()
{
    using var provider = new RNGCryptoServiceProvider();
    var bytes = new byte[32];
    provider.GetBytes(bytes);

    return "CT-" + Convert.ToBase64String(bytes)
        .Replace("/", "")
        .Replace("+", "")
        .Replace("=", "")
        .Substring(0, 33);
}

The RNGCryptoServiceProvider class is deprecated in .NET 6, so the above needs to be changed to use the new RandomNumberGenerator:

public string GenerateApiKey()
{
    var bytes = RandomNumberGenerator.GetBytes(_bytesCountToGenerate);
    
    return "CT-" + Convert.ToBase64String(bytes)
        .Replace("/", "")
        .Replace("+", "")
        .Replace("=", "")
        .Substring(0, 33);
}

Let's look at what that piece of code outputs:

CT-ueeiWl4LBYpzELUyyumzvXteti8lNnycg

Nice! We have a customized API key that's always 36 characters long. Now, if a client had an issue with not understanding which key to use, you could tell them "the key that starts with CT- and is 36 characters long".


Let's put it in a service now, and refine it a bit further (.NET 6 code):

public interface IApiKeyService
{
    string GenerateApiKey();
}

internal class ApiKeyService : IApiKeyService
{
    private const string _prefix = "CT-";
    private const int _numberOfSecureBytesToGenerate = 32;
    private const int _lengthOfKey = 36;

    public string GenerateApiKey()
    {
        var bytes = RandomNumberGenerator.GetBytes(_numberOfSecureBytesToGenerate);

        return string.Concat(_prefix, Convert.ToBase64String(bytes)
            .Replace("/", "")
            .Replace("+", "")
            .Replace("=", "")
            .AsSpan(0, _lengthOfKey - _prefix.Length));
    }
}

2. Securely validating requests

Given that having an API key represents an identity, as in, a client calling your application, the process for validating such key is known as authentication. The correct way to do the validation then is in the authentication part of the ASP.NET Core pipeline.

One of the easiest way to do this and have a lot of the plumbing code taken care of is to implement the class AuthenticationHandler<TOptions>.


In order to achieve this, let's start by first creating a class extending AuthenticationSchemeOptions:

public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
    public const string DefaultScheme = "ClientKey";
    public const string HeaderName = "x-api-key";
}

While the DefaultScheme can be anything that makes sense for your project, as it's completely internal, the HeaderName x-api-key follows standards for custom headers, and is a name used in multiple API keys implementations.


We're now ready to create our custom AuthenticationHandler class. Let's start with a basic implementation:

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

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.TryGetValue(ApiKeyAuthenticationOptions.HeaderName, out var apiKey) || apiKey.Count != 1)
        {
            Logger.LogWarning("An API request was received without the x-api-key header");
            return AuthenticateResult.Fail("Invalid parameters");
        }

        var clientId = await GetClientIdFromApiKey(apiKey);

        if (clientId == null)
        {
            Logger.LogWarning($"An API request was received with an invalid API key: {apiKey}");
            return AuthenticateResult.Fail("Invalid parameters");
        }

        Logger.BeginScope("{ClientId}", clientId);
        Logger.LogInformation("Client authenticated");

        var claims = new[] { new Claim(ClaimTypes.Name, clientId.ToString()) };
        var identity = new ClaimsIdentity(claims, ApiKeyAuthenticationOptions.DefaultScheme);
        var identities = new List<ClaimsIdentity> { identity };
        var principal = new ClaimsPrincipal(identities);
        var ticket = new AuthenticationTicket(principal, ApiKeyAuthenticationOptions.DefaultScheme);

        return AuthenticateResult.Success(ticket);
    }
}

You can probably notice a couple of things in the code above:

  • For authentication to succeeded, there must be exactly one x-api-key header.

  • We're logging warnings when authentication fails, and information when it succeeds.

  • We're missing the implementation for GetClientIdFromApiKey.

  • There's a single claim being added to the ticket generated, which will then be available when we use User.Identity.Name in Controllers, for example.

So, how should the implementation for GetClientIdFromApiKey look like?


3. Efficiently validating the keys

We need something that can tell us whether a key is valid or not, and give us something meaningful, like the client's id in the example above, but it can be any information needed by the system that can be obtained quickly enough.

Since it needs to be fast as this is in the hot path, the code cannot do a database roundtrip just for getting this information, we need either a noSQL database or a caching system.


In order to keep this simple, let's use the memory cache that comes with .NET Core:

public interface ICacheService
{
    ValueTask<Guid> GetClientIdFromApiKey(string apiKey);
}

public class CacheService : ICacheService
{
    private readonly IMemoryCache _memoryCache;
    private readonly IClientsService _clientsService;

    public CacheService(IMemoryCache memoryCache, IClientsService clientsService)
    {
        _memoryCache = memoryCache;
        _clientsService = clientsService;
    }

    public async ValueTask<Guid> GetClientIdFromApiKey(string apiKey)
    {
        if (!_memoryCache.TryGetValue<Dictionary<string, Guid>>($"Authentication_ApiKeys", out var internalKeys))
        {
            internalKeys = await _clientsService.GetActiveClients();

            _memoryCache.Set($"Authentication_ApiKeys", internalKeys);
        }

        if (!internalKeys.TryGetValue(apiKey, out var clientId))
        {
            return Guid.Empty;
        }

        return clientId;
    }
}

So, in the above sample code, we:

  • Use a memory cache to store our key/clientId map.

  • Use a separate service to get the data when the data is not in the cache.

Of course, whether it makes more sense to get a single client or all clients at once will depend on the amount of clients and the data you want to store in the cache.


We can then update ApiKeyAuthenticationHandler to consume this service:

private readonly ICacheService _cacheService;

public ApiKeyAuthenticationHandler (IOptionsMonitor<ApiKeyAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, ICacheService cacheService) : base(options, logger, encoder, clock)
{
    _cacheService = cacheService;
}

// and then update:
var clientId = await _cacheService.GetClientIdFromApiKey(apiKey);

With the above, we finish a simple yet fast implementation for validating keys. All that is left is to register the middleware.


Given that how the API keys are stored in your system depends greatly on your infrastructure (database, another microservice, secrets vault, etc), I'll omit the implementation for the Clients service.


4. Injecting the services required

With the implementation ready, we now need to inject the services we created:

services
    .AddScoped<ICacheService, CacheService>()
    .AddScoped<ApiKeyAuthenticationHandler>();
    
services.AddAuthentication()
    .AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationOptions.DefaultScheme, null);

Notice that the call to AddAuthentication is not using the overload that accepts a scheme. If this API key is the only authentication method you will use in an application, it might be better to use this instead so that the scheme is the default one:

services.AddAuthentication(ApiKeyAuthenticationOptions.DefaultScheme)

5. Adding support for invalidating keys

Now that we have our solution in place, we need to be able to invalidate keys on demand. It's a common mistake to think that it is enough to just support keys expiring after a given set of time, when it can happen that keys become exposed and cannot be trusted anymore.


In order to approach this, it might be enough to just delete the entry from the memory cache followed by invalidating the key on your key storage:


public class CacheService : ICacheService
{
    // ...
    
    public async Task InvalidateApiKey(string apiKey)
    {
        if (_memoryCache.TryGetValue<Dictionary<string, Guid>>("Authentication_ApiKeys", out var internalKeys))
        {
            if (internalKeys.ContainsKey(apiKey))
            {
                internalKeys.Remove(apiKey);
                _memoryCache.Set("Authentication_ApiKeys", internalKeys);
            }
        }

        await _clientsService.InvalidateApiKey(apiKey);
    }
}


6. Adding OpenAPI support

Considering that OpenAPI adoption grows every time more, a common requirement is to support authentication in OpenAPI so that users can execute requests. Fortunately, adding support for our custom API keys is quite simple!

services.AddSwaggerGen(setup =>
{
    setup.AddSecurityDefinition(ApiKeyAuthenticationOptions.DefaultScheme, new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Name = ApiKeyAuthenticationOptions.HeaderName,
        Type = SecuritySchemeType.ApiKey
    });

    setup.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = ApiKeyAuthenticationOptions.DefaultScheme
                }
            }, 
            Array.Empty<string>()
        }
    });
});

7. Summing up

With the code fragments above, we have a fully functional API key implementation in ASP.NET Core. Let's review our objectives:

  • Generating secure keys - Done!

  • Securely validating the keys - Done!

  • Ability to invalidate keys - Done!

  • [Optional] Validating the keys should be unobtrusive - Done!

  • [Optional] Validating the keys should be fast - Done!

  • [Optional] The keys should be URL-safe - Done!

  • [Optional] The keys should look a bit custom! - Done!


We now have all our objectives, including a bonus for supporting OpenAPI done.

This solution can still be improved, as for example the AuthenticationHandler class supports customizing Challenge and Forbidden results.


The source code for this solution can be found on my GitHub page: https://github.com/CamiloTerevinto/Blog.


Thanks for reading! I hope you have learned with this tutorial and please feel free to reach out in the comments if you have any questions, suggestions or any other comment.

3,316 views6 comments

Related Posts

See All