< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Authentication.BasicAuthHandler
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Authentication/BasicAuthHandler.cs
Tag: Kestrun/Kestrun@2d87023b37eb91155071c91dd3d6a2eeb3004705
Line coverage
89%
Covered lines: 107
Uncovered lines: 13
Coverable lines: 120
Total lines: 371
Line coverage: 89.1%
Branch coverage
79%
Covered branches: 49
Total branches: 62
Branch coverage: 79%
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: 86.3% (101/117) Branch coverage: 74.1% (46/62) Total lines: 356 Tag: Kestrun/Kestrun@07f821172e5dc3657f1be7e6818f18d6721cf38a09/06/2025 - 18:30:33 Line coverage: 88.8% (104/117) Branch coverage: 79% (49/62) Total lines: 356 Tag: Kestrun/Kestrun@aeddbedb8a96e9137aac94c2d5edd011b57ac87110/13/2025 - 16:52:37 Line coverage: 89.1% (107/120) Branch coverage: 79% (49/62) Total lines: 371 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e 08/26/2025 - 01:25:22 Line coverage: 86.3% (101/117) Branch coverage: 74.1% (46/62) Total lines: 356 Tag: Kestrun/Kestrun@07f821172e5dc3657f1be7e6818f18d6721cf38a09/06/2025 - 18:30:33 Line coverage: 88.8% (104/117) Branch coverage: 79% (49/62) Total lines: 356 Tag: Kestrun/Kestrun@aeddbedb8a96e9137aac94c2d5edd011b57ac87110/13/2025 - 16:52:37 Line coverage: 89.1% (107/120) Branch coverage: 79% (49/62) Total lines: 371 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Host()100%11100%
.ctor(...)100%22100%
HandleAuthenticateAsync()91.66%131284.21%
PreValidateRequest()100%66100%
TryGetAuthorizationHeader(...)100%2278.57%
TryGetUserPass(...)100%44100%
ValidateSchemeAndParameter(...)70%1010100%
TryDecodeCredentials(...)50%66100%
TryParseCredentials(...)100%66100%
Fail(...)100%11100%
HandleChallengeAsync(...)0%2040%
HandleForbiddenAsync(...)100%210%
BuildPsValidator(...)100%22100%
BuildCsValidator(...)75%44100%
BuildVBNetValidator(...)75%44100%

File(s)

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

