File: CertificateTests.cs
Web Access
Project: src\src\Security\Authentication\test\Microsoft.AspNetCore.Authentication.Test.csproj (Microsoft.AspNetCore.Authentication.Test)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using System.Net;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Xml.Linq;
using Microsoft.AspNetCore.Authentication.Certificate;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
 
namespace Microsoft.AspNetCore.Authentication.Certificate.Test;
 
public class ClientCertificateAuthenticationTests
{
 
    [Fact]
    public async Task VerifySchemeDefaults()
    {
        var services = new ServiceCollection().ConfigureAuthTestServices();
        services.AddAuthentication().AddCertificate();
        var sp = services.BuildServiceProvider();
        var schemeProvider = sp.GetRequiredService<IAuthenticationSchemeProvider>();
        var scheme = await schemeProvider.GetSchemeAsync(CertificateAuthenticationDefaults.AuthenticationScheme);
        Assert.NotNull(scheme);
        Assert.Equal("CertificateAuthenticationHandler", scheme.HandlerType.Name);
        Assert.Null(scheme.DisplayName);
    }
 
    [Fact]
    public void VerifyIsSelfSignedExtensionMethod()
    {
        Assert.True(Certificates.SelfSignedValidWithNoEku.IsSelfSigned());
    }
 
    [Fact]
    public async Task VerifyValidSelfSignedWithClientEkuAuthenticates()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.SelfSigned,
                Events = successfulValidationEvents
            },
            Certificates.SelfSignedValidWithClientEku);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyValidSelfSignedWithNoEkuAuthenticates()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.SelfSigned,
                Events = successfulValidationEvents
            },
            Certificates.SelfSignedValidWithNoEku);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyValidSelfSignedWithClientEkuFailsWhenSelfSignedCertsNotAllowed()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.Chained
            },
            Certificates.SelfSignedValidWithClientEku);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyValidSelfSignedWithNoEkuFailsWhenSelfSignedCertsNotAllowed()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.Chained,
                Events = successfulValidationEvents
            },
            Certificates.SelfSignedValidWithNoEku);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyValidSelfSignedWithServerFailsEvenIfSelfSignedCertsAreAllowed()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.SelfSigned,
                Events = successfulValidationEvents
            },
            Certificates.SelfSignedValidWithServerEku);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyValidSelfSignedWithServerPassesWhenSelfSignedCertsAreAllowedAndPurposeValidationIsOff()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.SelfSigned,
                ValidateCertificateUse = false,
                Events = successfulValidationEvents
            },
            Certificates.SelfSignedValidWithServerEku);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyValidSelfSignedWithServerFailsPurposeValidationIsOffButSelfSignedCertsAreNotAllowed()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.Chained,
                ValidateCertificateUse = false,
                Events = successfulValidationEvents
            },
            Certificates.SelfSignedValidWithServerEku);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }
 
    [ConditionalFact]
    [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/32813", Queues = $"All.Ubuntu;{HelixConstants.AlmaLinuxAmd64}")]
    public async Task VerifyExpiredSelfSignedFails()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.SelfSigned,
                ValidateCertificateUse = false,
                Events = successfulValidationEvents
            },
            Certificates.SelfSignedExpired);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyExpiredSelfSignedPassesIfDateRangeValidationIsDisabled()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.SelfSigned,
                ValidateValidityPeriod = false,
                Events = successfulValidationEvents
            },
            Certificates.SelfSignedExpired);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
 
    [ConditionalFact]
    [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/32813", Queues = $"All.Ubuntu;{HelixConstants.AlmaLinuxAmd64}")]
    public async Task VerifyNotYetValidSelfSignedFails()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.SelfSigned,
                ValidateCertificateUse = false,
                Events = successfulValidationEvents
            },
            Certificates.SelfSignedNotYetValid);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyNotYetValidSelfSignedPassesIfDateRangeValidationIsDisabled()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.SelfSigned,
                ValidateValidityPeriod = false,
                Events = successfulValidationEvents
            },
            Certificates.SelfSignedNotYetValid);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyFailingInTheValidationEventReturnsForbidden()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                ValidateCertificateUse = false,
                Events = failedValidationEvents
            },
            Certificates.SelfSignedValidWithServerEku);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }
 
    [Fact]
    public async Task DoingNothingInTheValidationEventReturnsOK()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.SelfSigned,
                ValidateCertificateUse = false,
                Events = unprocessedValidationEvents
            },
            Certificates.SelfSignedValidWithServerEku);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyNotSendingACertificateEndsUpInForbidden()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                Events = successfulValidationEvents
            });
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyUntrustedClientCertEndsUpInForbidden()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                Events = successfulValidationEvents
            }, Certificates.SignedClient);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyValidationFailureCanBeHandled()
    {
        var failCalled = false;
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                Events = new CertificateAuthenticationEvents()
                {
                    OnAuthenticationFailed = context =>
                    {
                        context.Fail("Validation failed: " + context.Exception);
                        failCalled = true;
                        return Task.CompletedTask;
                    }
                }
            }, Certificates.SignedClient);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
        Assert.True(failCalled);
    }
 
    [Fact]
    public async Task VerifyClientCertWithUntrustedRootAndTrustedChainEndsUpInForbidden()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                Events = successfulValidationEvents,
                CustomTrustStore = new X509Certificate2Collection() { Certificates.SignedSecondaryRoot },
                ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust,
                RevocationMode = X509RevocationMode.NoCheck
            }, Certificates.SignedClient);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }
 
    [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/39669")]
    public async Task VerifyValidClientCertWithTrustedChainAuthenticates()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                Events = successfulValidationEvents,
                CustomTrustStore = new X509Certificate2Collection() { Certificates.SelfSignedPrimaryRoot, Certificates.SignedSecondaryRoot },
                ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust,
                RevocationMode = X509RevocationMode.NoCheck
            }, Certificates.SignedClient);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
 
    [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/39669")]
    public async Task VerifyValidClientCertWithAdditionalCertificatesAuthenticates()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                Events = successfulValidationEvents,
                ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust,
                CustomTrustStore = new X509Certificate2Collection() { Certificates.SelfSignedPrimaryRoot, },
                AdditionalChainCertificates = new X509Certificate2Collection() { Certificates.SignedSecondaryRoot },
                RevocationMode = X509RevocationMode.NoCheck
            }, Certificates.SignedClient);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyValidClientCertFailsWithoutAdditionalCertificatesAuthenticates()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                Events = successfulValidationEvents,
                ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust,
                CustomTrustStore = new X509Certificate2Collection() { Certificates.SelfSignedPrimaryRoot, },
                RevocationMode = X509RevocationMode.NoCheck
            }, Certificates.SignedClient);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyHeaderIsUsedIfCertIsNotPresent()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.SelfSigned,
                Events = successfulValidationEvents
            },
            wireUpHeaderMiddleware: true);
 
        using var server = host.GetTestServer();
        var client = server.CreateClient();
        client.DefaultRequestHeaders.Add("X-Client-Cert", Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData));
        var response = await client.GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyHeaderEncodedCertFailsOnBadEncoding()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                Events = successfulValidationEvents
            },
            wireUpHeaderMiddleware: true);
 
        using var server = host.GetTestServer();
        var client = server.CreateClient();
        client.DefaultRequestHeaders.Add("X-Client-Cert", "OOPS" + Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData));
        var response = await client.GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifySettingTheAzureHeaderOnTheForwarderOptionsWorks()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.SelfSigned,
                Events = successfulValidationEvents
            },
            wireUpHeaderMiddleware: true,
            headerName: "X-ARR-ClientCert");
 
        using var server = host.GetTestServer();
        var client = server.CreateClient();
        client.DefaultRequestHeaders.Add("X-ARR-ClientCert", Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData));
        var response = await client.GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyACustomHeaderFailsIfTheHeaderIsNotPresent()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                Events = successfulValidationEvents
            },
            wireUpHeaderMiddleware: true,
            headerName: "X-ARR-ClientCert");
 
        using var server = host.GetTestServer();
        var client = server.CreateClient();
        client.DefaultRequestHeaders.Add("random-Weird-header", Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData));
        var response = await client.GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }
 
    [Fact]
    public async Task VerifyNoEventWireupWithAValidCertificateCreatesADefaultUser()
    {
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.SelfSigned
            },
            Certificates.SelfSignedValidWithNoEku);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        XElement responseAsXml = null;
        if (response.Content != null &&
            response.Content.Headers.ContentType != null &&
            response.Content.Headers.ContentType.MediaType == "text/xml")
        {
            var responseContent = await response.Content.ReadAsStringAsync();
            responseAsXml = XElement.Parse(responseContent);
        }
 
        Assert.NotNull(responseAsXml);
 
        // There should always be an Issuer and a Thumbprint.
        var actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == "issuer");
        Assert.Single(actual);
        Assert.Equal(Certificates.SelfSignedValidWithNoEku.Issuer, actual.First().Value);
 
        actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Thumbprint);
        Assert.Single(actual);
        Assert.Equal(Certificates.SelfSignedValidWithNoEku.Thumbprint, actual.First().Value);
 
        // Now the optional ones
        if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.SubjectName.Name))
        {
            actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.X500DistinguishedName);
            if (actual.Any())
            {
                Assert.Single(actual);
                Assert.Equal(Certificates.SelfSignedValidWithNoEku.SubjectName.Name, actual.First().Value);
            }
        }
 
        if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.SerialNumber))
        {
            actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.SerialNumber);
            if (actual.Any())
            {
                Assert.Single(actual);
                Assert.Equal(Certificates.SelfSignedValidWithNoEku.SerialNumber, actual.First().Value);
            }
        }
 
        if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.DnsName, false)))
        {
            actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Dns);
            if (actual.Any())
            {
                Assert.Single(actual);
                Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.DnsName, false), actual.First().Value);
            }
        }
 
        if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.EmailName, false)))
        {
            actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Email);
            if (actual.Any())
            {
                Assert.Single(actual);
                Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.EmailName, false), actual.First().Value);
            }
        }
 
        if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.SimpleName, false)))
        {
            actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Name);
            if (actual.Any())
            {
                Assert.Single(actual);
                Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.SimpleName, false), actual.First().Value);
            }
        }
 
        if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UpnName, false)))
        {
            actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Upn);
            if (actual.Any())
            {
                Assert.Single(actual);
                Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UpnName, false), actual.First().Value);
            }
        }
 
        if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UrlName, false)))
        {
            actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Uri);
            if (actual.Any())
            {
                Assert.Single(actual);
                Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UrlName, false), actual.First().Value);
            }
        }
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task VerifyValidationResultCanBeCached(bool cache)
    {
        const string Expected = "John Doe";
        var validationCount = 0;
 
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.SelfSigned,
                Events = new CertificateAuthenticationEvents
                {
                    OnCertificateValidated = context =>
                    {
                        validationCount++;
 
                        // Make sure we get the validated principal
                        Assert.NotNull(context.Principal);
 
                        var claims = new[]
                        {
                                new Claim(ClaimTypes.Name, Expected, ClaimValueTypes.String, context.Options.ClaimsIssuer),
                                new Claim("ValidationCount", validationCount.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.String, context.Options.ClaimsIssuer)
                        };
 
                        context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
                        context.Success();
                        return Task.CompletedTask;
                    }
                }
            },
            Certificates.SelfSignedValidWithNoEku, null, null, false, "", cache);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        XElement responseAsXml = null;
        if (response.Content != null &&
            response.Content.Headers.ContentType != null &&
            response.Content.Headers.ContentType.MediaType == "text/xml")
        {
            var responseContent = await response.Content.ReadAsStringAsync();
            responseAsXml = XElement.Parse(responseContent);
        }
 
        Assert.NotNull(responseAsXml);
        var name = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Name);
        Assert.Single(name);
        Assert.Equal(Expected, name.First().Value);
        var count = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == "ValidationCount");
        Assert.Single(count);
        Assert.Equal("1", count.First().Value);
 
        // Second request should not trigger validation if caching
        response = await server.CreateClient().GetAsync("https://example.com/");
        responseAsXml = null;
        if (response.Content != null &&
            response.Content.Headers.ContentType != null &&
            response.Content.Headers.ContentType.MediaType == "text/xml")
        {
            var responseContent = await response.Content.ReadAsStringAsync();
            responseAsXml = XElement.Parse(responseContent);
        }
 
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        name = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Name);
        Assert.Single(name);
        Assert.Equal(Expected, name.First().Value);
        count = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == "ValidationCount");
        Assert.Single(count);
        var expected = cache ? "1" : "2";
        Assert.Equal(expected, count.First().Value);
    }
 
    [Fact]
    public async Task VerifyValidationEventPrincipalIsPropogated()
    {
        const string Expected = "John Doe";
 
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.SelfSigned,
                Events = new CertificateAuthenticationEvents
                {
                    OnCertificateValidated = context =>
                    {
                        // Make sure we get the validated principal
                        Assert.NotNull(context.Principal);
                        var claims = new[]
                        {
                                new Claim(ClaimTypes.Name, Expected, ClaimValueTypes.String, context.Options.ClaimsIssuer)
                        };
 
                        context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
                        context.Success();
                        return Task.CompletedTask;
                    }
                }
            },
            Certificates.SelfSignedValidWithNoEku);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        XElement responseAsXml = null;
        if (response.Content != null &&
            response.Content.Headers.ContentType != null &&
            response.Content.Headers.ContentType.MediaType == "text/xml")
        {
            var responseContent = await response.Content.ReadAsStringAsync();
            responseAsXml = XElement.Parse(responseContent);
        }
 
        Assert.NotNull(responseAsXml);
        var actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Name);
        Assert.Single(actual);
        Assert.Equal(Expected, actual.First().Value);
        Assert.Single(responseAsXml.Elements("claim"));
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task VerifyValidationResultNeverCachedAfter30Min(bool cache)
    {
        const string Expected = "John Doe";
        var validationCount = 0;
        // The test certs are generated based off UtcNow.
        var timeProvider = new FakeTimeProvider(TimeProvider.System.GetUtcNow());
 
        using var host = await CreateHost(
            new CertificateAuthenticationOptions
            {
                AllowedCertificateTypes = CertificateTypes.SelfSigned,
                Events = new CertificateAuthenticationEvents
                {
                    OnCertificateValidated = context =>
                    {
                        validationCount++;
 
                        // Make sure we get the validated principal
                        Assert.NotNull(context.Principal);
 
                        var claims = new[]
                        {
                            new Claim(ClaimTypes.Name, Expected, ClaimValueTypes.String, context.Options.ClaimsIssuer),
                            new Claim("ValidationCount", validationCount.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.String, context.Options.ClaimsIssuer)
                        };
 
                        context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
                        context.Success();
                        return Task.CompletedTask;
                    }
                }
            },
            Certificates.SelfSignedValidWithNoEku, null, null, false, "", cache, timeProvider);
 
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("https://example.com/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        XElement responseAsXml = null;
        if (response.Content != null &&
            response.Content.Headers.ContentType != null &&
            response.Content.Headers.ContentType.MediaType == "text/xml")
        {
            var responseContent = await response.Content.ReadAsStringAsync();
            responseAsXml = XElement.Parse(responseContent);
        }
 
        Assert.NotNull(responseAsXml);
        var name = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Name);
        Assert.Single(name);
        Assert.Equal(Expected, name.First().Value);
        var count = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == "ValidationCount");
        Assert.Single(count);
        Assert.Equal("1", count.First().Value);
 
        // Second request should not trigger validation if caching
        response = await server.CreateClient().GetAsync("https://example.com/");
        responseAsXml = null;
        if (response.Content != null &&
            response.Content.Headers.ContentType != null &&
            response.Content.Headers.ContentType.MediaType == "text/xml")
        {
            var responseContent = await response.Content.ReadAsStringAsync();
            responseAsXml = XElement.Parse(responseContent);
        }
 
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        name = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Name);
        Assert.Single(name);
        Assert.Equal(Expected, name.First().Value);
        count = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == "ValidationCount");
        Assert.Single(count);
        var expected = cache ? "1" : "2";
        Assert.Equal(expected, count.First().Value);
 
        timeProvider.Advance(TimeSpan.FromMinutes(31));
 
        // Third request should always trigger validation even if caching
        response = await server.CreateClient().GetAsync("https://example.com/");
        responseAsXml = null;
        if (response.Content != null &&
            response.Content.Headers.ContentType != null &&
            response.Content.Headers.ContentType.MediaType == "text/xml")
        {
            var responseContent = await response.Content.ReadAsStringAsync();
            responseAsXml = XElement.Parse(responseContent);
        }
 
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        name = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Name);
        Assert.Single(name);
        Assert.Equal(Expected, name.First().Value);
        count = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == "ValidationCount");
        Assert.Single(count);
 
        var laterExpected = cache ? "2" : "3";
        Assert.Equal(laterExpected, count.First().Value);
    }
 
    private static async Task<IHost> CreateHost(
        CertificateAuthenticationOptions configureOptions,
        X509Certificate2 clientCertificate = null,
        Func<HttpContext, bool> handler = null,
        Uri baseAddress = null,
        bool wireUpHeaderMiddleware = false,
        string headerName = "",
        bool useCache = false,
        TimeProvider timeProvider = null)
    {
        var host = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                    .Configure(app =>
                    {
                        app.Use((context, next) =>
                        {
                            if (clientCertificate != null)
                            {
                                context.Connection.ClientCertificate = clientCertificate;
                            }
                            return next(context);
                        });
 
                        if (wireUpHeaderMiddleware)
                        {
                            app.UseCertificateForwarding();
                        }
 
                        app.UseAuthentication();
 
                        app.Run(async (context) =>
                        {
                            var request = context.Request;
                            var response = context.Response;
 
                            var authenticationResult = await context.AuthenticateAsync();
 
                            if (authenticationResult.Succeeded)
                            {
                                response.StatusCode = (int)HttpStatusCode.OK;
                                response.ContentType = "text/xml";
 
                                await response.WriteAsync("<claims>");
                                foreach (Claim claim in context.User.Claims)
                                {
                                    await response.WriteAsync($"<claim Type=\"{claim.Type}\" Issuer=\"{claim.Issuer}\">{claim.Value}</claim>");
                                }
                                await response.WriteAsync("</claims>");
                            }
                            else
                            {
                                await context.ChallengeAsync();
                            }
                        });
                    })
                .ConfigureServices(services =>
                {
                    AuthenticationBuilder authBuilder;
                    if (configureOptions != null)
                    {
                        authBuilder = services.AddAuthentication().AddCertificate(options =>
                        {
                            options.CustomTrustStore = configureOptions.CustomTrustStore;
                            options.ChainTrustValidationMode = configureOptions.ChainTrustValidationMode;
                            options.AllowedCertificateTypes = configureOptions.AllowedCertificateTypes;
                            options.Events = configureOptions.Events;
                            options.ValidateCertificateUse = configureOptions.ValidateCertificateUse;
                            options.RevocationFlag = configureOptions.RevocationFlag;
                            options.RevocationMode = configureOptions.RevocationMode;
                            options.ValidateValidityPeriod = configureOptions.ValidateValidityPeriod;
                            options.AdditionalChainCertificates = configureOptions.AdditionalChainCertificates;
                            options.TimeProvider = configureOptions.TimeProvider;
 
                            if (timeProvider != null)
                            {
                                options.TimeProvider = timeProvider;
                            }
                        });
                    }
                    else
                    {
                        authBuilder = services.AddAuthentication().AddCertificate(options =>
                        {
                            if (timeProvider != null)
                            {
                                options.TimeProvider = timeProvider;
                            }
                        });
                    }
                    if (useCache)
                    {
                        if (timeProvider != null)
                        {
                            services.AddSingleton<ICertificateValidationCache>(new CertificateValidationCache(Options.Create(new CertificateValidationCacheOptions()), timeProvider));
                        }
                        else
                        {
                            authBuilder.AddCertificateCache();
                        }
                    }
 
                    if (wireUpHeaderMiddleware && !string.IsNullOrEmpty(headerName))
                    {
                        services.AddCertificateForwarding(options =>
                        {
                            options.CertificateHeader = headerName;
                        });
                    }
                }))
            .Build();
 
        await host.StartAsync();
 
        var server = host.GetTestServer();
        server.BaseAddress = baseAddress;
        return host;
    }
 
    private readonly CertificateAuthenticationEvents successfulValidationEvents = new CertificateAuthenticationEvents()
    {
        OnCertificateValidated = context =>
        {
            var claims = new[]
            {
                    new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
                    new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
            };
 
            context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
            context.Success();
            return Task.CompletedTask;
        }
    };
 
    private readonly CertificateAuthenticationEvents failedValidationEvents = new CertificateAuthenticationEvents()
    {
        OnCertificateValidated = context =>
        {
            context.Fail("Not validated");
            return Task.CompletedTask;
        }
    };
 
    private readonly CertificateAuthenticationEvents unprocessedValidationEvents = new CertificateAuthenticationEvents()
    {
        OnCertificateValidated = context =>
        {
            return Task.CompletedTask;
        }
    };
}