Adding API Key Authentication to ASP.NET Core Web API with AuthenticationHandler
Authentication is the process by which a user of a system is identified. ASP.NET Core allows multiple methods of authentication through authentication handlers which each support a different method of authentication. This can be with a cookie, a JSON Web Token (JWT), through OpenID or others.
What is an AuthenticationHandler?
An AuthenticationHandler
is a class that is responsible for authenticating a request. For each method of authentication, for example you can see how the JwtBearerHandler in ASP.NET Core 7 validates a provided JWT to identify the user for that request.
You can also see how if a JWT is not provided, it is also responsible for sending the appropriate challenge to the client by modifying the response. For more detail on what authentication handlers are responsible for and how they operate I’d recommend reading the Overview of ASP.NET Core Authentication page on Microsoft Learn. I also have an additional blog post covering the ASP.NET Core authentication process in more detail.
Implementing API Key Authentication
API key authentication is where a user is identified through a credential in the form of a secret key. The server is sent the secret key with each request, which it will then check to ensure that the client is authorised to access the requested resource or perform the specified operation.
There are many different options to implementing API key authentication, such as checking the key in a piece of custom ASP.NET Core middleware or through a controller attribute. In this post, I am going to implement it by extending the ASP.NET Core authentication middleware. This is my preference as it will allow any existing policies to operate the same as they would with any other method of authentication.
To implement API key authentication, we will need to do the following:
- Add an options class to load settings such as the header name to check for the API key.
- Add an
ApiKeyHandler
by extendingAuthenticationHandler
. - Register the options and handler with the authentication middleware.
You can find the steps for each of these below.
In the example below, I will use an API key from appsettings.json
as the valid key, however, in a real scenario this will probably be checked against a database.
Creating ApiKeyAuthenticationOptions
We will need to store some settings in appsettings.json
to allow the API key authentication process to be modified. In this example, the name of the header to check for the API key, the authorised API key, and whether the key is allowed for read-only or read and write access (to demonstrate changing claims).
To do this we will need to create an ApiKeySchemeOptions
class which inherits AuthenticationSchemeOptions
.
public class ApiKeySchemeOptions : AuthenticationSchemeOptions
{
public string HeaderName { get; set; } = "X-Api-Key";
public string? ApiKey { get; set; }
public bool ReadOnly { get; set; } = true;
public override void Validate()
{
if (string.IsNullOrEmpty(HeaderName))
{
throw new ArgumentException("Header name must be provided.");
}
if (string.IsNullOrEmpty(ApiKey))
{
throw new ArgumentException("API key must be provided.");
}
}
}
This class also implements the `Validate` method to ensure that all values are provided when necessary. The class also sets a default header name of X-Api-Key
if one is not provided.
To finish this, we will need to add the following section to the appsettings.json
file.
"ApiKeyOptions": {
"HeaderName": "X-Api-Key",
"ApiKey": "my-test-api-key",
"ReadOnly": true
}
The next step will be to implement the ApiKeyHandler
class to handle the authentication process.
Implementing the ApiKeyHandler
The ApiKeyHandler
will be responsible for checking that a provided API key is valid, and returning an AuthenticateResult
to indicate if the request is authenticated or not.
Create the ApiKeyHandler
class which inherits AuthenticationHandler<ApiKeySchemeOptons>
. This will provide access to the API key authentication options we setup in the previous step. As we need to access the HttpRequest
we will need to use IHttpContextAccessor
. To do this we will need to add IHttpContextAccessor
to the class constructor and store it in a private field.
After this has been done, your class should look like the following.
public class ApiKeyHandler : AuthenticationHandler<ApiKeySchemeOptions>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public TestAuth(IOptionsMonitor<ApiKeySchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IHttpContextAccessor httpContextAccessor) : base(options, logger, encoder, clock)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
throw new NotImplementedException();
}
}
The next step, will be to implement the HandleAuthenticateAsync
method, which will need to carry out the following steps:
- Check the API key header is present, if not, it should return not return a result.
- Ensure that only one API key has been provided, if more than one is provided authentication should fail.
- Check that the provided API key is correct, if not, authentication should fail.
- Now that the API key has been validated, create a ticket for the user with an appropriate list of claims.
- Return a successful authentication with the user information.
You can see this process followed in the implementation below.
public class ApiKeyHandler : AuthenticationHandler<ApiKeySchemeOptions>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ApiKeyHandler(IOptionsMonitor<ApiKeySchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IHttpContextAccessor httpContextAccessor) : base(options, logger, encoder, clock)
{
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Retrieve the API key from the specified header name.
StringValues apiKeys = StringValues.Empty;
bool apiKeyPresent = _httpContextAccessor.HttpContext?.Request.Headers.TryGetValue(Options.HeaderName, out apiKeys) ?? false;
// Return 'NoResult' if the header is not present as this is not intended for the ApiKeyHandler to handle.
if (!apiKeyPresent)
{
return AuthenticateResult.NoResult();
}
// Ensure only one API key is provided, otherwise return 'Fail' with an error message.
if (apiKeys.Count > 1)
{
return AuthenticateResult.Fail("Multiple API keys found in request. Please only provide one key.");
}
// Ensure the API key provided is valid.
if (string.IsNullOrEmpty(Options.ApiKey) || !Options.ApiKey.Equals(apiKeys.FirstOrDefault()))
{
return AuthenticateResult.Fail("Invalid API key.");
}
// Create a ClaimsIdentity with all the claims associated with the API key. This would usually come from a database.
List<Claim> claims = new()
{
new Claim(ClaimTypes.NameIdentifier, Options.ApiKey),
new Claim(ClaimTypes.Name, "API Key User")
};
if (Options.ReadOnly)
{
claims.Add(new Claim(ClaimTypes.Role, "ReadOnly")); ;
}
else
{
claims.Add(new Claim(ClaimTypes.Role, "ReadWrite")); ;
}
// Create a ClaimsIdentity, ClaimsPrincipal and return an AuthenticationTicket for the user with the claims.
ClaimsIdentity identity = new(claims, Scheme.Name);
ClaimsPrincipal principal = new(identity);
AuthenticationTicket ticket = new(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
This also includes a check for the ReadOnly
option to determine if the added role should be ReadOnly
or ReadWrite
.
Finally, the claims are used to create a ClaimsIdentity
, which is then passed to a ClaimsPrincipal
to represent the user. This is then wrapped in a AuthenticationTicket
and returned as a successful authentication using AuthenticateResult.Success
.
Registering with the Authentication Middleware
Now that the ApiKeyHandler
has been implemented, we need to register the services we need, our options, and our new scheme with the ASP.NET Core middleware.
To do this we can add the following code to our application startup.cs
file.
builder.Services.AddHttpContextAccessor();
builder.Services.AddAuthentication().AddScheme<ApiKeySchemeOptions, ApiKeyHandler>("ApiKey", (options) =>
{
builder.Configuration.GetRequiredSection("ApiKeyOptions").Bind(options);
});
builder.Services.AddAuthorization();
This will add the HttpContextAccessor
service used in the ApiKeyHandler
, and register the ApiKey
authentication scheme, while binding the required options from the ApiKey appsettings.json
section.
Bonus: Updating Swagger for API Key Authentication
To make it easier to test the API while using API key authentication, we can make some changes to Swagger to add the necessary options to the UI and tell Swagger how to modify the requests when using that authentication method.
We will need to replace builder.Services.AddSwaggerGen();
with the following code to add the necessary option in startup.cs
.
builder.Services.AddSwaggerGen(options =>
{
options.AddSecurityDefinition("APIKey", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please enter your API Key",
Name = "X-Api-Key",
Type = SecuritySchemeType.ApiKey
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "APIKey"
}
},
Array.Empty<string>()
}
});
});
This will automatically enable authentication in Swagger and add an API key authentication option which allows the valid API key to be provided to authenticate a request.
If you would like to find out more about how to modify Swagger for different authentication methods, I’d recommend reading the Add Security Definitions and Requirements section in the Swashbuckle.AspNetCore project readme file.
Summary
We have added API key authentication to an ASP.NET Core project by implementing the appropriate authentication handler and registering options to allow the behaviour of the handler to be controlled.
The Swagger configuration has also been updated to define a SecurityDefinition
and a SecurityRequirement
, which will allow the request to be authenticated using an API key while exploring the API in Swagger.