From ece5ad36e22daea23e7428d744238ea146e47fd5 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Tue, 18 Sep 2018 14:22:56 -0700 Subject: [PATCH] Implement ITlsConnectionFeature (#1390) --- .../Core/IISHttpContext.FeatureCollection.cs | 37 ++++- .../Core/IISHttpContext.Features.cs | 1 + .../NativeMethods.cs | 1 - .../ClientCertificateFixture.cs | 91 +++++++++++ .../ClientCertificateTests.cs | 85 ++++++++++ .../CommonStartupTests.cs | 4 - .../PublishedSitesFixture.cs | 2 +- .../SkipIfNotAdminAttribute.cs | 25 +++ test/IISExpress.FunctionalTests/HttpsTests.cs | 60 +++++++ .../OutOfProcess/HttpsTest.cs | 148 ------------------ test/WebSites/InProcessWebSite/Startup.cs | 3 + test/WebSites/OutOfProcessWebSite/Startup.cs | 4 - .../shared/SharedStartup/Startup.shared.cs | 6 + 13 files changed, 307 insertions(+), 160 deletions(-) create mode 100644 test/Common.FunctionalTests/ClientCertificateFixture.cs create mode 100644 test/Common.FunctionalTests/ClientCertificateTests.cs create mode 100644 test/Common.FunctionalTests/SkipIfNotAdminAttribute.cs create mode 100644 test/IISExpress.FunctionalTests/HttpsTests.cs delete mode 100644 test/IISExpress.FunctionalTests/OutOfProcess/HttpsTest.cs diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs index df6ba267d0..19f65bc900 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs @@ -6,7 +6,9 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Runtime.InteropServices; using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -24,13 +26,15 @@ namespace Microsoft.AspNetCore.Server.IIS.Core IHttpRequestLifetimeFeature, IHttpAuthenticationFeature, IServerVariablesFeature, - IHttpBufferingFeature + IHttpBufferingFeature, + ITlsConnectionFeature { // NOTE: When feature interfaces are added to or removed from this HttpProtocol implementation, // then the list of `implementedFeatures` in the generated code project MUST also be updated. private int _featureRevision; private string _httpProtocolVersion = null; + private X509Certificate2 _certificate; private List> MaybeExtra; public void ResetFeatureCollection() @@ -276,7 +280,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core ReasonPhrase = ReasonPhrases.GetReasonPhrase(StatusCodes.Status101SwitchingProtocols); // If we started reading before calling Upgrade Task should be completed at this point - // because read would return 0 syncronosly + // because read would return 0 synchronously Debug.Assert(_readBodyTask == null || _readBodyTask.IsCompleted); // Reset reading status to allow restarting with new IO @@ -290,6 +294,35 @@ namespace Microsoft.AspNetCore.Server.IIS.Core return new DuplexStream(RequestBody, ResponseBody); } + Task ITlsConnectionFeature.GetClientCertificateAsync(CancellationToken cancellationToken) + { + return Task.FromResult(((ITlsConnectionFeature)this).ClientCertificate); + } + + unsafe X509Certificate2 ITlsConnectionFeature.ClientCertificate + { + get + { + if (_certificate == null && + NativeRequest->pSslInfo != null && + NativeRequest->pSslInfo->pClientCertInfo != null && + NativeRequest->pSslInfo->pClientCertInfo->pCertEncoded != null && + NativeRequest->pSslInfo->pClientCertInfo->CertEncodedSize != 0) + { + // Based off of from https://referencesource.microsoft.com/#system/net/System/Net/HttpListenerRequest.cs,1037c8ec82879ba0,references + var rawCertificateCopy = new byte[NativeRequest->pSslInfo->pClientCertInfo->CertEncodedSize]; + Marshal.Copy((IntPtr)NativeRequest->pSslInfo->pClientCertInfo->pCertEncoded, rawCertificateCopy, 0, rawCertificateCopy.Length); + _certificate = new X509Certificate2(rawCertificateCopy); + } + + return _certificate; + } + set + { + _certificate = value; + } + } + IEnumerator> IEnumerable>.GetEnumerator() => FastEnumerable().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => FastEnumerable().GetEnumerator(); diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Features.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Features.cs index f2a0016c0a..6e107e03f0 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Features.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Features.cs @@ -61,6 +61,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core _currentIHttpAuthenticationFeature = this; _currentIServerVariablesFeature = this; _currentIHttpBufferingFeature = this; + _currentITlsConnectionFeature = this; } internal object FastFeatureGet(Type key) diff --git a/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs b/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs index c8feb2ef40..906a86cdda 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs @@ -23,7 +23,6 @@ namespace Microsoft.AspNetCore.Server.IIS public static extern bool CloseHandle(IntPtr handle); - [DllImport("kernel32.dll")] private static extern IntPtr GetModuleHandle(string lpModuleName); diff --git a/test/Common.FunctionalTests/ClientCertificateFixture.cs b/test/Common.FunctionalTests/ClientCertificateFixture.cs new file mode 100644 index 0000000000..c3fa927b46 --- /dev/null +++ b/test/Common.FunctionalTests/ClientCertificateFixture.cs @@ -0,0 +1,91 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public class ClientCertificateFixture : IDisposable + { + public ClientCertificateFixture() + { + using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine)) + { + store.Open(OpenFlags.ReadWrite); + + foreach (var cert in store.Certificates) + { + if (cert.Issuer != "CN=IISIntegrationTest_Root") + { + continue; + } + Certificate = cert; + store.Close(); + return; + } + + var parentKey = CreateKeyMaterial(2048); + + // On first run of the test, creates the certificate in the trusted root certificate authorities. + var parentRequest = new CertificateRequest("CN=IISIntegrationTest_Root", parentKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + parentRequest.CertificateExtensions.Add( + new X509BasicConstraintsExtension( + certificateAuthority: true, + hasPathLengthConstraint: false, + pathLengthConstraint: 0, + critical: true)); + + parentRequest.CertificateExtensions.Add( + new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.NonRepudiation, critical: true)); + + parentRequest.CertificateExtensions.Add( + new X509SubjectKeyIdentifierExtension(parentRequest.PublicKey, false)); + + var notBefore = DateTimeOffset.Now.AddDays(-1); + var notAfter = DateTimeOffset.Now.AddYears(5); + + var parentCert = parentRequest.CreateSelfSigned(notBefore, notAfter); + + // Need to export/import the certificate to associate the private key with the cert. + var imported = parentCert; + + var export = parentCert.Export(X509ContentType.Pkcs12, ""); + imported = new X509Certificate2(export, "", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); + Array.Clear(export, 0, export.Length); + + // Add the cert to the cert store + Certificate = imported; + + store.Add(certificate: imported); + store.Close(); + } + } + + public X509Certificate2 Certificate { get; } + + public void Dispose() + { + using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine)) + { + store.Open(OpenFlags.ReadWrite); + store.Remove(Certificate); + store.Close(); + } + } + + private RSA CreateKeyMaterial(int minimumKeySize) + { + var rsa = RSA.Create(minimumKeySize); + if (rsa.KeySize < minimumKeySize) + { + throw new InvalidOperationException($"Failed to create a key with a size of {minimumKeySize} bits"); + } + + return rsa; + } + } +} diff --git a/test/Common.FunctionalTests/ClientCertificateTests.cs b/test/Common.FunctionalTests/ClientCertificateTests.cs new file mode 100644 index 0000000000..289fb18c2f --- /dev/null +++ b/test/Common.FunctionalTests/ClientCertificateTests.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + [SkipIfNotAdmin] + public class ClientCertificateTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + private readonly ClientCertificateFixture _certFixture; + + public ClientCertificateTests(PublishedSitesFixture fixture, ClientCertificateFixture certFixture) + { + _fixture = fixture; + _certFixture = certFixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22, Tfm.Net461) + .WithAllApplicationTypes() + .WithAllAncmVersions() + .WithAllHostingModels(); + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public Task HttpsNoClientCert_NoClientCert(TestVariant variant) + { + return ClientCertTest(variant, sendClientCert: false); + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public Task HttpsClientCert_GetCertInformation(TestVariant variant) + { + return ClientCertTest(variant, sendClientCert: true); + } + + private async Task ClientCertTest(TestVariant variant, bool sendClientCert) + { + var port = TestPortHelper.GetNextSSLPort(); + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant); + deploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/"; + deploymentParameters.AddHttpsToServerConfig(); + + var deploymentResult = await DeployAsync(deploymentParameters); + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (a, b, c, d) => true, + ClientCertificateOptions = ClientCertificateOption.Manual, + }; + + if (sendClientCert) + { + Assert.NotNull(_certFixture.Certificate); + handler.ClientCertificates.Add(_certFixture.Certificate); + } + + var client = deploymentResult.CreateClient(handler); + var response = await client.GetAsync("GetClientCert"); + + var responseText = await response.Content.ReadAsStringAsync(); + + if (sendClientCert) + { + Assert.Equal($"Enabled;{_certFixture.Certificate.GetCertHashString()}", responseText); + } + else + { + Assert.Equal("Disabled", responseText); + } + } + } +} diff --git a/test/Common.FunctionalTests/CommonStartupTests.cs b/test/Common.FunctionalTests/CommonStartupTests.cs index 760c1ad04b..e2bcf2a8f9 100644 --- a/test/Common.FunctionalTests/CommonStartupTests.cs +++ b/test/Common.FunctionalTests/CommonStartupTests.cs @@ -1,14 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.IO; -using System.Linq; using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; using Microsoft.AspNetCore.Server.IntegrationTesting; -using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; using Microsoft.AspNetCore.Testing.xunit; using Xunit; diff --git a/test/Common.FunctionalTests/PublishedSitesFixture.cs b/test/Common.FunctionalTests/PublishedSitesFixture.cs index 1ebfdab38e..282ee26109 100644 --- a/test/Common.FunctionalTests/PublishedSitesFixture.cs +++ b/test/Common.FunctionalTests/PublishedSitesFixture.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests /// This type just maps collection names to available fixtures /// [CollectionDefinition(Name)] - public class PublishedSitesCollection : ICollectionFixture + public class PublishedSitesCollection : ICollectionFixture, ICollectionFixture { public const string Name = nameof(PublishedSitesCollection); } diff --git a/test/Common.FunctionalTests/SkipIfNotAdminAttribute.cs b/test/Common.FunctionalTests/SkipIfNotAdminAttribute.cs new file mode 100644 index 0000000000..d2acb70415 --- /dev/null +++ b/test/Common.FunctionalTests/SkipIfNotAdminAttribute.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Principal; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class SkipIfNotAdminAttribute : Attribute, ITestCondition + { + public bool IsMet + { + get + { + var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + } + + public string SkipReason => "The current process is not running as admin."; + } +} diff --git a/test/IISExpress.FunctionalTests/HttpsTests.cs b/test/IISExpress.FunctionalTests/HttpsTests.cs new file mode 100644 index 0000000000..ac58f73c0e --- /dev/null +++ b/test/IISExpress.FunctionalTests/HttpsTests.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class HttpsTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public HttpsTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22, Tfm.Net461) + .WithAllApplicationTypes() + .WithAllAncmVersions() + .WithAllHostingModels(); + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task HttpsHelloWorld(TestVariant variant) + { + var port = TestPortHelper.GetNextSSLPort(); + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant); + deploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/"; + deploymentParameters.AddHttpsToServerConfig(); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (a, b, c, d) => true + }; + var client = deploymentResult.CreateClient(handler); + var response = await client.GetAsync("HttpsHelloWorld"); + var responseText = await response.Content.ReadAsStringAsync(); + if (variant.HostingModel == HostingModel.OutOfProcess) + { + Assert.Equal("Scheme:https; Original:http", responseText); + } + else + { + Assert.Equal("Scheme:https; Original:", responseText); + } + } + } +} diff --git a/test/IISExpress.FunctionalTests/OutOfProcess/HttpsTest.cs b/test/IISExpress.FunctionalTests/OutOfProcess/HttpsTest.cs deleted file mode 100644 index 5735393c57..0000000000 --- a/test/IISExpress.FunctionalTests/OutOfProcess/HttpsTest.cs +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Net.Http; -using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; -using Microsoft.AspNetCore.Server.IntegrationTesting; -using Microsoft.AspNetCore.Server.IntegrationTesting.Common; -using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; -using Microsoft.AspNetCore.Testing.xunit; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests -{ - // IIS Express preregisteres 44300-44399 ports with SSL bindings. - // So these tests always have to use ports in this range, and we can't rely on OS-allocated ports without a whole lot of ceremony around - // creating self-signed certificates and registering SSL bindings with HTTP.sys - // Test specific to IISExpress - [Collection(PublishedSitesCollection.Name)] - public class HttpsTest : IISFunctionalTestBase - { - private readonly PublishedSitesFixture _fixture; - - public HttpsTest(PublishedSitesFixture fixture) - { - _fixture = fixture; - } - - public static TestMatrix TestVariants - => TestMatrix.ForServers(DeployerSelector.ServerType) - .WithTfms(Tfm.NetCoreApp22, Tfm.Net461) - .WithAllAncmVersions(); - - [ConditionalTheory] - [MemberData(nameof(TestVariants))] - public async Task HttpsHelloWorld(TestVariant variant) - { - var port = TestPortHelper.GetNextSSLPort(); - var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant); - deploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/"; - deploymentParameters.AddHttpsToServerConfig(); - - var deploymentResult = await DeployAsync(deploymentParameters); - - var handler = new HttpClientHandler - { - ServerCertificateCustomValidationCallback = (a, b, c, d) => true - }; - var client = deploymentResult.CreateClient(handler); - var response = await client.GetAsync("HttpsHelloWorld"); - var responseText = await response.Content.ReadAsStringAsync(); - Assert.Equal("Scheme:https; Original:http", responseText); - } - - [ConditionalTheory] - [MemberData(nameof(TestVariants))] - public Task HttpsHelloWorld_NoClientCert(TestVariant variant) - { - return HttpsHelloWorldCerts(variant, sendClientCert: false); - } - -#pragma warning disable xUnit1004 // Test methods should not be skipped - [ConditionalTheory(Skip = "Manual test only, selecting a client cert is non-determanistic on different machines.")] - [MemberData(nameof(TestVariants))] -#pragma warning restore xUnit1004 // Test methods should not be skipped - public Task HttpsHelloWorld_ClientCert(TestVariant variant) - { - return HttpsHelloWorldCerts(variant, sendClientCert: true); - } - - private async Task HttpsHelloWorldCerts(TestVariant variant, bool sendClientCert) - { - var port = TestPortHelper.GetNextSSLPort(); - var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant); - deploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/"; - deploymentParameters.AddHttpsToServerConfig(); - - var deploymentResult = await DeployAsync(deploymentParameters); - - var handler = new HttpClientHandler - { - ServerCertificateCustomValidationCallback = (a, b, c, d) => true, - ClientCertificateOptions = ClientCertificateOption.Manual - }; - - if (sendClientCert) - { - X509Certificate2 clientCert = FindClientCert(); - Assert.NotNull(clientCert); - handler.ClientCertificates.Add(clientCert); - } - - var client = deploymentResult.CreateClient(handler); - - // Request to base address and check if various parts of the body are rendered & measure the cold startup time. - var response = await client.GetAsync("checkclientcert"); - - var responseText = await response.Content.ReadAsStringAsync(); - if (sendClientCert) - { - Assert.Equal("Scheme:https; Original:http; has cert? True", responseText); - } - else - { - Assert.Equal("Scheme:https; Original:http; has cert? False", responseText); - } - } - - private X509Certificate2 FindClientCert() - { - var store = new X509Store(); - store.Open(OpenFlags.ReadOnly); - - foreach (var cert in store.Certificates) - { - bool isClientAuth = false; - bool isSmartCard = false; - foreach (var extension in cert.Extensions) - { - var eku = extension as X509EnhancedKeyUsageExtension; - if (eku != null) - { - foreach (var oid in eku.EnhancedKeyUsages) - { - if (oid.FriendlyName == "Client Authentication") - { - isClientAuth = true; - } - else if (oid.FriendlyName == "Smart Card Logon") - { - isSmartCard = true; - break; - } - } - } - } - - if (isClientAuth && !isSmartCard) - { - return cert; - } - } - return null; - } - } -} diff --git a/test/WebSites/InProcessWebSite/Startup.cs b/test/WebSites/InProcessWebSite/Startup.cs index ef1863ef2b..39d3b9f765 100644 --- a/test/WebSites/InProcessWebSite/Startup.cs +++ b/test/WebSites/InProcessWebSite/Startup.cs @@ -681,5 +681,8 @@ namespace TestSite { await ctx.Response.WriteAsync(string.Join("|", Environment.GetCommandLineArgs().Skip(1))); } + + public Task HttpsHelloWorld(HttpContext ctx) => + ctx.Response.WriteAsync("Scheme:" + ctx.Request.Scheme + "; Original:" + ctx.Request.Headers["x-original-proto"]); } } diff --git a/test/WebSites/OutOfProcessWebSite/Startup.cs b/test/WebSites/OutOfProcessWebSite/Startup.cs index a796f9a7a3..de54a85a8a 100644 --- a/test/WebSites/OutOfProcessWebSite/Startup.cs +++ b/test/WebSites/OutOfProcessWebSite/Startup.cs @@ -42,10 +42,6 @@ namespace TestSite public Task HttpsHelloWorld(HttpContext ctx) => ctx.Response.WriteAsync("Scheme:" + ctx.Request.Scheme + "; Original:" + ctx.Request.Headers["x-original-proto"]); - public Task CheckClientCert(HttpContext ctx) => - ctx.Response.WriteAsync("Scheme:" + ctx.Request.Scheme + "; Original:" + ctx.Request.Headers["x-original-proto"] - + "; has cert? " + (ctx.Connection.ClientCertificate != null)); - public Task Anonymous(HttpContext context) => context.Response.WriteAsync("Anonymous?" + !context.User.Identity.IsAuthenticated); public Task Restricted(HttpContext context) diff --git a/test/WebSites/shared/SharedStartup/Startup.shared.cs b/test/WebSites/shared/SharedStartup/Startup.shared.cs index a804449029..4c6d2eb4cf 100644 --- a/test/WebSites/shared/SharedStartup/Startup.shared.cs +++ b/test/WebSites/shared/SharedStartup/Startup.shared.cs @@ -43,5 +43,11 @@ namespace TestSite await ctx.Response.WriteAsync(authScheme?.Name ?? "null"); } + + public async Task GetClientCert(HttpContext context) + { + var clientCert = context.Connection.ClientCertificate; + await context.Response.WriteAsync(clientCert != null ? $"Enabled;{clientCert.GetCertHashString()}" : "Disabled"); + } } }