Search code examples
c#asp.net-coreauthenticationconfigurationasp.net-core-webapi

Can't make customized option work when enforcing API-key based authentication


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:

  1. Setup a customized 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; }
    }
  1. In startup, the authentication logic is added to service collection
    public void ConfigureServices(IServiceCollection services)
    {
            services.AddSingleton(Configuration);
         services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
             .AddApiKeyAuthentication(Configuration);
         services.AddControllers();
    }
  1. The extension method of 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;
    }
  1. The authentication handler is defined as
    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.

  1. Secure API calls by using:

    [Authorize(AuthenticationSchemes = "ApiKey")]
    
  2. 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.


Solution

  • 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;
        }
    }