< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Authentication.ApiKeyAuthHandler
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Authentication/ApiKeyAuthHandler.cs
Tag: Kestrun/Kestrun@9d3a582b2d63930269564a7591aa77ef297cadeb
Line coverage
95%
Covered lines: 85
Uncovered lines: 4
Coverable lines: 89
Total lines: 251
Line coverage: 95.5%
Branch coverage
81%
Covered branches: 44
Total branches: 54
Branch coverage: 81.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%
HandleAuthenticateAsync()81.25%181678.94%
TryGetApiKey(...)100%1616100%
ValidateApiKeyAsync()25%44100%
CreateTicketAsync(...)100%11100%
Fail(...)100%11100%
HandleChallengeAsync(...)66.66%66100%
BuildPsValidator(...)100%22100%
BuildCsValidator(...)75%44100%
BuildVBNetValidator(...)75%44100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Authentication/ApiKeyAuthHandler.cs

#LineLine coverage
 1using System.Text;
 2using System.Text.Encodings.Web;
 3using Microsoft.AspNetCore.Authentication;
 4using Microsoft.Extensions.Options;
 5using Microsoft.Extensions.Primitives;
 6
 7namespace Kestrun.Authentication;
 8
 9/// <summary>
 10/// Handles API Key authentication for incoming HTTP requests.
 11/// </summary>
 12public class ApiKeyAuthHandler
 13    : AuthenticationHandler<ApiKeyAuthenticationOptions>
 14{
 15    /// <summary>
 16    /// Initializes a new instance of the <see cref="ApiKeyAuthHandler"/> class.
 17    /// </summary>
 18    /// <param name="options">The options monitor for API key authentication options.</param>
 19    /// <param name="logger">The logger factory.</param>
 20    /// <param name="encoder">The URL encoder.</param>
 21    public ApiKeyAuthHandler(
 22        IOptionsMonitor<ApiKeyAuthenticationOptions> options,
 23        ILoggerFactory logger,
 24        UrlEncoder encoder)
 1225        : base(options, logger, encoder)
 26    {
 1227        if (options.CurrentValue.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 28        {
 929            options.CurrentValue.Logger.Debug("ApiKeyAuthHandler initialized");
 30        }
 1231    }
 32
 33    /// <summary>
 34    /// Authenticates the incoming request using an API key.
 35    /// </summary>
 36    /// <returns>An <see cref="AuthenticateResult"/> indicating the authentication outcome.</returns>
 37    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
 38    {
 39        try
 40        {
 1141            if (Options.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 42            {
 843                Options.Logger.Debug("Handling API Key authentication for request: {Request}", Request);
 44            }
 45
 1146            if (Options.RequireHttps && !Request.IsHttps)
 47            {
 148                return Fail("HTTPS required");
 49            }
 50
 1051            if (!TryGetApiKey(out var providedKey))
 52            {
 153                return Fail("Missing API Key");
 54            }
 55
 956            var providedKeyBytes = Encoding.UTF8.GetBytes(providedKey);
 957            var valid = await ValidateApiKeyAsync(providedKey, providedKeyBytes);
 958            if (!valid)
 59            {
 360                return Fail($"Invalid API Key: {providedKey}");
 61            }
 62
 663            var ticket = await CreateTicketAsync(providedKey);
 664            if (ticket.Principal is null)
 65            {
 066                return Fail("Authentication ticket has no principal");
 67            }
 668            Options.Logger.Information("API Key authentication succeeded for identity: {Identity}", ticket.Principal.Ide
 69
 670            return AuthenticateResult.Success(ticket);
 71        }
 072        catch (Exception ex)
 73        {
 074            Options.Logger.Error(ex, "API Key authentication failed with exception.");
 075            return Fail("Error processing API Key");
 76        }
 1177    }
 78
 79    /// <summary>
 80    /// Tries to retrieve the API key from the request headers or query string.
 81    /// </summary>
 82    /// <param name="providedKey">The retrieved API key.</param>
 83    /// <returns>True if the API key was found; otherwise, false.</returns>
 84    private bool TryGetApiKey(out string providedKey)
 85    {
 1086        providedKey = string.Empty;
 87
 88        // Primary header
 1089        if (!Request.Headers.TryGetValue(Options.HeaderName, out var values))
 90        {
 91            // Additional headers
 1192            foreach (var header in Options.AdditionalHeaderNames)
 93            {
 394                if (Request.Headers.TryGetValue(header, out values))
 95                {
 96                    break;
 97                }
 98            }
 99        }
 100
 101        // Query string fallback
 10102        if ((values.Count == 0 || StringValues.IsNullOrEmpty(values))
 10103            && Options.AllowQueryStringFallback
 10104            && Request.Query.TryGetValue(Options.HeaderName, out var qsValues))
 105        {
 2106            values = qsValues;
 107        }
 108
 10109        if (StringValues.IsNullOrEmpty(values))
 110        {
 1111            return false;
 112        }
 113
 114        // Normalize potential whitespace/newlines from transport
 9115        providedKey = values.ToString().Trim();
 9116        return true;
 117    }
 118
 119    /// <summary>
 120    /// Validates the provided API key against the expected key or a custom validation method.
 121    /// </summary>
 122    /// <param name="providedKey">The API key provided by the client.</param>
 123    /// <param name="providedKeyBytes">The byte representation of the provided API key.</param>
 124    /// <returns>True if the API key is valid; otherwise, false.</returns>
 125    private async Task<bool> ValidateApiKeyAsync(string providedKey, byte[] providedKeyBytes)
 126    {
 9127        return Options.ExpectedKeyBytes is not null
 9128            ? FixedTimeEquals.Test(providedKeyBytes, Options.ExpectedKeyBytes)
 9129            : Options.ValidateKeyAsync is not null
 9130            ? await Options.ValidateKeyAsync(Context, providedKey, providedKeyBytes)
 9131            : throw new InvalidOperationException(
 9132            "No API key validation configured. Either set ValidateKey or ExpectedKey in ApiKeyAuthenticationOptions.");
 9133    }
 134
 135    /// <summary>
 136    /// Creates an authentication ticket for the provided API key.
 137    /// </summary>
 138    /// <param name="providedKey"></param>
 139    /// <returns></returns>
 140    private Task<AuthenticationTicket> CreateTicketAsync(string providedKey)
 6141        => IAuthHandler.GetAuthenticationTicketAsync(Context, providedKey, Options, Scheme, "ApiKeyClient");
 142
 143    /// <summary>
 144    /// Fails the authentication process with the specified reason.
 145    /// </summary>
 146    /// <param name="reason">The reason for the failure.</param>
 147    /// <returns>An <see cref="AuthenticateResult"/> indicating the failure.</returns>
 148    private AuthenticateResult Fail(string reason)
 149    {
 5150        Options.Logger.Warning("API Key authentication failed: {Reason}", reason);
 5151        return AuthenticateResult.Fail(reason);
 152    }
 153
 154    /// <summary>
 155    /// Handles the authentication challenge by setting the appropriate response headers and status code.
 156    /// </summary>
 157    /// <param name="properties">Authentication properties for the challenge.</param>
 158    /// <returns>A completed task.</returns>
 159    protected override Task HandleChallengeAsync(AuthenticationProperties properties)
 160    {
 1161        if (Options.EmitChallengeHeader)
 162        {
 1163            var header = Options.HeaderName ?? "X-Api-Key";
 164
 1165            var value = Options.ChallengeHeaderFormat == ApiKeyChallengeFormat.ApiKeyHeader
 1166                ? $"ApiKey header=\"{header}\""
 1167                : header;
 168
 1169            Response.Headers.WWWAuthenticate = value;
 170        }
 1171        Response.StatusCode = StatusCodes.Status401Unauthorized;
 1172        return Task.CompletedTask;
 173    }
 174    /// <summary>
 175    /// Builds a PowerShell-based API key validator delegate using the provided authentication code settings.
 176    /// </summary>
 177    /// <param name="settings">The settings containing the PowerShell authentication code.</param>
 178    /// <param name="logger">The logger to use for debug output.</param>
 179    /// <returns>A delegate that validates an API key using PowerShell code.</returns>
 180    /// <remarks>
 181    ///  This method compiles the PowerShell script and returns a delegate that can be used to validate API keys.
 182    /// </remarks>
 183    public static Func<HttpContext, string, byte[], Task<bool>> BuildPsValidator(AuthenticationCodeSettings settings, Se
 184    {
 2185        if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 186        {
 2187            logger.Debug("BuildPsValidator  settings: {Settings}", settings);
 188        }
 189
 2190        return async (ctx, providedKey, providedKeyBytes) =>
 2191               {
 2192                   return await IAuthHandler.ValidatePowerShellAsync(settings.Code, ctx, new Dictionary<string, string>
 2193                   {
 2194                    { "providedKey", providedKey },
 2195                    { "providedKeyBytes", Convert.ToBase64String(providedKeyBytes) }
 2196                   }, logger);
 4197               };
 198    }
 199
 200    /// <summary>
 201    /// Builds a C#-based API key validator delegate using the provided authentication code settings.
 202    /// </summary>
 203    /// <param name="settings">The settings containing the C# authentication code.</param>
 204    /// <param name="logger">The logger to use for debug output.</param>
 205    /// <returns>A delegate that validates an API key using C# code.</returns>
 206    public static Func<HttpContext, string, byte[], Task<bool>> BuildCsValidator(AuthenticationCodeSettings settings, Se
 207    {
 1208        if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 209        {
 1210            logger.Debug("BuildCsValidator  settings: {Settings}", settings);
 211        }
 212        // pass the settings to the core C# validator
 1213        var core = IAuthHandler.BuildCsValidator(
 1214            settings,
 1215            logger,
 1216            ("providedKey", string.Empty), ("providedKeyBytes", Array.Empty<byte>())
 1217            ) ?? throw new InvalidOperationException("Failed to build C# validator delegate from provided settings.");
 1218        return (ctx, providedKey, providedKeyBytes) =>
 3219            core(ctx, new Dictionary<string, object?>
 3220            {
 3221                ["providedKey"] = providedKey,
 3222                ["providedKeyBytes"] = providedKeyBytes
 3223            });
 224    }
 225
 226    /// <summary>
 227    /// Builds a VB.NET-based API key validator delegate using the provided authentication code settings.
 228    /// </summary>
 229    /// <param name="settings">The settings containing the VB.NET authentication code.</param>
 230    /// <param name="logger">The logger to use for debug output.</param>
 231    /// <returns>A delegate that validates an API key using VB.NET code.</returns>
 232    public static Func<HttpContext, string, byte[], Task<bool>> BuildVBNetValidator(AuthenticationCodeSettings settings,
 233    {
 2234        if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 235        {
 1236            logger.Debug("BuildVBNetValidator  settings: {Settings}", settings);
 237        }
 238        // pass the settings to the core VB.NET validator
 2239        var core = IAuthHandler.BuildVBNetValidator(
 2240            settings,
 2241            logger,
 2242            ("providedKey", string.Empty), ("providedKeyBytes", Array.Empty<byte>())
 2243            ) ?? throw new InvalidOperationException("Failed to build VB.NET validator delegate from provided settings."
 2244        return (ctx, providedKey, providedKeyBytes) =>
 4245            core(ctx, new Dictionary<string, object?>
 4246            {
 4247                ["providedKey"] = providedKey,
 4248                ["providedKeyBytes"] = providedKeyBytes
 4249            });
 250    }
 251}