#LineLine coverage
 1using System.Net.Http.Headers;
 2using System.Text;
 3using System.Text.Encodings.Web;
 4using Kestrun.Hosting;
 5using Microsoft.AspNetCore.Authentication;
 6using Microsoft.Extensions.Options;
 7
 8namespace Kestrun.Authentication;
 9
 10/// <summary>
 11/// Handles Basic Authentication for HTTP requests.
 12/// </summary>
 13public class BasicAuthHandler : AuthenticationHandler<BasicAuthenticationOptions>, IAuthHandler
 14{
 15    /// <summary>
 16    /// The Kestrun host instance.
 17    /// </summary>
 6518    public KestrunHost Host { get; private set; }
 19
 20    /// <summary>
 21    /// Initializes a new instance of the <see cref="BasicAuthHandler"/> class.
 22    /// </summary>
 23    /// <param name="host">The Kestrun host instance.</param>
 24    /// <param name="options">The options for Basic Authentication.</param>
 25    /// <param name="loggerFactory">The logger factory.</param>
 26    /// <param name="encoder">The URL encoder.</param>
 27    /// <remarks>
 28    /// This constructor is used to set up the Basic Authentication handler with the provided options, logger factory, a
 29    /// </remarks>
 30    public BasicAuthHandler(
 31        KestrunHost host,
 32        IOptionsMonitor<BasicAuthenticationOptions> options,
 33        ILoggerFactory loggerFactory,
 34        UrlEncoder encoder)
 1535        : base(options, loggerFactory, encoder)
 36    {
 1537        ArgumentNullException.ThrowIfNull(host);
 1538        Host = host;
 1539        if (Host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 40        {
 1541            Host.Logger.Debug("BasicAuthHandler initialized");
 42        }
 1543    }
 44
 45    /// <summary>
 46    /// Handles the authentication process for Basic Authentication.
 47    /// </summary>
 48    /// <returns>A task representing the authentication result.</returns>
 49    /// <remarks>
 50    /// This method is called to authenticate a user based on the Basic Authentication scheme.
 51    /// </remarks>
 52    /// <exception cref="FormatException">Thrown if the Authorization header is not properly formatted.</exception>
 53    /// <exception cref="ArgumentNullException">Thrown if the Authorization header is null or empty.</exception>
 54    /// <exception cref="Exception">Thrown for any other unexpected errors during authentication.</exception>
 55    /// <remarks>
 56    /// The method checks for the presence of the Authorization header, decodes it, and validates the credentials.
 57    /// </remarks>
 58    /// <remarks>
 59    /// If the credentials are valid, it creates a ClaimsPrincipal and returns a successful authentication result.
 60    /// If the credentials are invalid, it returns a failure result.
 61    /// </remarks>
 62    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
 63    {
 64        try
 65        {
 66            // Pre-flight validations
 967            if (PreValidateRequest() is { } preFail)
 68            {
 269                return preFail;
 70            }
 71
 72            // Read and parse Authorization header safely
 773            if (!TryGetAuthorizationHeader(out var authHeader, out var failResult))
 74            {
 175                return failResult!;
 76            }
 77
 78            // Scheme/parameter validation
 679            if (ValidateSchemeAndParameter(authHeader) is { } schemeFail)
 80            {
 181                return schemeFail;
 82            }
 83
 584            Host.Logger.Information("Processing Basic Authentication for header: {Context}", Context);
 85
 86            // Extract user/pass
 587            if (!TryGetUserPass(authHeader, out var user, out var pass, out var err))
 88            {
 389                return Fail(err ?? "Malformed credentials");
 90            }
 91
 92            // Validate credentials
 293            if (!await Options.ValidateCredentialsAsync!(Context, user, pass))
 94            {
 195                return Fail("Invalid credentials");
 96            }
 97
 198            Host.Logger.Information("Basic auth succeeded for user: {User}", user);
 99
 1100            var ticket = await IAuthHandler.GetAuthenticationTicketAsync(Context, user, Options, Scheme);
 1101            Host.Logger.Information("Basic auth ticket created for user: {User}", user);
 1102            return AuthenticateResult.Success(ticket);
 103        }
 0104        catch (Exception ex)
 105        {
 106            // Log the exception and return a failure result
 0107            Host.Logger.Error(ex, "Error processing Authentication");
 0108            return Fail("Exception during authentication");
 109        }
 9110    }
 111
 112    /// <summary>
 113    /// Validates preconditions before processing the Authorization header.
 114    /// </summary>
 115    /// <returns>An AuthenticateResult indicating the validation result.</returns>
 116    private AuthenticateResult? PreValidateRequest()
 117    {
 12118        return Options.ValidateCredentialsAsync is null
 12119            ? Fail("No credentials validation function provided")
 12120            : Options.RequireHttps && !Request.IsHttps ? Fail("HTTPS required") : null;
 121    }
 122
 123    /// <summary>
 124    /// Tries to get and parse the Authorization header, returning a Fail result when absent/invalid
 125    /// </summary>
 126    /// <param name="authHeader">The parsed Authorization header.</param>
 127    /// <param name="fail">An AuthenticateResult indicating the failure reason, if any.</param>
 128    /// <returns>True if the header was successfully parsed; otherwise, false.</returns>
 129    private bool TryGetAuthorizationHeader(out AuthenticationHeaderValue authHeader, out AuthenticateResult? fail)
 130    {
 10131        fail = null;
 10132        authHeader = default!;
 10133        if (!Request.Headers.TryGetValue(Options.HeaderName, out var authHeaderVal))
 134        {
 2135            fail = Fail("Missing Authorization Header");
 2136            return false;
 137        }
 138
 139        try
 140        {
 8141            authHeader = AuthenticationHeaderValue.Parse(authHeaderVal.ToString());
 7142            return true;
 143        }
 1144        catch (FormatException)
 145        {
 1146            fail = Fail("Malformed credentials");
 1147            return false;
 148        }
 0149        catch (ArgumentException)
 150        {
 0151            fail = Fail("Malformed credentials");
 0152            return false;
 153        }
 8154    }
 155
 156    /// <summary>
 157    /// Tries to extract and validate user/password from the Authorization header.
 158    /// </summary>
 159    /// <param name="authHeader">The parsed Authorization header.</param>
 160    /// <param name="user">The extracted username.</param>
 161    /// <param name="pass">The extracted password.</param>
 162    /// <param name="error">An error message, if extraction fails.</param>
 163    /// <returns>True if user/password were successfully extracted; otherwise, false.</returns>
 164    private bool TryGetUserPass(AuthenticationHeaderValue authHeader, out string user, out string pass, out string? erro
 165    {
 5166        user = string.Empty;
 5167        pass = string.Empty;
 5168        error = null;
 169
 5170        var (Success, Value, Error) = TryDecodeCredentials(authHeader.Parameter!, Options.Base64Encoded);
 5171        if (!Success)
 172        {
 1173            error = Error;
 1174            return false;
 175        }
 176
 4177        var parsed = TryParseCredentials(Value!);
 4178        if (!parsed.Success)
 179        {
 2180            error = parsed.Error;
 2181            return false;
 182        }
 183
 2184        user = parsed.Username!;
 2185        pass = parsed.Password!;
 2186        return true;
 187    }
 188
 189    /// <summary>
 190    /// Validates the scheme and parameter of the Authorization header.
 191    /// </summary>
 192    /// <param name="authHeader">The parsed Authorization header.</param>
 193    /// <returns>An AuthenticateResult indicating the validation result.</returns>
 194    private AuthenticateResult? ValidateSchemeAndParameter(AuthenticationHeaderValue authHeader)
 195    {
 6196        return Options.Base64Encoded && !string.Equals(authHeader.Scheme, "Basic", StringComparison.OrdinalIgnoreCase)
 6197            ? Fail("Invalid Authorization Scheme")
 6198            : string.IsNullOrEmpty(authHeader.Parameter)
 6199            ? Fail("Missing credentials in Authorization Header")
 6200            : (authHeader.Parameter?.Length ?? 0) > 8 * 1024 ? Fail("Header too large") : null;
 201    }
 202
 203    /// <summary>
 204    /// Tries to decode the credentials from the Authorization header.
 205    /// </summary>
 206    /// <param name="parameter">The encoded credentials.</param>
 207    /// <param name="base64">Indicates if the credentials are Base64 encoded.</param>
 208    /// <returns>A tuple indicating the success status, decoded value, and any error message.</returns>
 209    private (bool Success, string? Value, string? Error) TryDecodeCredentials(string parameter, bool base64)
 210    {
 211        try
 212        {
 5213            var raw = base64
 5214                ? Encoding.UTF8.GetString(Convert.FromBase64String(parameter ?? string.Empty))
 5215                : parameter ?? string.Empty;
 4216            return (true, raw, null);
 217        }
 1218        catch (FormatException)
 219        {
 1220            Host.Logger.Warning("Invalid Base64 in Authorization header");
 1221            return (false, null, "Malformed credentials");
 222        }
 5223    }
 224
 225    /// <summary>
 226    /// Tries to parse the credentials from the raw credentials string.
 227    /// </summary>
 228    /// <param name="rawCreds">The raw credentials string.</param>
 229    /// <returns>A tuple indicating the success status, username, password, and any error message.</returns>
 230    private (bool Success, string? Username, string? Password, string? Error) TryParseCredentials(string rawCreds)
 231    {
 4232        var match = Options.SeparatorRegex.Match(rawCreds);
 4233        if (!match.Success || match.Groups.Count < 3)
 234        {
 1235            return (false, null, null, "Malformed credentials");
 236        }
 237
 3238        var user = match.Groups[1].Value;
 3239        var pass = match.Groups[2].Value;
 3240        return string.IsNullOrEmpty(user) ? ((bool Success, string? Username, string? Password, string? Error))(false, n
 241    }
 242
 243    private AuthenticateResult Fail(string reason)
 244    {
 12245        Host.Logger.Warning("Basic auth failed: {Reason}", reason);
 12246        return AuthenticateResult.Fail(reason);
 247    }
 248
 249    /// <summary>
 250    /// Handles the challenge response for Basic Authentication.
 251    /// </summary>
 252    /// <param name="properties">The authentication properties.</param>
 253    /// <remarks>
 254    /// This method is called to challenge the client for credentials if authentication fails.
 255    /// If the request is not secure, it does not challenge with WWW-Authenticate.
 256    /// If the SuppressWwwAuthenticate option is set, it does not add the WWW-Authenticate header.
 257    /// If the Realm is set, it includes it in the WWW-Authenticate header.
 258    /// If the request is secure, it adds the WWW-Authenticate header with the Basic scheme.
 259    /// The response status code is set to 401 Unauthorized.
 260    /// </remarks>
 261    /// <returns>A task representing the asynchronous operation.</returns>
 262    /// <exception cref="InvalidOperationException">Thrown if the Realm is not set and SuppressWwwAuthenticate is false.
 263    protected override Task HandleChallengeAsync(AuthenticationProperties properties)
 264    {
 0265        if (!Options.SuppressWwwAuthenticate)
 266        {
 0267            var realm = Options.Realm ?? "Kestrun";
 0268            Response.Headers.WWWAuthenticate = $"Basic realm=\"{realm}\", charset=\"UTF-8\"";
 269        }
 270        // If the request is not secure, we don't challenge with WWW-Authenticate
 0271        Response.StatusCode = StatusCodes.Status401Unauthorized;
 272
 0273        return Task.CompletedTask;
 274    }
 275
 276    /// <summary>
 277    /// Handles the forbidden response for Basic Authentication.
 278    /// </summary>
 279    /// <param name="properties">The authentication properties.</param>
 280    /// <remarks>
 281    /// This method is called to handle forbidden responses for Basic Authentication.
 282    /// </remarks>
 283    protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
 284    {
 0285        Response.StatusCode = StatusCodes.Status403Forbidden;
 0286        return Task.CompletedTask;
 287    }
 288
 289    /// <summary>
 290    /// Builds a PowerShell-based validator function for authenticating users.
 291    /// </summary>
 292    /// <param name="host">The Kestrun host instance.</param>
 293    /// <param name="settings">The authentication code settings containing the PowerShell script.</param>
 294    /// <returns>A function that validates credentials using the provided PowerShell script.</returns>
 295    /// <remarks>
 296    /// This method compiles the PowerShell script and returns a delegate that can be used to validate user credentials.
 297    /// </remarks>
 298    public static Func<HttpContext, string, string, Task<bool>> BuildPsValidator(
 299        KestrunHost host,
 300        AuthenticationCodeSettings settings)
 301    {
 1302        if (host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 303        {
 1304            host.Logger.Debug("BuildPsValidator  settings: {Settings}", settings);
 305        }
 306
 1307        return async (ctx, user, pass) =>
 1308        {
 2309            return await IAuthHandler.ValidatePowerShellAsync(settings.Code, ctx, new Dictionary<string, string>
 2310            {
 2311                { "username", user },
 2312                { "password", pass }
 2313            }, host.Logger);
 3314        };
 315    }
 316    /// <summary>
 317    /// Builds a C#-based validator function for authenticating users.
 318    /// </summary>
 319    /// <param name="host">The Kestrun host instance.</param>
 320    /// <param name="settings">The authentication code settings containing the C# script.</param>
 321    /// <returns>A function that validates credentials using the provided C# script.</returns>
 322    public static Func<HttpContext, string, string, Task<bool>> BuildCsValidator(
 323        KestrunHost host,
 324        AuthenticationCodeSettings settings)
 325    {
 1326        if (host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 327        {
 1328            host.Logger.Debug("BuildCsValidator  settings: {Settings}", settings);
 329        }
 330
 331        // pass the settings to the core C# validator
 1332        var core = IAuthHandler.BuildCsValidator(
 1333            host,
 1334            settings,
 1335            ("username", string.Empty), ("password", string.Empty)
 1336            ) ?? throw new InvalidOperationException("Failed to build C# validator delegate from provided settings.");
 1337        return (ctx, username, password) =>
 3338            core(ctx, new Dictionary<string, object?>
 3339            {
 3340                ["username"] = username,
 3341                ["password"] = password
 3342            });
 343    }
 344
 345    /// <summary>
 346    /// Builds a VB.NET-based validator function for authenticating users.
 347    /// </summary>
 348    /// <param name="host">The Kestrun host instance.</param>
 349    /// <param name="settings">The authentication code settings containing the VB.NET script.</param>
 350    /// <returns>A function that validates credentials using the provided VB.NET script.</returns>
 351    public static Func<HttpContext, string, string, Task<bool>> BuildVBNetValidator(
 352        KestrunHost host,
 353        AuthenticationCodeSettings settings)
 354    {
 1355        if (host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 356        {
 1357            host.Logger.Debug("BuildCsValidator  settings: {Settings}", settings);
 358        }
 359        // pass the settings to the core VB.NET validator
 1360        var core = IAuthHandler.BuildVBNetValidator(
 1361            host,
 1362            settings,
 1363            ("username", string.Empty), ("password", string.Empty)) ?? throw new InvalidOperationException("Failed to bu
 1364        return (ctx, username, password) =>
 3365            core(ctx, new Dictionary<string, object?>
 3366            {
 3367                ["username"] = username,
 3368                ["password"] = password
 3369            });
 370    }
 371}