< 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@9d3a582b2d63930269564a7591aa77ef297cadeb
Line coverage
88%
Covered lines: 104
Uncovered lines: 13
Coverable lines: 117
Total lines: 356
Line coverage: 88.8%
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

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.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 Microsoft.AspNetCore.Authentication;
 5using Microsoft.Extensions.Options;
 6using Serilog;
 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    /// Initializes a new instance of the <see cref="BasicAuthHandler"/> class.
 17    /// </summary>
 18    /// <param name="options">The options for Basic Authentication.</param>
 19    /// <param name="loggerFactory">The logger factory.</param>
 20    /// <param name="encoder">The URL encoder.</param>
 21    /// <remarks>
 22    /// This constructor is used to set up the Basic Authentication handler with the provided options, logger factory, a
 23    /// </remarks>
 24    public BasicAuthHandler(
 25        IOptionsMonitor<BasicAuthenticationOptions> options,
 26        ILoggerFactory loggerFactory,
 27        UrlEncoder encoder)
 1528        : base(options, loggerFactory, encoder)
 29    {
 1530        if (options.CurrentValue.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 31        {
 1532            options.CurrentValue.Logger.Debug("BasicAuthHandler initialized");
 33        }
 1534    }
 35
 36    /// <summary>
 37    /// Handles the authentication process for Basic Authentication.
 38    /// </summary>
 39    /// <returns>A task representing the authentication result.</returns>
 40    /// <remarks>
 41    /// This method is called to authenticate a user based on the Basic Authentication scheme.
 42    /// </remarks>
 43    /// <exception cref="FormatException">Thrown if the Authorization header is not properly formatted.</exception>
 44    /// <exception cref="ArgumentNullException">Thrown if the Authorization header is null or empty.</exception>
 45    /// <exception cref="Exception">Thrown for any other unexpected errors during authentication.</exception>
 46    /// <remarks>
 47    /// The method checks for the presence of the Authorization header, decodes it, and validates the credentials.
 48    /// </remarks>
 49    /// <remarks>
 50    /// If the credentials are valid, it creates a ClaimsPrincipal and returns a successful authentication result.
 51    /// If the credentials are invalid, it returns a failure result.
 52    /// </remarks>
 53    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
 54    {
 55        try
 56        {
 57            // Pre-flight validations
 958            if (PreValidateRequest() is { } preFail)
 59            {
 260                return preFail;
 61            }
 62
 63            // Read and parse Authorization header safely
 764            if (!TryGetAuthorizationHeader(out var authHeader, out var failResult))
 65            {
 166                return failResult!;
 67            }
 68
 69            // Scheme/parameter validation
 670            if (ValidateSchemeAndParameter(authHeader) is { } schemeFail)
 71            {
 172                return schemeFail;
 73            }
 74
 575            Log.Information("Processing Basic Authentication for header: {Context}", Context);
 76
 77            // Extract user/pass
 578            if (!TryGetUserPass(authHeader, out var user, out var pass, out var err))
 79            {
 380                return Fail(err ?? "Malformed credentials");
 81            }
 82
 83            // Validate credentials
 284            if (!await Options.ValidateCredentialsAsync!(Context, user, pass))
 85            {
 186                return Fail("Invalid credentials");
 87            }
 88
 189            Options.Logger.Information("Basic auth succeeded for user: {User}", user);
 90
 191            var ticket = await IAuthHandler.GetAuthenticationTicketAsync(Context, user, Options, Scheme);
 192            Options.Logger.Information("Basic auth ticket created for user: {User}", user);
 193            return AuthenticateResult.Success(ticket);
 94        }
 095        catch (Exception ex)
 96        {
 97            // Log the exception and return a failure result
 098            Options.Logger.Error(ex, "Error processing Authentication");
 099            return Fail("Exception during authentication");
 100        }
 9101    }
 102
 103    /// <summary>
 104    /// Validates preconditions before processing the Authorization header.
 105    /// </summary>
 106    /// <returns>An AuthenticateResult indicating the validation result.</returns>
 107    private AuthenticateResult? PreValidateRequest()
 108    {
 12109        return Options.ValidateCredentialsAsync is null
 12110            ? Fail("No credentials validation function provided")
 12111            : Options.RequireHttps && !Request.IsHttps ? Fail("HTTPS required") : null;
 112    }
 113
 114    /// <summary>
 115    /// Tries to get and parse the Authorization header, returning a Fail result when absent/invalid
 116    /// </summary>
 117    /// <param name="authHeader">The parsed Authorization header.</param>
 118    /// <param name="fail">An AuthenticateResult indicating the failure reason, if any.</param>
 119    /// <returns>True if the header was successfully parsed; otherwise, false.</returns>
 120    private bool TryGetAuthorizationHeader(out AuthenticationHeaderValue authHeader, out AuthenticateResult? fail)
 121    {
 10122        fail = null;
 10123        authHeader = default!;
 10124        if (!Request.Headers.TryGetValue(Options.HeaderName, out var authHeaderVal))
 125        {
 2126            fail = Fail("Missing Authorization Header");
 2127            return false;
 128        }
 129
 130        try
 131        {
 8132            authHeader = AuthenticationHeaderValue.Parse(authHeaderVal.ToString());
 7133            return true;
 134        }
 1135        catch (FormatException)
 136        {
 1137            fail = Fail("Malformed credentials");
 1138            return false;
 139        }
 0140        catch (ArgumentException)
 141        {
 0142            fail = Fail("Malformed credentials");
 0143            return false;
 144        }
 8145    }
 146
 147    /// <summary>
 148    /// Tries to extract and validate user/password from the Authorization header.
 149    /// </summary>
 150    /// <param name="authHeader">The parsed Authorization header.</param>
 151    /// <param name="user">The extracted username.</param>
 152    /// <param name="pass">The extracted password.</param>
 153    /// <param name="error">An error message, if extraction fails.</param>
 154    /// <returns>True if user/password were successfully extracted; otherwise, false.</returns>
 155    private bool TryGetUserPass(AuthenticationHeaderValue authHeader, out string user, out string pass, out string? erro
 156    {
 5157        user = string.Empty;
 5158        pass = string.Empty;
 5159        error = null;
 160
 5161        var (Success, Value, Error) = TryDecodeCredentials(authHeader.Parameter!, Options.Base64Encoded);
 5162        if (!Success)
 163        {
 1164            error = Error;
 1165            return false;
 166        }
 167
 4168        var parsed = TryParseCredentials(Value!);
 4169        if (!parsed.Success)
 170        {
 2171            error = parsed.Error;
 2172            return false;
 173        }
 174
 2175        user = parsed.Username!;
 2176        pass = parsed.Password!;
 2177        return true;
 178    }
 179
 180    /// <summary>
 181    /// Validates the scheme and parameter of the Authorization header.
 182    /// </summary>
 183    /// <param name="authHeader">The parsed Authorization header.</param>
 184    /// <returns>An AuthenticateResult indicating the validation result.</returns>
 185    private AuthenticateResult? ValidateSchemeAndParameter(AuthenticationHeaderValue authHeader)
 186    {
 6187        return Options.Base64Encoded && !string.Equals(authHeader.Scheme, "Basic", StringComparison.OrdinalIgnoreCase)
 6188            ? Fail("Invalid Authorization Scheme")
 6189            : string.IsNullOrEmpty(authHeader.Parameter)
 6190            ? Fail("Missing credentials in Authorization Header")
 6191            : (authHeader.Parameter?.Length ?? 0) > 8 * 1024 ? Fail("Header too large") : null;
 192    }
 193
 194    /// <summary>
 195    /// Tries to decode the credentials from the Authorization header.
 196    /// </summary>
 197    /// <param name="parameter">The encoded credentials.</param>
 198    /// <param name="base64">Indicates if the credentials are Base64 encoded.</param>
 199    /// <returns>A tuple indicating the success status, decoded value, and any error message.</returns>
 200    private (bool Success, string? Value, string? Error) TryDecodeCredentials(string parameter, bool base64)
 201    {
 202        try
 203        {
 5204            var raw = base64
 5205                ? Encoding.UTF8.GetString(Convert.FromBase64String(parameter ?? string.Empty))
 5206                : parameter ?? string.Empty;
 4207            return (true, raw, null);
 208        }
 1209        catch (FormatException)
 210        {
 1211            Options.Logger.Warning("Invalid Base64 in Authorization header");
 1212            return (false, null, "Malformed credentials");
 213        }
 5214    }
 215
 216    /// <summary>
 217    /// Tries to parse the credentials from the raw credentials string.
 218    /// </summary>
 219    /// <param name="rawCreds">The raw credentials string.</param>
 220    /// <returns>A tuple indicating the success status, username, password, and any error message.</returns>
 221    private (bool Success, string? Username, string? Password, string? Error) TryParseCredentials(string rawCreds)
 222    {
 4223        var match = Options.SeparatorRegex.Match(rawCreds);
 4224        if (!match.Success || match.Groups.Count < 3)
 225        {
 1226            return (false, null, null, "Malformed credentials");
 227        }
 228
 3229        var user = match.Groups[1].Value;
 3230        var pass = match.Groups[2].Value;
 3231        return string.IsNullOrEmpty(user) ? ((bool Success, string? Username, string? Password, string? Error))(false, n
 232    }
 233
 234    private AuthenticateResult Fail(string reason)
 235    {
 12236        Options.Logger.Warning("Basic auth failed: {Reason}", reason);
 12237        return AuthenticateResult.Fail(reason);
 238    }
 239
 240    /// <summary>
 241    /// Handles the challenge response for Basic Authentication.
 242    /// </summary>
 243    /// <param name="properties">The authentication properties.</param>
 244    /// <remarks>
 245    /// This method is called to challenge the client for credentials if authentication fails.
 246    /// If the request is not secure, it does not challenge with WWW-Authenticate.
 247    /// If the SuppressWwwAuthenticate option is set, it does not add the WWW-Authenticate header.
 248    /// If the Realm is set, it includes it in the WWW-Authenticate header.
 249    /// If the request is secure, it adds the WWW-Authenticate header with the Basic scheme.
 250    /// The response status code is set to 401 Unauthorized.
 251    /// </remarks>
 252    /// <returns>A task representing the asynchronous operation.</returns>
 253    /// <exception cref="InvalidOperationException">Thrown if the Realm is not set and SuppressWwwAuthenticate is false.
 254    protected override Task HandleChallengeAsync(AuthenticationProperties properties)
 255    {
 0256        if (!Options.SuppressWwwAuthenticate)
 257        {
 0258            var realm = Options.Realm ?? "Kestrun";
 0259            Response.Headers.WWWAuthenticate = $"Basic realm=\"{realm}\", charset=\"UTF-8\"";
 260        }
 261        // If the request is not secure, we don't challenge with WWW-Authenticate
 0262        Response.StatusCode = StatusCodes.Status401Unauthorized;
 263
 0264        return Task.CompletedTask;
 265    }
 266
 267    /// <summary>
 268    /// Handles the forbidden response for Basic Authentication.
 269    /// </summary>
 270    /// <param name="properties">The authentication properties.</param>
 271    /// <remarks>
 272    /// This method is called to handle forbidden responses for Basic Authentication.
 273    /// </remarks>
 274    protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
 275    {
 0276        Response.StatusCode = StatusCodes.Status403Forbidden;
 0277        return Task.CompletedTask;
 278    }
 279
 280    /// <summary>
 281    /// Builds a PowerShell-based validator function for authenticating users.
 282    /// </summary>
 283    /// <param name="settings">The authentication code settings containing the PowerShell script.</param>
 284    /// <param name="logger">The logger instance.</param>
 285    /// <returns>A function that validates credentials using the provided PowerShell script.</returns>
 286    /// <remarks>
 287    /// This method compiles the PowerShell script and returns a delegate that can be used to validate user credentials.
 288    /// </remarks>
 289    public static Func<HttpContext, string, string, Task<bool>> BuildPsValidator(AuthenticationCodeSettings settings, Se
 290    {
 1291        if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 292        {
 1293            logger.Debug("BuildPsValidator  settings: {Settings}", settings);
 294        }
 295
 1296        return async (ctx, user, pass) =>
 1297        {
 2298            return await IAuthHandler.ValidatePowerShellAsync(settings.Code, ctx, new Dictionary<string, string>
 2299            {
 2300                { "username", user },
 2301                { "password", pass }
 2302            }, logger);
 3303        };
 304    }
 305    /// <summary>
 306    /// Builds a C#-based validator function for authenticating users.
 307    /// </summary>
 308    /// <param name="settings">The authentication code settings containing the C# script.</param>
 309    /// <param name="logger">The logger instance.</param>
 310    /// <returns>A function that validates credentials using the provided C# script.</returns>
 311    public static Func<HttpContext, string, string, Task<bool>> BuildCsValidator(AuthenticationCodeSettings settings, Se
 312    {
 1313        if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 314        {
 1315            logger.Debug("BuildCsValidator  settings: {Settings}", settings);
 316        }
 317
 318        // pass the settings to the core C# validator
 1319        var core = IAuthHandler.BuildCsValidator(
 1320            settings,
 1321            logger.ForContext<BasicAuthHandler>(),
 1322            ("username", string.Empty), ("password", string.Empty)
 1323            ) ?? throw new InvalidOperationException("Failed to build C# validator delegate from provided settings.");
 1324        return (ctx, username, password) =>
 3325            core(ctx, new Dictionary<string, object?>
 3326            {
 3327                ["username"] = username,
 3328                ["password"] = password
 3329            });
 330    }
 331
 332    /// <summary>
 333    /// Builds a VB.NET-based validator function for authenticating users.
 334    /// </summary>
 335    /// <param name="settings">The authentication code settings containing the VB.NET script.</param>
 336    /// <param name="logger">The logger instance.</param>
 337    /// <returns>A function that validates credentials using the provided VB.NET script.</returns>
 338    public static Func<HttpContext, string, string, Task<bool>> BuildVBNetValidator(AuthenticationCodeSettings settings,
 339    {
 1340        if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 341        {
 1342            logger.Debug("BuildCsValidator  settings: {Settings}", settings);
 343        }
 344        // pass the settings to the core VB.NET validator
 1345        var core = IAuthHandler.BuildVBNetValidator(
 1346            settings,
 1347            logger.ForContext<BasicAuthHandler>(),
 1348            ("username", string.Empty), ("password", string.Empty)) ?? throw new InvalidOperationException("Failed to bu
 1349        return (ctx, username, password) =>
 3350            core(ctx, new Dictionary<string, object?>
 3351            {
 3352                ["username"] = username,
 3353                ["password"] = password
 3354            });
 355    }
 356}