< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Authentication.ClientCertificateAuthHandler
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Authentication/ClientCertificateAuthHandler.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
65%
Covered lines: 78
Uncovered lines: 42
Coverable lines: 120
Total lines: 315
Line coverage: 65%
Branch coverage
58%
Covered branches: 40
Total branches: 68
Branch coverage: 58.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 01/21/2026 - 17:07:46 Line coverage: 65% (78/120) Branch coverage: 58.8% (40/68) Total lines: 315 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a36 01/21/2026 - 17:07:46 Line coverage: 65% (78/120) Branch coverage: 58.8% (40/68) Total lines: 315 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a36

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Host()100%11100%
.ctor(...)100%22100%
HandleAuthenticateAsync()50%201469.23%
HandleChallengeAsync(...)100%210%
HandleForbiddenAsync(...)100%210%
.ctor(...)100%11100%
ValidateAsync(...)100%66100%
ValidateAllowedCertificateTypes(...)80%101091.66%
ValidateValidityPeriod(...)83.33%66100%
ValidateCertificateUse(...)75%44100%
ValidateRevocation(...)33.33%1661823.07%
HasEnhancedKeyUsageOid(...)37.5%31828.57%

File(s)

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

#LineLine coverage
 1using System.Security.Cryptography.X509Certificates;
 2using System.Text.Encodings.Web;
 3using Kestrun.Hosting;
 4using Microsoft.AspNetCore.Authentication;
 5using Microsoft.AspNetCore.Authentication.Certificate;
 6using Microsoft.Extensions.Options;
 7using System.Security.Claims;
 8
 9namespace Kestrun.Authentication;
 10
 11/// <summary>
 12/// Handles Client Certificate Authentication for HTTP requests.
 13/// </summary>
 14public class ClientCertificateAuthHandler : AuthenticationHandler<ClientCertificateAuthenticationOptions>, IAuthHandler
 15{
 16    /// <summary>
 17    /// The Kestrun host instance.
 18    /// </summary>
 3219    public KestrunHost Host { get; private set; }
 20
 21    /// <summary>
 22    /// Initializes a new instance of the <see cref="ClientCertificateAuthHandler"/> class.
 23    /// </summary>
 24    /// <param name="host">The Kestrun host instance.</param>
 25    /// <param name="options">The options for Client Certificate Authentication.</param>
 26    /// <param name="loggerFactory">The logger factory.</param>
 27    /// <param name="encoder">The URL encoder.</param>
 28    public ClientCertificateAuthHandler(
 29        KestrunHost host,
 30        IOptionsMonitor<ClientCertificateAuthenticationOptions> options,
 31        ILoggerFactory loggerFactory,
 32        UrlEncoder encoder)
 833        : base(options, loggerFactory, encoder)
 34    {
 835        ArgumentNullException.ThrowIfNull(host);
 836        Host = host;
 37
 838        if (Host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 39        {
 840            Host.Logger.Debug("ClientCertificateAuthHandler initialized");
 41        }
 842    }
 43
 44    /// <summary>
 45    /// Handles the authentication process for Client Certificate Authentication.
 46    /// </summary>
 47    /// <returns>A task representing the authentication result.</returns>
 48    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
 49    {
 50        try
 51        {
 52            // Get the client certificate
 853            var clientCertificate = await Context.Connection.GetClientCertificateAsync();
 54
 855            if (clientCertificate == null)
 56            {
 157                Host.Logger.Warning("No client certificate provided");
 158                return AuthenticateResult.NoResult();
 59            }
 60
 61            // Validate the certificate using built-in validation
 762            var certificateValidator = new CertificateValidator(Options);
 763            var (Success, FailureMessage) = await certificateValidator.ValidateAsync(clientCertificate);
 64
 765            if (!Success)
 66            {
 467                Host.Logger.Warning("Certificate validation failed: {Reason}", FailureMessage);
 468                return AuthenticateResult.Fail(FailureMessage ?? "Certificate validation failed");
 69            }
 70
 71            // Build the claims identity
 372            var claims = new List<Claim>
 373            {
 374                new(ClaimTypes.NameIdentifier, clientCertificate.Subject, ClaimValueTypes.String, Options.ClaimsIssuer),
 375                new(ClaimTypes.Name, clientCertificate.Subject, ClaimValueTypes.String, Options.ClaimsIssuer),
 376                // Add thumbprint as a claim
 377                new("thumbprint", clientCertificate.Thumbprint, ClaimValueTypes.String, Options.ClaimsIssuer),
 378
 379                // Add issuer and serial number
 380                new("issuer", clientCertificate.Issuer, ClaimValueTypes.String, Options.ClaimsIssuer),
 381                new("serialnumber", clientCertificate.SerialNumber ?? string.Empty, ClaimValueTypes.String, Options.Clai
 382            };
 83
 84            // Create identity and principal
 385            var identity = new ClaimsIdentity(claims, Scheme.Name);
 386            var principal = new ClaimsPrincipal(identity);
 87
 88            // Invoke the OnAuthenticationSucceeded event if configured
 389            if (Options.Events is CertificateAuthenticationEvents certEvents)
 90            {
 091                var certValidatedContext = new CertificateValidatedContext(Context, Scheme, Options)
 092                {
 093                    Principal = principal
 094                };
 95
 096                await certEvents.CertificateValidated(certValidatedContext);
 97
 098                if (certValidatedContext.Result != null)
 99                {
 0100                    return certValidatedContext.Result;
 101                }
 102
 0103                principal = certValidatedContext.Principal ?? principal;
 0104            }
 105
 3106            Host.Logger.Information("Client certificate authentication succeeded for subject: {Subject}", clientCertific
 107
 3108            var ticket = new AuthenticationTicket(principal, Scheme.Name);
 3109            return AuthenticateResult.Success(ticket);
 110        }
 0111        catch (Exception ex)
 112        {
 0113            Host.Logger.Error(ex, "Error processing Client Certificate Authentication");
 0114            return AuthenticateResult.Fail("Exception during authentication");
 115        }
 8116    }
 117
 118    /// <summary>
 119    /// Handles the challenge response for Client Certificate Authentication.
 120    /// </summary>
 121    /// <param name="properties">The authentication properties.</param>
 122    protected override Task HandleChallengeAsync(AuthenticationProperties properties)
 123    {
 0124        Response.StatusCode = StatusCodes.Status401Unauthorized;
 0125        return Task.CompletedTask;
 126    }
 127
 128    /// <summary>
 129    /// Handles the forbidden response for Client Certificate Authentication.
 130    /// </summary>
 131    /// <param name="properties">The authentication properties.</param>
 132    protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
 133    {
 0134        Response.StatusCode = StatusCodes.Status403Forbidden;
 0135        return Task.CompletedTask;
 136    }
 137
 138    /// <summary>
 139    /// Helper class to validate X509 certificates.
 140    /// </summary>
 141    /// <param name="options">The client certificate authentication options.</param>
 7142    private class CertificateValidator(ClientCertificateAuthenticationOptions options)
 143    {
 7144        private readonly ClientCertificateAuthenticationOptions _options = options;
 145
 146        private const string ClientAuthenticationOid = "1.3.6.1.5.5.7.3.2";
 147
 148        /// <summary>
 149        /// Validates the specified certificate according to the configured options.
 150        /// </summary>
 151        /// <param name="certificate">The X509 certificate to validate.</param>
 152        /// <returns>A task that represents the asynchronous validation operation. The task result contains a tuple indi
 153        public Task<(bool Success, string? FailureMessage)> ValidateAsync(X509Certificate2 certificate)
 154        {
 7155            ArgumentNullException.ThrowIfNull(certificate);
 156
 7157            var result = ValidateAllowedCertificateTypes(certificate);
 7158            if (!result.Success)
 159            {
 1160                return Task.FromResult(result);
 161            }
 162
 6163            result = ValidateValidityPeriod(certificate);
 6164            if (!result.Success)
 165            {
 2166                return Task.FromResult(result);
 167            }
 168
 4169            result = ValidateCertificateUse(certificate);
 4170            if (!result.Success)
 171            {
 1172                return Task.FromResult(result);
 173            }
 174
 3175            result = ValidateRevocation(certificate);
 3176            return Task.FromResult(result);
 177        }
 178
 179        /// <summary>
 180        /// Validates that the certificate type (self-signed vs chained) is allowed by the configured options.
 181        /// </summary>
 182        /// <param name="certificate">The certificate to validate.</param>
 183        /// <returns>Success when allowed; otherwise a failure with a message.</returns>
 184        private (bool Success, string? FailureMessage) ValidateAllowedCertificateTypes(X509Certificate2 certificate)
 185        {
 7186            if (_options.AllowedCertificateTypes == CertificateTypes.All)
 187            {
 5188                return (true, null);
 189            }
 190
 2191            var isSelfSigned = string.Equals(certificate.Subject, certificate.Issuer, StringComparison.Ordinal);
 2192            var isAllowed = _options.AllowedCertificateTypes switch
 2193            {
 1194                CertificateTypes.Chained => !isSelfSigned,
 1195                CertificateTypes.SelfSigned => isSelfSigned,
 0196                _ => true
 2197            };
 198
 2199            return isAllowed
 2200                ? (true, null)
 2201                : (false, $"Certificate type not allowed: {(isSelfSigned ? "SelfSigned" : "Chained")}");
 202        }
 203
 204        /// <summary>
 205        /// Validates that the certificate is within its validity period when enabled.
 206        /// </summary>
 207        /// <param name="certificate">The certificate to validate.</param>
 208        /// <returns>Success when valid or validation is disabled; otherwise a failure with a message.</returns>
 209        private (bool Success, string? FailureMessage) ValidateValidityPeriod(X509Certificate2 certificate)
 210        {
 6211            if (!_options.ValidateValidityPeriod)
 212            {
 4213                return (true, null);
 214            }
 215
 2216            var now = DateTime.UtcNow;
 2217            return certificate.NotBefore > now || certificate.NotAfter < now
 2218                ? (false, "Certificate is not within its validity period")
 2219                : (true, null);
 220        }
 221
 222        /// <summary>
 223        /// Validates that the certificate has client authentication usage when enabled.
 224        /// </summary>
 225        /// <param name="certificate">The certificate to validate.</param>
 226        /// <returns>Success when usage is present or validation is disabled; otherwise a failure with a message.</retur
 227        private (bool Success, string? FailureMessage) ValidateCertificateUse(X509Certificate2 certificate)
 228        {
 4229            if (!_options.ValidateCertificateUse)
 230            {
 3231                return (true, null);
 232            }
 233
 1234            return HasEnhancedKeyUsageOid(certificate, ClientAuthenticationOid)
 1235                ? (true, null)
 1236                : (false, "Certificate does not have Client Authentication usage");
 237        }
 238
 239        /// <summary>
 240        /// Validates the certificate chain and revocation status when enabled by the configured options.
 241        /// </summary>
 242        /// <param name="certificate">The certificate to validate.</param>
 243        /// <returns>Success when valid or revocation checking is disabled; otherwise a failure with a message.</returns
 244        private (bool Success, string? FailureMessage) ValidateRevocation(X509Certificate2 certificate)
 245        {
 3246            var isSelfSigned = string.Equals(certificate.Subject, certificate.Issuer, StringComparison.Ordinal);
 3247            var hasCustomTrustStore = _options.CustomTrustStore is { Count: > 0 };
 248
 249            // If self-signed certificates are explicitly allowed and the caller did not supply a trust store,
 250            // treat the certificate as valid at the authentication layer.
 251            // (Chain building for self-signed end-entity certs is platform-dependent and commonly fails.)
 3252            if (isSelfSigned && !hasCustomTrustStore &&
 3253                (_options.AllowedCertificateTypes == CertificateTypes.SelfSigned ||
 3254                 _options.AllowedCertificateTypes == CertificateTypes.All))
 255            {
 3256                return (true, null);
 257            }
 258
 0259            using var chain = new X509Chain
 0260            {
 0261                ChainPolicy =
 0262                {
 0263                    RevocationMode = _options.RevocationMode,
 0264                    RevocationFlag = _options.RevocationFlag
 0265                }
 0266            };
 267
 0268            if (hasCustomTrustStore)
 269            {
 0270                chain.ChainPolicy.CustomTrustStore.AddRange(_options.CustomTrustStore);
 0271                chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
 272            }
 273
 0274            if (chain.Build(certificate))
 275            {
 0276                return (true, null);
 277            }
 278
 0279            var errors = string.Join(", ", chain.ChainStatus.Select(s =>
 0280            {
 0281                var info = s.StatusInformation?.Trim();
 0282                return string.IsNullOrEmpty(info) ? s.Status.ToString() : info;
 0283            }));
 284
 0285            return (false, $"Certificate chain validation failed: {errors}");
 0286        }
 287
 288        /// <summary>
 289        /// Checks whether the certificate contains the specified Enhanced Key Usage (EKU) OID.
 290        /// </summary>
 291        /// <param name="certificate">The certificate to inspect.</param>
 292        /// <param name="oidValue">The OID value to match.</param>
 293        /// <returns><c>true</c> when the EKU is present; otherwise <c>false</c>.</returns>
 294        private static bool HasEnhancedKeyUsageOid(X509Certificate2 certificate, string oidValue)
 295        {
 2296            foreach (var extension in certificate.Extensions)
 297            {
 0298                if (extension is not X509EnhancedKeyUsageExtension eku)
 299                {
 300                    continue;
 301                }
 302
 0303                foreach (var oid in eku.EnhancedKeyUsages)
 304                {
 0305                    if (string.Equals(oid.Value, oidValue, StringComparison.Ordinal))
 306                    {
 0307                        return true;
 308                    }
 309                }
 310            }
 311
 1312            return false;
 0313        }
 314    }
 315}