'How to create a ApiKey authentication scheme for .NET Core

I trying to set ApiKey authentication scheme to my Api but can't find any post or documentation about it.

Closer thing i found is this Microsoft page, but nothing say on how to register a authentication handler.

I'm using .Net Core 6.



Solution 1:[1]

Found out a solution mostly based on this great Joonas Westlin guide to implement basic authentication scheme. Credits should go to him.

Steps:

1. Implement the options class inheriting from `AuthenticationSchemeOptions` and other boiler classes that will be need after.
2. Create the handler, inherit from `AuthenticationHandler<TOptions>`    
3. Override handler methods `HandleAuthenticateAsync` to get the key and call your implementation of `IApiKeyAuthenticationService` 
4. Register the scheme with `AddScheme<TOptions, THandler>(string, Action<TOptions>)` on the `AuthenticationBuilder`, which you get by calling `AddAuthentication` on the service collection
5. Implement the `IApiKeyAuthenticationService` and add it to Service Collection.

Here all the code. The AuthenticationSchemeOptions and other boiler classes:

//the Service interface for the service that will get the key to validate against some store
public interface IApiKeyAuthenticationService
{
    Task<bool> IsValidAsync(string apiKey);
}
//the class for defaults following the similar to .Net Core JwtBearerDefaults class
public static class ApiKeyAuthenticationDefaults
{
    public const string AuthenticationScheme = "ApiKey";
}

public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions{}; //Nothing to do

public class ApiKeyAuthenticationPostConfigureOptions : IPostConfigureOptions<ApiKeyAuthenticationOptions>
{
    public void PostConfigure(string name, ApiKeyAuthenticationOptions options){} //Nothing to do
};

The handler:

public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
    private const string AuthorizationHeaderName = "Authorization";
    private const string ApiKeySchemeName = ApiKeyAuthenticationDefaults.AuthenticationScheme;
    private readonly IApiKeyAuthenticationService _authenticationService;

    public ApiKeyAuthenticationHandler(
        IOptionsMonitor<ApiKeyAuthenticationOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock,
        IApiKeyAuthenticationService authenticationService)
        : base(options, logger, encoder, clock)
    {
        _authenticationService = authenticationService;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.ContainsKey(AuthorizationHeaderName))
        {
            //Authorization header not in request
            return AuthenticateResult.NoResult();
        }

        if (!AuthenticationHeaderValue.TryParse(Request.Headers[AuthorizationHeaderName], out AuthenticationHeaderValue? headerValue))
        {
            //Invalid Authorization header
            return AuthenticateResult.NoResult();
        }

        if (!ApiKeySchemeName.Equals(headerValue.Scheme, StringComparison.OrdinalIgnoreCase))
        {
            //Not ApiKey authentication header
            return AuthenticateResult.NoResult();
        }
        if ( headerValue.Parameter is null)
        {
            //Missing key
            return AuthenticateResult.Fail("Missing apiKey");
        }            

        bool isValid = await _authenticationService.IsValidAsync(headerValue.Parameter);

        if (!isValid)
        {
            return AuthenticateResult.Fail("Invalid apiKey");
        }
        var claims = new[] { new Claim(ClaimTypes.Name, "Service") };            
        var identity = new ClaimsIdentity(claims, Scheme.Name);
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, Scheme.Name);
        return AuthenticateResult.Success(ticket);
    }

    protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        Response.Headers["WWW-Authenticate"] = $"ApiKey \", charset=\"UTF-8\"";
        await base.HandleChallengeAsync(properties);
    }
}

The extensions to make it easy to register the scheme:

public static class ApiKeyAuthenticationExtensions
{
    public static AuthenticationBuilder AddApiKey<TAuthService>(this AuthenticationBuilder builder)
        where TAuthService : class, IApiKeyAuthenticationService
    {
        return AddApiKey<TAuthService>(builder, ApiKeyAuthenticationDefaults.AuthenticationScheme, _ => { });
    }

    public static AuthenticationBuilder AddApiKey<TAuthService>(this AuthenticationBuilder builder, string authenticationScheme)
        where TAuthService : class, IApiKeyAuthenticationService
    {
        return AddApiKey<TAuthService>(builder, authenticationScheme, _ => { });
    }

    public static AuthenticationBuilder AddApiKey<TAuthService>(this AuthenticationBuilder builder, Action<ApiKeyAuthenticationOptions> configureOptions)
        where TAuthService : class, IApiKeyAuthenticationService
    {
        return AddApiKey<TAuthService>(builder, ApiKeyAuthenticationDefaults.AuthenticationScheme, configureOptions);
    }

    public static AuthenticationBuilder AddApiKey<TAuthService>(this AuthenticationBuilder builder, string authenticationScheme, Action<ApiKeyAuthenticationOptions> configureOptions)
        where TAuthService : class, IApiKeyAuthenticationService
    {
        builder.Services.AddSingleton<IPostConfigureOptions<ApiKeyAuthenticationOptions>, ApiKeyAuthenticationPostConfigureOptions>();
        builder.Services.AddTransient<IApiKeyAuthenticationService, TAuthService>();

        return builder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(
            authenticationScheme, configureOptions);
    }
}

Implement the Authentication service to validate the key against your config file or other store:

public class ApiKeyAuthenticationService : IApiKeyAuthenticationService
{
    public Task<bool> IsValidAsync(string apiKey)
    {
        //Write your validation code here
        return Task.FromResult(apiKey == "Test");
    }
}

Now to use only is need to add this at start:

//register the schema
builder.Services.AddAuthentication(ApiKeyAuthenticationDefaults.AuthenticationScheme)
  .AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, null);

//Register the Authentication service Handler that will be consumed by the handler.
builder.Services.AddSingleton<IApiKeyAuthenticationService,ApiKeyAuthenticationService>();

Or in a more elegant way using the extensions:

builder.Services
.AddAuthentication(ApiKeyAuthenticationDefaults.AuthenticationScheme)
.AddApiKey<ApiKeyAuthenticationService>();

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1