/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Security/Authentication/test/CertificateTests.cs
1 069 строк
45 KB
Korolev Dmitry
Include scheme in certificate cache for Authentication middleware (#66673)
14 май 2026, 14:26
Не верифицирован
14 май 2026, 14:26
fde6094
Код
Авторство
О чём код?
// 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 void EventsPropertyIsInitializedOnConstruction() { var options = new CertificateAuthenticationOptions(); Assert.NotNull(options.Events); } [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; } }; [Fact] public async Task VerifyCacheIsIsolatedAcrossSchemes() { var scheme1ValidationCount = 0; var scheme2ValidationCount = 0; using var host = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .Configure(app => { app.Use((context, next) => { context.Connection.ClientCertificate = Certificates.SelfSignedValidWithNoEku; return next(context); }); app.UseAuthentication(); app.Run(async context => { var schemeName = context.Request.Query["scheme"].ToString(); var result = await context.AuthenticateAsync(schemeName); if (result.Succeeded) { context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.ContentType = "text/plain"; await context.Response.WriteAsync("Authenticated"); } else { context.Response.StatusCode = (int)HttpStatusCode.Forbidden; context.Response.ContentType = "text/plain"; await context.Response.WriteAsync("Denied"); } }); }) .ConfigureServices(services => { services.AddAuthentication() .AddCertificate("scheme1", options => { options.AllowedCertificateTypes = CertificateTypes.SelfSigned; options.Events = new CertificateAuthenticationEvents { OnCertificateValidated = context => { scheme1ValidationCount++; context.Principal = new ClaimsPrincipal( new ClaimsIdentity( [new Claim(ClaimTypes.Name, "scheme1User")], context.Scheme.Name)); context.Success(); return Task.CompletedTask; } }; }) .AddCertificate("scheme2", options => { options.AllowedCertificateTypes = CertificateTypes.SelfSigned; options.Events = new CertificateAuthenticationEvents { OnCertificateValidated = context => { scheme2ValidationCount++; context.Fail("Certificate does not meet scheme2 requirements"); return Task.CompletedTask; } }; }) .AddCertificateCache(); })) .Build(); await host.StartAsync(); using var server = host.GetTestServer(); var client = server.CreateClient(); // 1. Authenticate with the scheme1 — should succeed and cache var response1 = await client.GetAsync("https://example.com/?scheme=scheme1"); Assert.Equal(HttpStatusCode.OK, response1.StatusCode); Assert.Equal(1, scheme1ValidationCount); // 2. Authenticate with the scheme2 — must NOT reuse the scheme1's cached success var response2 = await client.GetAsync("https://example.com/?scheme=scheme2"); Assert.Equal(HttpStatusCode.Forbidden, response2.StatusCode); Assert.Equal(1, scheme2ValidationCount); // 3. Authenticate with the scheme1 again — should use the cache (no re-validation) var response3 = await client.GetAsync("https://example.com/?scheme=scheme1"); Assert.Equal(HttpStatusCode.OK, response3.StatusCode); Assert.Equal(1, scheme1ValidationCount); // Still 1 — served from cache } [Fact] public void VerifyCacheNoOpsWithoutSchemeInHttpContextItems() { var cache = new CertificateValidationCache(Options.Create(new CertificateValidationCacheOptions())); var certificate = Certificates.SelfSignedValidWithNoEku; var httpContext = new DefaultHttpContext(); // Do NOT set httpContext.Items[CertificateAuthenticationHandler.CertificateSchemeCacheKeyItem] // Put should no-op (no scheme = no cache key) var successResult = AuthenticateResult.Success( new AuthenticationTicket( new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "test") }, "test")), "test")); cache.Put(httpContext, certificate, successResult); // Get should return null even after Put var cached = cache.Get(httpContext, certificate); Assert.Null(cached); // Now set the scheme item — Put and Get should work httpContext.Items[CertificateAuthenticationHandler.CertificateSchemeCacheKeyItem] = "MyScheme"; cache.Put(httpContext, certificate, successResult); var cachedWithScheme = cache.Get(httpContext, certificate); Assert.NotNull(cachedWithScheme); Assert.True(cachedWithScheme.Succeeded); } }