< 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@2d87023b37eb91155071c91dd3d6a2eeb3004705
Line coverage
94%
Covered lines: 85
Uncovered lines: 5
Coverable lines: 90
Total lines: 262
Line coverage: 94.4%
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 08/26/2025 - 01:25:22 Line coverage: 93.2% (83/89) Branch coverage: 77.7% (42/54) Total lines: 251 Tag: Kestrun/Kestrun@07f821172e5dc3657f1be7e6818f18d6721cf38a09/06/2025 - 18:30:33 Line coverage: 95.5% (85/89) Branch coverage: 81.4% (44/54) Total lines: 251 Tag: Kestrun/Kestrun@aeddbedb8a96e9137aac94c2d5edd011b57ac87110/13/2025 - 16:52:37 Line coverage: 94.4% (85/90) Branch coverage: 81.4% (44/54) Total lines: 262 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e 08/26/2025 - 01:25:22 Line coverage: 93.2% (83/89) Branch coverage: 77.7% (42/54) Total lines: 251 Tag: Kestrun/Kestrun@07f821172e5dc3657f1be7e6818f18d6721cf38a09/06/2025 - 18:30:33 Line coverage: 95.5% (85/89) Branch coverage: 81.4% (44/54) Total lines: 251 Tag: Kestrun/Kestrun@aeddbedb8a96e9137aac94c2d5edd011b57ac87110/13/2025 - 16:52:37 Line coverage: 94.4% (85/90) Branch coverage: 81.4% (44/54) Total lines: 262 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Host()100%210%
.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 Kestrun.Hosting;
 4using Microsoft.AspNetCore.Authentication;
 5using Microsoft.Extensions.Options;
 6using Microsoft.Extensions.Primitives;
 7
 8namespace Kestrun.Authentication;
 9
 10/// <summary>
 11/// Handles API Key authentication for incoming HTTP requests.
 12/// </summary>
 13public class ApiKeyAuthHandler
 14    : AuthenticationHandler<ApiKeyAuthenticationOptions>
 15{
 16    /// <summary> The Kestrun host instance. </summary>
 017    public KestrunHost Host { get; }
 18
 19    /// <summary>
 20    /// Initializes a new instance of the <see cref="ApiKeyAuthHandler"/> class.
 21    /// </summary>
 22    /// <param name="host">The Kestrun host instance.</param>
 23    /// <param name="options">The options monitor for API key authentication options.</param>
 24    /// <param name="logger">The logger factory.</param>
 25    /// <param name="encoder">The URL encoder.</param>
 26    public ApiKeyAuthHandler(KestrunHost host,
 27        IOptionsMonitor<ApiKeyAuthenticationOptions> options,
 28        ILoggerFactory logger,
 29        UrlEncoder encoder)
 1230        : base(options, logger, encoder)
 31    {
 1232        ArgumentNullException.ThrowIfNull(host);
 1233        Host = host;
 1234        if (options.CurrentValue.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 35        {
 936            options.CurrentValue.Logger.Debug("ApiKeyAuthHandler initialized");
 37        }
 1238    }
 39
 40    /// <summary>
 41    /// Authenticates the incoming request using an API key.
 42    /// </summary>
 43    /// <returns>An <see cref="AuthenticateResult"/> indicating the authentication outcome.</returns>
 44    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
 45    {
 46        try
 47        {
 1148            if (Options.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 49            {
 850                Options.Logger.Debug("Handling API Key authentication for request: {Request}", Request);
 51            }
 52
 1153            if (Options.RequireHttps && !Request.IsHttps)
 54            {
 155                return Fail("HTTPS required");
 56            }
 57
 1058            if (!TryGetApiKey(out var providedKey))
 59            {
 160                return Fail("Missing API Key");
 61            }
 62
 963            var providedKeyBytes = Encoding.UTF8.GetBytes(providedKey);
 964            var valid = await ValidateApiKeyAsync(providedKey, providedKeyBytes);
 965            if (!valid)
 66            {
 367                return Fail($"Invalid API Key: {providedKey}");
 68            }
 69
 670            var ticket = await CreateTicketAsync(providedKey);
 671            if (ticket.Principal is null)
 72            {
 073                return Fail("Authentication ticket has no principal");
 74            }
 675            Options.Logger.Information("API Key authentication succeeded for identity: {Identity}", ticket.Principal.Ide
 76
 677            return AuthenticateResult.Success(ticket);
 78        }
 079        catch (Exception ex)
 80        {
 081            Options.Logger.Error(ex, "API Key authentication failed with exception.");
 082            return Fail("Error processing API Key");
 83        }
 1184    }
 85
 86    /// <summary>
 87    /// Tries to retrieve the API key from the request headers or query string.
 88    /// </summary>
 89    /// <param name="providedKey">The retrieved API key.</param>
 90    /// <returns>True if the API key was found; otherwise, false.</returns>
 91    private bool TryGetApiKey(out string providedKey)
 92    {
 1093        providedKey = string.Empty;
 94
 95        // Primary header
 1096        if (!Request.Headers.TryGetValue(Options.HeaderName, out var values))
 97        {
 98            // Additional headers
 1199            foreach (var header in Options.AdditionalHeaderNames)
 100            {
 3101                if (Request.Headers.TryGetValue(header, out values))
 102                {
 103                    break;
 104                }
 105            }
 106        }
 107
 108        // Query string fallback
 10109        if ((values.Count == 0 || StringValues.IsNullOrEmpty(values))
 10110            && Options.AllowQueryStringFallback
 10111            && Request.Query.TryGetValue(Options.HeaderName, out var qsValues))
 112        {
 2113            values = qsValues;
 114        }
 115
 10116        if (StringValues.IsNullOrEmpty(values))
 117        {
 1118            return false;
 119        }
 120
 121        // Normalize potential whitespace/newlines from transport
 9122        providedKey = values.ToString().Trim();
 9123        return true;
 124    }
 125
 126    /// <summary>
 127    /// Validates the provided API key against the expected key or a custom validation method.
 128    /// </summary>
 129    /// <param name="providedKey">The API key provided by the client.</param>
 130    /// <param name="providedKeyBytes">The byte representation of the provided API key.</param>
 131    /// <returns>True if the API key is valid; otherwise, false.</returns>
 132    private async Task<bool> ValidateApiKeyAsync(string providedKey, byte[] providedKeyBytes)
 133    {
 9134        return Options.ExpectedKeyBytes is not null
 9135            ? FixedTimeEquals.Test(providedKeyBytes, Options.ExpectedKeyBytes)
 9136            : Options.ValidateKeyAsync is not null
 9137            ? await Options.ValidateKeyAsync(Context, providedKey, providedKeyBytes)
 9138            : throw new InvalidOperationException(
 9139            "No API key validation configured. Either set ValidateKey or ExpectedKey in ApiKeyAuthenticationOptions.");
 9140    }
 141
 142    /// <summary>
 143    /// Creates an authentication ticket for the provided API key.
 144    /// </summary>
 145    /// <param name="providedKey"></param>
 146    /// <returns></returns>
 147    private Task<AuthenticationTicket> CreateTicketAsync(string providedKey)
 6148        => IAuthHandler.GetAuthenticationTicketAsync(Context, providedKey, Options, Scheme, "ApiKeyClient");
 149
 150    /// <summary>
 151    /// Fails the authentication process with the specified reason.
 152    /// </summary>
 153    /// <param name="reason">The reason for the failure.</param>
 154    /// <returns>An <see cref="AuthenticateResult"/> indicating the failure.</returns>
 155    private AuthenticateResult Fail(string reason)
 156    {
 5157        Options.Logger.Warning("API Key authentication failed: {Reason}", reason);
 5158        return AuthenticateResult.Fail(reason);
 159    }
 160
 161    /// <summary>
 162    /// Handles the authentication challenge by setting the appropriate response headers and status code.
 163    /// </summary>
 164    /// <param name="properties">Authentication properties for the challenge.</param>
 165    /// <returns>A completed task.</returns>
 166    protected override Task HandleChallengeAsync(AuthenticationProperties properties)
 167    {
 1168        if (Options.EmitChallengeHeader)
 169        {
 1170            var header = Options.HeaderName ?? "X-Api-Key";
 171
 1172            var value = Options.ChallengeHeaderFormat == ApiKeyChallengeFormat.ApiKeyHeader
 1173                ? $"ApiKey header=\"{header}\""
 1174                : header;
 175
 1176            Response.Headers.WWWAuthenticate = value;
 177        }
 1178        Response.StatusCode = StatusCodes.Status401Unauthorized;
 1179        return Task.CompletedTask;
 180    }
 181    /// <summary>
 182    /// Builds a PowerShell-based API key validator delegate using the provided authentication code settings.
 183    /// </summary>
 184    ///<param name="host">The Kestrun host instance.</param>
 185    /// <param name="settings">The settings containing the PowerShell authentication code.</param>
 186    /// <returns>A delegate that validates an API key using PowerShell code.</returns>
 187    /// <remarks>
 188    ///  This method compiles the PowerShell script and returns a delegate that can be used to validate API keys.
 189    /// </remarks>
 190    public static Func<HttpContext, string, byte[], Task<bool>> BuildPsValidator(
 191            KestrunHost host,
 192        AuthenticationCodeSettings settings)
 193    {
 2194        if (host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 195        {
 2196            host.Logger.Debug("BuildPsValidator  settings: {Settings}", settings);
 197        }
 198
 2199        return async (ctx, providedKey, providedKeyBytes) =>
 2200               {
 2201                   return await IAuthHandler.ValidatePowerShellAsync(settings.Code, ctx, new Dictionary<string, string>
 2202                   {
 2203                    { "providedKey", providedKey },
 2204                    { "providedKeyBytes", Convert.ToBase64String(providedKeyBytes) }
 2205                   }, host.Logger);
 4206               };
 207    }
 208
 209    /// <summary>
 210    /// Builds a C#-based API key validator delegate using the provided authentication code settings.
 211    /// </summary>
 212    /// <param name="host">The Kestrun host instance.</param>
 213    /// <param name="settings">The settings containing the C# authentication code.</param>
 214    /// <returns>A delegate that validates an API key using C# code.</returns>
 215    public static Func<HttpContext, string, byte[], Task<bool>> BuildCsValidator(
 216        KestrunHost host,
 217        AuthenticationCodeSettings settings)
 218    {
 1219        if (host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 220        {
 1221            host.Logger.Debug("BuildCsValidator  settings: {Settings}", settings);
 222        }
 223        // pass the settings to the core C# validator
 1224        var core = IAuthHandler.BuildCsValidator(host,
 1225            settings,
 1226            ("providedKey", string.Empty), ("providedKeyBytes", Array.Empty<byte>())
 1227            ) ?? throw new InvalidOperationException("Failed to build C# validator delegate from provided settings.");
 1228        return (ctx, providedKey, providedKeyBytes) =>
 3229            core(ctx, new Dictionary<string, object?>
 3230            {
 3231                ["providedKey"] = providedKey,
 3232                ["providedKeyBytes"] = providedKeyBytes
 3233            });
 234    }
 235
 236    /// <summary>
 237    /// Builds a VB.NET-based API key validator delegate using the provided authentication code settings.
 238    /// </summary>
 239    /// <param name="host">The Kestrun host instance.</param>
 240    /// <param name="settings">The settings containing the VB.NET authentication code.</param>
 241    /// <returns>A delegate that validates an API key using VB.NET code.</returns>
 242    public static Func<HttpContext, string, byte[], Task<bool>> BuildVBNetValidator(
 243         KestrunHost host,
 244        AuthenticationCodeSettings settings)
 245    {
 2246        if (host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 247        {
 1248            host.Logger.Debug("BuildVBNetValidator  settings: {Settings}", settings);
 249        }
 250        // pass the settings to the core VB.NET validator
 2251        var core = IAuthHandler.BuildVBNetValidator(host,
 2252            settings,
 2253            ("providedKey", string.Empty), ("providedKeyBytes", Array.Empty<byte>())
 2254            ) ?? throw new InvalidOperationException("Failed to build VB.NET validator delegate from provided settings."
 2255        return (ctx, providedKey, providedKeyBytes) =>
 4256            core(ctx, new Dictionary<string, object?>
 4257            {
 4258                ["providedKey"] = providedKey,
 4259                ["providedKeyBytes"] = providedKeyBytes
 4260            });
 261    }
 262}