I have an ASP.NET Core 3.1 Web API project that has no security enforced. Now I need to secure it with an API key based authentication scheme.
The basic idea is to let the clients of this Web API pass in an API key in a header, then all the client API keys will be held in the appsettings.json
file.
These are the implementation details:
ApiKeyAuthenticationOptions
where MyConfigValue
will be used to hold the keys public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "ApiKey";
public string Scheme => DefaultScheme;
public string AuthenticationType = DefaultScheme;
public string MyConfigValue { get; set; }
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(Configuration);
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddApiKeyAuthentication(Configuration);
services.AddControllers();
}
AuthenticationBuilder
is defined as public static AuthenticationBuilder AddApiKeyAuthentication(this AuthenticationBuilder builder, IConfiguration configuration)
{
var config = configuration["MyConfigValue"];
builder.Services.Configure<ApiKeyAuthenticationOptions>(options =>
{
options.MyConfigValue = config;
});
builder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationOptions.DefaultScheme, options => { });
return builder;
}
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private const string ApiKeyName = "X-Api-Key";
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var myConfigValue = Options.MyConfigValue;
if (!Request.Headers.ContainsKey(ApiKeyName))
{
return AuthenticateResult.Fail("Missing API Key");
}
var apiKey = Request.Headers[ApiKeyName];
if (apiKey != myConfigValue)
{
return AuthenticateResult.Fail("Invalid API Key");
}
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, "api-user") };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
The problem occurs here: The options
in the constructor
has a property CurrentValue
that has the customized property MyConfigValue
with the correct value from appsettings
, however, in the function HandleAuthenticateAsync
, the Options
doesn't have a property of CurrentValue
, but it has the customized property MyConfigValue
that is null. Therefore, the options
in the constructor is not the same as the Options
in the function. My implementation of class ApiKeyAuthenticationHandler
probably is wrong. One workaround now is to retrieve MyConfigValue
inside the constructor
and save it for use in the function.
Secure API calls by using:
[Authorize(AuthenticationSchemes = "ApiKey")]
I set MyConfigValue
in the root level of appsettings.json
.
However, whenever the function HandleAuthenticateAsync
is called triggered by secured API calls, options.MyConfigValue
is always null.
As soon as I use default ApiKeyAuthenticationOptions
without MyConfigValue
included, and then obtaining it inside ApiKeyAuthenticationHandler
, then it works. But that requires obtaining MyConfigValue
for each API call, not efficient.
A simple solution in Visual Studio 2022 is available, I can provide it if asked. I will also try to figure out a way to upload to GitHub.
Can someone tell me what goes wrong?
Thanks a lot in advance.
The builder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationOptions.DefaultScheme, options => { });
reset the ApiKeyAuthenticationOptions which caused your issue.
You should directly use the builder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationOptions.DefaultScheme, options => { options.MyConfigValue = configuration["MyConfigValue"]; });
Details, like below:
public static class AuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddApiKeyAuthentication(this AuthenticationBuilder builder, IConfiguration configuration)
{
var config = configuration["MyConfigValue"];
builder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationOptions.DefaultScheme, options => { options.MyConfigValue = configuration["MyConfigValue"]; });
return builder;
}
}