< 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@0d738bf294e6281b936d031e1979d928007495ff
Line coverage
94%
Covered lines: 85
Uncovered lines: 5
Coverable lines: 90
Total lines: 263
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 - 14:53:17 Line coverage: 93.2% (83/89) Branch coverage: 77.7% (42/54) Total lines: 251 Tag: Kestrun/Kestrun@78d1e497d8ba989d121b57aa39aa3c6b22de743109/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@10d476bee71c71ad215bb8ab59f219887b5b4a5e12/12/2025 - 17:27:19 Line coverage: 94.4% (85/90) Branch coverage: 81.4% (44/54) Total lines: 263 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd 08/26/2025 - 14:53:17 Line coverage: 93.2% (83/89) Branch coverage: 77.7% (42/54) Total lines: 251 Tag: Kestrun/Kestrun@78d1e497d8ba989d121b57aa39aa3c6b22de743109/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@10d476bee71c71ad215bb8ab59f219887b5b4a5e12/12/2025 - 17:27:19 Line coverage: 94.4% (85/90) Branch coverage: 81.4% (44/54) Total lines: 263 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd

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 (host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 35        {
 936            host.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            {
 350                Options.Logger.Debug("Handling API Key authentication for request: {Request}", Request);
 51            }
 52
 53            // If insecure HTTP is NOT allowed and the request is not HTTPS, fail.
 1154            if (!Options.AllowInsecureHttp && !Request.IsHttps)
 55            {
 156                return Fail("HTTPS required");
 57            }
 58
 1059            if (!TryGetApiKey(out var providedKey))
 60            {
 161                return Fail("Missing API Key");
 62            }
 63
 964            var providedKeyBytes = Encoding.UTF8.GetBytes(providedKey);
 965            var valid = await ValidateApiKeyAsync(providedKey, providedKeyBytes);
 966            if (!valid)
 67            {
 368                return Fail($"Invalid API Key: {providedKey}");
 69            }
 70
 671            var ticket = await CreateTicketAsync(providedKey);
 672            if (ticket.Principal is null)
 73            {
 074                return Fail("Authentication ticket has no principal");
 75            }
 676            Options.Logger.Information("API Key authentication succeeded for identity: {Identity}", ticket.Principal.Ide
 77
 678            return AuthenticateResult.Success(ticket);
 79        }
 080        catch (Exception ex)
 81        {
 082            Options.Logger.Error(ex, "API Key authentication failed with exception.");
 083            return Fail("Error processing API Key");
 84        }
 1185    }
 86
 87    /// <summary>
 88    /// Tries to retrieve the API key from the request headers or query string.
 89    /// </summary>
 90    /// <param name="providedKey">The retrieved API key.</param>
 91    /// <returns>True if the API key was found; otherwise, false.</returns>
 92    private bool TryGetApiKey(out string providedKey)
 93    {
 1094        providedKey = string.Empty;
 95
 96        // Primary header
 1097        if (!Request.Headers.TryGetValue(Options.ApiKeyName, out var values))
 98        {
 99            // Additional headers
 11100            foreach (var header in Options.AdditionalHeaderNames)
 101            {
 3102                if (Request.Headers.TryGetValue(header, out values))
 103                {
 104                    break;
 105                }
 106            }
 107        }
 108
 109        // Query string fallback
 10110        if ((values.Count == 0 || StringValues.IsNullOrEmpty(values))
 10111            && Options.AllowQueryStringFallback
 10112            && Request.Query.TryGetValue(Options.ApiKeyName, out var qsValues))
 113        {
 2114            values = qsValues;
 115        }
 116
 10117        if (StringValues.IsNullOrEmpty(values))
 118        {
 1119            return false;
 120        }
 121
 122        // Normalize potential whitespace/newlines from transport
 9123        providedKey = values.ToString().Trim();
 9124        return true;
 125    }
 126
 127    /// <summary>
 128    /// Validates the provided API key against the expected key or a custom validation method.
 129    /// </summary>
 130    /// <param name="providedKey">The API key provided by the client.</param>
 131    /// <param name="providedKeyBytes">The byte representation of the provided API key.</param>
 132    /// <returns>True if the API key is valid; otherwise, false.</returns>
 133    private async Task<bool> ValidateApiKeyAsync(string providedKey, byte[] providedKeyBytes)
 134    {
 9135        return Options.StaticApiKeyAsBytes is not null
 9136            ? FixedTimeEquals.Test(providedKeyBytes, Options.StaticApiKeyAsBytes)
 9137            : Options.ValidateKeyAsync is not null
 9138            ? await Options.ValidateKeyAsync(Context, providedKey, providedKeyBytes)
 9139            : throw new InvalidOperationException(
 9140            "No API key validation configured. Either set ValidateKey or ExpectedKey in ApiKeyAuthenticationOptions.");
 9141    }
 142
 143    /// <summary>
 144    /// Creates an authentication ticket for the provided API key.
 145    /// </summary>
 146    /// <param name="providedKey"></param>
 147    /// <returns></returns>
 148    private Task<AuthenticationTicket> CreateTicketAsync(string providedKey)
 6149        => IAuthHandler.GetAuthenticationTicketAsync(Context, providedKey, Options, Scheme, "ApiKeyClient");
 150
 151    /// <summary>
 152    /// Fails the authentication process with the specified reason.
 153    /// </summary>
 154    /// <param name="reason">The reason for the failure.</param>
 155    /// <returns>An <see cref="AuthenticateResult"/> indicating the failure.</returns>
 156    private AuthenticateResult Fail(string reason)
 157    {
 5158        Options.Logger.Warning("API Key authentication failed: {Reason}", reason);
 5159        return AuthenticateResult.Fail(reason);
 160    }
 161
 162    /// <summary>
 163    /// Handles the authentication challenge by setting the appropriate response headers and status code.
 164    /// </summary>
 165    /// <param name="properties">Authentication properties for the challenge.</param>
 166    /// <returns>A completed task.</returns>
 167    protected override Task HandleChallengeAsync(AuthenticationProperties properties)
 168    {
 1169        if (Options.EmitChallengeHeader)
 170        {
 1171            var header = Options.ApiKeyName ?? "X-Api-Key";
 172
 1173            var value = Options.ChallengeHeaderFormat == ApiKeyChallengeFormat.ApiKeyHeader
 1174                ? $"ApiKey header=\"{header}\""
 1175                : header;
 176
 1177            Response.Headers.WWWAuthenticate = value;
 178        }
 1179        Response.StatusCode = StatusCodes.Status401Unauthorized;
 1180        return Task.CompletedTask;
 181    }
 182    /// <summary>
 183    /// Builds a PowerShell-based API key validator delegate using the provided authentication code settings.
 184    /// </summary>
 185    ///<param name="host">The Kestrun host instance.</param>
 186    /// <param name="settings">The settings containing the PowerShell authentication code.</param>
 187    /// <returns>A delegate that validates an API key using PowerShell code.</returns>
 188    /// <remarks>
 189    ///  This method compiles the PowerShell script and returns a delegate that can be used to validate API keys.
 190    /// </remarks>
 191    public static Func<HttpContext, string, byte[], Task<bool>> BuildPsValidator(
 192            KestrunHost host,
 193        AuthenticationCodeSettings settings)
 194    {
 2195        if (host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 196        {
 2197            host.Logger.Debug("BuildPsValidator  settings: {Settings}", settings);
 198        }
 199
 2200        return async (ctx, providedKey, providedKeyBytes) =>
 2201               {
 2202                   return await IAuthHandler.ValidatePowerShellAsync(settings.Code, ctx, new Dictionary<string, string>
 2203                   {
 2204                    { "providedKey", providedKey },
 2205                    { "providedKeyBytes", Convert.ToBase64String(providedKeyBytes) }
 2206                   }, host.Logger);
 4207               };
 208    }
 209
 210    /// <summary>
 211    /// Builds a C#-based API key validator delegate using the provided authentication code settings.
 212    /// </summary>
 213    /// <param name="host">The Kestrun host instance.</param>
 214    /// <param name="settings">The settings containing the C# authentication code.</param>
 215    /// <returns>A delegate that validates an API key using C# code.</returns>
 216    public static Func<HttpContext, string, byte[], Task<bool>> BuildCsValidator(
 217        KestrunHost host,
 218        AuthenticationCodeSettings settings)
 219    {
 1220        if (host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 221        {
 1222            host.Logger.Debug("BuildCsValidator  settings: {Settings}", settings);
 223        }
 224        // pass the settings to the core C# validator
 1225        var core = IAuthHandler.BuildCsValidator(host,
 1226            settings,
 1227            ("providedKey", string.Empty), ("providedKeyBytes", Array.Empty<byte>())
 1228            ) ?? throw new InvalidOperationException("Failed to build C# validator delegate from provided settings.");
 1229        return (ctx, providedKey, providedKeyBytes) =>
 3230            core(ctx, new Dictionary<string, object?>
 3231            {
 3232                ["providedKey"] = providedKey,
 3233                ["providedKeyBytes"] = providedKeyBytes
 3234            });
 235    }
 236
 237    /// <summary>
 238    /// Builds a VB.NET-based API key validator delegate using the provided authentication code settings.
 239    /// </summary>
 240    /// <param name="host">The Kestrun host instance.</param>
 241    /// <param name="settings">The settings containing the VB.NET authentication code.</param>
 242    /// <returns>A delegate that validates an API key using VB.NET code.</returns>
 243    public static Func<HttpContext, string, byte[], Task<bool>> BuildVBNetValidator(
 244         KestrunHost host,
 245        AuthenticationCodeSettings settings)
 246    {
 2247        if (host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 248        {
 1249            host.Logger.Debug("BuildVBNetValidator  settings: {Settings}", settings);
 250        }
 251        // pass the settings to the core VB.NET validator
 2252        var core = IAuthHandler.BuildVBNetValidator(host,
 2253            settings,
 2254            ("providedKey", string.Empty), ("providedKeyBytes", Array.Empty<byte>())
 2255            ) ?? throw new InvalidOperationException("Failed to build VB.NET validator delegate from provided settings."
 2256        return (ctx, providedKey, providedKeyBytes) =>
 4257            core(ctx, new Dictionary<string, object?>
 4258            {
 4259                ["providedKey"] = providedKey,
 4260                ["providedKeyBytes"] = providedKeyBytes
 4261            });
 262    }
 263}