Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/dotnet/aspnetcore.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHao Kung <HaoK@users.noreply.github.com>2019-06-01 08:49:40 +0300
committerGitHub <noreply@github.com>2019-06-01 08:49:40 +0300
commitb75b892eac51be8b2f0eb9dc9b47537fc02001c9 (patch)
treefa47b9a608102bd82e71e97fe0b0e039f32a26b2
parent4dde8b9461ed9b79030e644363dab628d681e05f (diff)
Add CertificateAuthentication (#9756)
-rw-r--r--eng/ProjectReferences.props1
-rw-r--r--src/Middleware/HttpOverrides/ref/Microsoft.AspNetCore.HttpOverrides.netcoreapp3.0.cs22
-rw-r--r--src/Middleware/HttpOverrides/src/CertificateForwardingBuilderExtensions.cs30
-rw-r--r--src/Middleware/HttpOverrides/src/CertificateForwardingFeature.cs51
-rw-r--r--src/Middleware/HttpOverrides/src/CertificateForwardingMiddleware.cs66
-rw-r--r--src/Middleware/HttpOverrides/src/CertificateForwardingOptions.cs30
-rw-r--r--src/Middleware/HttpOverrides/src/CertificateForwardingServiceExtensions.cs38
-rw-r--r--src/Middleware/HttpOverrides/src/LoggingExtensions.cs25
-rw-r--r--src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs222
-rw-r--r--src/Middleware/HttpOverrides/test/Microsoft.AspNetCore.HttpOverrides.Tests.csproj3
-rw-r--r--src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.csproj10
-rw-r--r--src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.netcoreapp3.0.cs59
-rw-r--r--src/Security/Authentication/Certificate/samples/Certificate.Sample/Certificate.Sample.csproj21
-rw-r--r--src/Security/Authentication/Certificate/samples/Certificate.Sample/Controllers/HomeController.cs12
-rw-r--r--src/Security/Authentication/Certificate/samples/Certificate.Sample/Program.cs26
-rw-r--r--src/Security/Authentication/Certificate/samples/Certificate.Sample/Properties/launchSettings.json20
-rw-r--r--src/Security/Authentication/Certificate/samples/Certificate.Sample/Startup.cs61
-rw-r--r--src/Security/Authentication/Certificate/samples/Certificate.Sample/Views/Home/Index.cshtml1
-rw-r--r--src/Security/Authentication/Certificate/src/CertificateAuthenticationDefaults.cs16
-rw-r--r--src/Security/Authentication/Certificate/src/CertificateAuthenticationExtensions.cs55
-rw-r--r--src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs235
-rw-r--r--src/Security/Authentication/Certificate/src/CertificateAuthenticationOptions.cs53
-rw-r--r--src/Security/Authentication/Certificate/src/CertificateTypes.cs29
-rw-r--r--src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationEvents.cs45
-rw-r--r--src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationFailedContext.cs33
-rw-r--r--src/Security/Authentication/Certificate/src/Events/CertificateValidatedContext.cs33
-rw-r--r--src/Security/Authentication/Certificate/src/LoggingExtensions.cs48
-rw-r--r--src/Security/Authentication/Certificate/src/Microsoft.AspNetCore.Authentication.Certificate.csproj16
-rw-r--r--src/Security/Authentication/Certificate/src/README-IISConfig.pngbin0 -> 10168 bytes
-rw-r--r--src/Security/Authentication/Certificate/src/README.md234
-rw-r--r--src/Security/Authentication/Certificate/src/X509CertificateExtensions.cs27
-rw-r--r--src/Security/Authentication/test/CertificateTests.cs628
-rw-r--r--src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj6
-rw-r--r--src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateExpired.cerbin0 -> 920 bytes
-rw-r--r--src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateNotValidYet.cerbin0 -> 932 bytes
-rw-r--r--src/Security/Authentication/test/TestCertificates/validSelfSignedClientEkuCertificate.cerbin0 -> 928 bytes
-rw-r--r--src/Security/Authentication/test/TestCertificates/validSelfSignedNoEkuCertificate.cerbin0 -> 930 bytes
-rw-r--r--src/Security/Authentication/test/TestCertificates/validSelfSignedServerEkuCertificate.cerbin0 -> 928 bytes
-rw-r--r--src/Security/Security.sln23
-rw-r--r--src/Shared/test/Certificates/selfSignedNoEkuCertificateExpired.cerbin0 -> 920 bytes
-rw-r--r--src/Shared/test/Certificates/selfSignedNoEkuCertificateNotValidYet.cerbin0 -> 932 bytes
-rw-r--r--src/Shared/test/Certificates/validSelfSignedClientEkuCertificate.cerbin0 -> 928 bytes
-rw-r--r--src/Shared/test/Certificates/validSelfSignedNoEkuCertificate.cerbin0 -> 930 bytes
-rw-r--r--src/Shared/test/Certificates/validSelfSignedServerEkuCertificate.cerbin0 -> 928 bytes
44 files changed, 2178 insertions, 1 deletions
diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props
index fa3ee5688b..9df79e4053 100644
--- a/eng/ProjectReferences.props
+++ b/eng/ProjectReferences.props
@@ -57,6 +57,7 @@
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions" ProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Abstractions\src\Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj" RefProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Abstractions\ref\Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" ProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Libuv\src\Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.csproj" RefProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Libuv\ref\Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" ProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Sockets\src\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj" RefProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Sockets\ref\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj" />
+ <ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.Certificate" ProjectPath="$(RepoRoot)src\Security\Authentication\Certificate\src\Microsoft.AspNetCore.Authentication.Certificate.csproj" RefProjectPath="$(RepoRoot)src\Security\Authentication\Certificate\ref\Microsoft.AspNetCore.Authentication.Certificate.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.Cookies" ProjectPath="$(RepoRoot)src\Security\Authentication\Cookies\src\Microsoft.AspNetCore.Authentication.Cookies.csproj" RefProjectPath="$(RepoRoot)src\Security\Authentication\Cookies\ref\Microsoft.AspNetCore.Authentication.Cookies.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication" ProjectPath="$(RepoRoot)src\Security\Authentication\Core\src\Microsoft.AspNetCore.Authentication.csproj" RefProjectPath="$(RepoRoot)src\Security\Authentication\Core\ref\Microsoft.AspNetCore.Authentication.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.Facebook" ProjectPath="$(RepoRoot)src\Security\Authentication\Facebook\src\Microsoft.AspNetCore.Authentication.Facebook.csproj" RefProjectPath="$(RepoRoot)src\Security\Authentication\Facebook\ref\Microsoft.AspNetCore.Authentication.Facebook.csproj" />
diff --git a/src/Middleware/HttpOverrides/ref/Microsoft.AspNetCore.HttpOverrides.netcoreapp3.0.cs b/src/Middleware/HttpOverrides/ref/Microsoft.AspNetCore.HttpOverrides.netcoreapp3.0.cs
index cb7ee21e14..f21794c54b 100644
--- a/src/Middleware/HttpOverrides/ref/Microsoft.AspNetCore.HttpOverrides.netcoreapp3.0.cs
+++ b/src/Middleware/HttpOverrides/ref/Microsoft.AspNetCore.HttpOverrides.netcoreapp3.0.cs
@@ -3,6 +3,10 @@
namespace Microsoft.AspNetCore.Builder
{
+ public static partial class CertificateForwardingBuilderExtensions
+ {
+ public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseCertificateForwarding(this Microsoft.AspNetCore.Builder.IApplicationBuilder app) { throw null; }
+ }
public static partial class ForwardedHeadersExtensions
{
public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseForwardedHeaders(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder) { throw null; }
@@ -37,6 +41,17 @@ namespace Microsoft.AspNetCore.Builder
}
namespace Microsoft.AspNetCore.HttpOverrides
{
+ public partial class CertificateForwardingMiddleware
+ {
+ public CertificateForwardingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.HttpOverrides.CertificateForwardingOptions> options) { }
+ public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext httpContext) { throw null; }
+ }
+ public partial class CertificateForwardingOptions
+ {
+ public System.Func<string, System.Security.Cryptography.X509Certificates.X509Certificate2> HeaderConverter;
+ public CertificateForwardingOptions() { }
+ public string CertificateHeader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ }
[System.FlagsAttribute]
public enum ForwardedHeaders
{
@@ -75,3 +90,10 @@ namespace Microsoft.AspNetCore.HttpOverrides
public bool Contains(System.Net.IPAddress address) { throw null; }
}
}
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static partial class CertificateForwardingServiceExtensions
+ {
+ public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddCertificateForwarding(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<Microsoft.AspNetCore.HttpOverrides.CertificateForwardingOptions> configure) { throw null; }
+ }
+}
diff --git a/src/Middleware/HttpOverrides/src/CertificateForwardingBuilderExtensions.cs b/src/Middleware/HttpOverrides/src/CertificateForwardingBuilderExtensions.cs
new file mode 100644
index 0000000000..038b19b637
--- /dev/null
+++ b/src/Middleware/HttpOverrides/src/CertificateForwardingBuilderExtensions.cs
@@ -0,0 +1,30 @@
+// 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 Microsoft.AspNetCore.HttpOverrides;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ /// <summary>
+ /// Extension methods for using certificate fowarding.
+ /// </summary>
+ public static class CertificateForwardingBuilderExtensions
+ {
+ /// <summary>
+ /// Adds a middleware to the pipeline that will look for a certificate in a request header
+ /// decode it, and updates HttpContext.Connection.ClientCertificate.
+ /// </summary>
+ /// <param name="app"></param>
+ /// <returns></returns>
+ public static IApplicationBuilder UseCertificateForwarding(this IApplicationBuilder app)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseMiddleware<CertificateForwardingMiddleware>();
+ }
+ }
+}
diff --git a/src/Middleware/HttpOverrides/src/CertificateForwardingFeature.cs b/src/Middleware/HttpOverrides/src/CertificateForwardingFeature.cs
new file mode 100644
index 0000000000..a7d284a0cc
--- /dev/null
+++ b/src/Middleware/HttpOverrides/src/CertificateForwardingFeature.cs
@@ -0,0 +1,51 @@
+// 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.X509Certificates;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.HttpOverrides
+{
+ internal class CertificateForwardingFeature : ITlsConnectionFeature
+ {
+ private ILogger _logger;
+ private StringValues _header;
+ private CertificateForwardingOptions _options;
+ private X509Certificate2 _certificate;
+
+ public CertificateForwardingFeature(ILogger logger, StringValues header, CertificateForwardingOptions options)
+ {
+ _logger = logger;
+ _options = options;
+ _header = header;
+ }
+
+ public X509Certificate2 ClientCertificate
+ {
+ get
+ {
+ if (_certificate == null)
+ {
+ try
+ {
+ _certificate = _options.HeaderConverter(_header);
+ }
+ catch (Exception e)
+ {
+ _logger.NoCertificate(e);
+ }
+ }
+ return _certificate;
+ }
+ set => _certificate = value;
+ }
+
+ public Task<X509Certificate2> GetClientCertificateAsync(CancellationToken cancellationToken)
+ => Task.FromResult(ClientCertificate);
+ }
+}
diff --git a/src/Middleware/HttpOverrides/src/CertificateForwardingMiddleware.cs b/src/Middleware/HttpOverrides/src/CertificateForwardingMiddleware.cs
new file mode 100644
index 0000000000..77ff282361
--- /dev/null
+++ b/src/Middleware/HttpOverrides/src/CertificateForwardingMiddleware.cs
@@ -0,0 +1,66 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.HttpOverrides
+{
+ /// <summary>
+ /// Middleware that converts a forward header into a client certificate if found.
+ /// </summary>
+ public class CertificateForwardingMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly CertificateForwardingOptions _options;
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// Constructor.
+ /// </summary>
+ /// <param name="next"></param>
+ /// <param name="loggerFactory"></param>
+ /// <param name="options"></param>
+ public CertificateForwardingMiddleware(
+ RequestDelegate next,
+ ILoggerFactory loggerFactory,
+ IOptions<CertificateForwardingOptions> options)
+ {
+ _next = next ?? throw new ArgumentNullException(nameof(next));
+
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ _options = options.Value;
+ _logger = loggerFactory.CreateLogger<CertificateForwardingMiddleware>();
+ }
+
+ /// <summary>
+ /// Looks for the presence of a <see cref="CertificateForwardingOptions.CertificateHeader"/> header in the request,
+ /// if found, converts this header to a ClientCertificate set on the connection.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="HttpContext"/>.</param>
+ /// <returns>A <see cref="Task"/>.</returns>
+ public Task Invoke(HttpContext httpContext)
+ {
+ var header = httpContext.Request.Headers[_options.CertificateHeader];
+ if (!StringValues.IsNullOrEmpty(header))
+ {
+ httpContext.Features.Set<ITlsConnectionFeature>(new CertificateForwardingFeature(_logger, header, _options));
+ }
+ return _next(httpContext);
+ }
+ }
+}
diff --git a/src/Middleware/HttpOverrides/src/CertificateForwardingOptions.cs b/src/Middleware/HttpOverrides/src/CertificateForwardingOptions.cs
new file mode 100644
index 0000000000..4dccdda3b1
--- /dev/null
+++ b/src/Middleware/HttpOverrides/src/CertificateForwardingOptions.cs
@@ -0,0 +1,30 @@
+// 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.X509Certificates;
+
+namespace Microsoft.AspNetCore.HttpOverrides
+{
+ /// <summary>
+ /// Used to configure the <see cref="CertificateForwardingMiddleware"/>.
+ /// </summary>
+ public class CertificateForwardingOptions
+ {
+ /// <summary>
+ /// The name of the header containing the client certificate.
+ /// </summary>
+ /// <remarks>
+ /// This defaults to X-Client-Cert
+ /// </remarks>
+ public string CertificateHeader { get; set; } = "X-Client-Cert";
+
+ /// <summary>
+ /// The function used to convert the header to an instance of <see cref="X509Certificate2"/>.
+ /// </summary>
+ /// <remarks>
+ /// This defaults to a conversion from a base64 encoded string.
+ /// </remarks>
+ public Func<string, X509Certificate2> HeaderConverter = (headerValue) => new X509Certificate2(Convert.FromBase64String(headerValue));
+ }
+}
diff --git a/src/Middleware/HttpOverrides/src/CertificateForwardingServiceExtensions.cs b/src/Middleware/HttpOverrides/src/CertificateForwardingServiceExtensions.cs
new file mode 100644
index 0000000000..ffdd4e403b
--- /dev/null
+++ b/src/Middleware/HttpOverrides/src/CertificateForwardingServiceExtensions.cs
@@ -0,0 +1,38 @@
+// 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 Microsoft.AspNetCore.HttpOverrides;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ /// <summary>
+ /// Extension methods for using certificate fowarding.
+ /// </summary>
+ public static class CertificateForwardingServiceExtensions
+ {
+ /// <summary>
+ /// Adds certificate forwarding to the specified <see cref="IServiceCollection" />.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection"/>.</param>
+ /// <param name="configure">An action delegate to configure the provided <see cref="CertificateForwardingOptions"/>.</param>
+ /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
+ public static IServiceCollection AddCertificateForwarding(
+ this IServiceCollection services,
+ Action<CertificateForwardingOptions> configure)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if (configure == null)
+ {
+ throw new ArgumentNullException(nameof(configure));
+ }
+
+ services.AddOptions<CertificateForwardingOptions>().Validate(o => !string.IsNullOrEmpty(o.CertificateHeader), "CertificateForwarderOptions.CertificateHeader cannot be null or empty.");
+ return services.Configure(configure);
+ }
+ }
+}
diff --git a/src/Middleware/HttpOverrides/src/LoggingExtensions.cs b/src/Middleware/HttpOverrides/src/LoggingExtensions.cs
new file mode 100644
index 0000000000..27a2dce494
--- /dev/null
+++ b/src/Middleware/HttpOverrides/src/LoggingExtensions.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;
+
+namespace Microsoft.Extensions.Logging
+{
+ internal static class LoggingExtensions
+ {
+ private static Action<ILogger, Exception> _noCertificate;
+
+ static LoggingExtensions()
+ {
+ _noCertificate = LoggerMessage.Define(
+ eventId: new EventId(0, "NoCertificate"),
+ logLevel: LogLevel.Warning,
+ formatString: "Could not read certificate from header.");
+ }
+
+ public static void NoCertificate(this ILogger logger, Exception exception)
+ {
+ _noCertificate(logger, exception);
+ }
+ }
+}
diff --git a/src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs b/src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs
new file mode 100644
index 0000000000..42464ccb98
--- /dev/null
+++ b/src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs
@@ -0,0 +1,222 @@
+// 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.Security.Cryptography.X509Certificates;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.HttpOverrides
+{
+ public class CertificateForwardingTests
+ {
+ [Fact]
+ public void VerifySettingNullHeaderOptionThrows()
+ {
+ var services = new ServiceCollection()
+ .AddOptions()
+ .AddCertificateForwarding(o => o.CertificateHeader = null);
+ var options = services.BuildServiceProvider().GetRequiredService<IOptions<CertificateForwardingOptions>>();
+ Assert.Throws<OptionsValidationException>(() => options.Value);
+ }
+
+ [Fact]
+ public void VerifySettingEmptyHeaderOptionThrows()
+ {
+ var services = new ServiceCollection()
+ .AddOptions()
+ .AddCertificateForwarding(o => o.CertificateHeader = "");
+ var options = services.BuildServiceProvider().GetRequiredService<IOptions<CertificateForwardingOptions>>();
+ Assert.Throws<OptionsValidationException>(() => options.Value);
+ }
+
+ [Fact]
+ public async Task VerifyHeaderIsUsedIfNoCertificateAlreadySet()
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddCertificateForwarding(options => { });
+ })
+ .Configure(app =>
+ {
+ app.Use(async (context, next) =>
+ {
+ Assert.Null(context.Connection.ClientCertificate);
+ await next();
+ });
+ app.UseCertificateForwarding();
+ app.Use(async (context, next) =>
+ {
+ Assert.Equal(context.Connection.ClientCertificate, Certificates.SelfSignedValidWithNoEku);
+ await next();
+ });
+ });
+ var server = new TestServer(builder);
+
+ var context = await server.SendAsync(c =>
+ {
+ c.Request.Headers["X-Client-Cert"] = Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData);
+ });
+ }
+
+ [Fact]
+ public async Task VerifyHeaderOverridesCertificateEvenAlreadySet()
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddCertificateForwarding(options => { });
+ })
+ .Configure(app =>
+ {
+ app.Use(async (context, next) =>
+ {
+ Assert.Null(context.Connection.ClientCertificate);
+ context.Connection.ClientCertificate = Certificates.SelfSignedNotYetValid;
+ await next();
+ });
+ app.UseCertificateForwarding();
+ app.Use(async (context, next) =>
+ {
+ Assert.Equal(context.Connection.ClientCertificate, Certificates.SelfSignedValidWithNoEku);
+ await next();
+ });
+ });
+ var server = new TestServer(builder);
+
+ var context = await server.SendAsync(c =>
+ {
+ c.Request.Headers["X-Client-Cert"] = Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData);
+ });
+ }
+
+ [Fact]
+ public async Task VerifySettingTheAzureHeaderOnTheForwarderOptionsWorks()
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddCertificateForwarding(options => options.CertificateHeader = "X-ARR-ClientCert");
+ })
+ .Configure(app =>
+ {
+ app.Use(async (context, next) =>
+ {
+ Assert.Null(context.Connection.ClientCertificate);
+ await next();
+ });
+ app.UseCertificateForwarding();
+ app.Use(async (context, next) =>
+ {
+ Assert.Equal(context.Connection.ClientCertificate, Certificates.SelfSignedValidWithNoEku);
+ await next();
+ });
+ });
+ var server = new TestServer(builder);
+
+ var context = await server.SendAsync(c =>
+ {
+ c.Request.Headers["X-ARR-ClientCert"] = Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData);
+ });
+ }
+
+ [Fact]
+ public async Task VerifyACustomHeaderFailsIfTheHeaderIsNotPresent()
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddCertificateForwarding(options => options.CertificateHeader = "some-random-header");
+ })
+ .Configure(app =>
+ {
+ app.Use(async (context, next) =>
+ {
+ Assert.Null(context.Connection.ClientCertificate);
+ await next();
+ });
+ app.UseCertificateForwarding();
+ app.Use(async (context, next) =>
+ {
+ Assert.Null(context.Connection.ClientCertificate);
+ await next();
+ });
+ });
+ var server = new TestServer(builder);
+
+ var context = await server.SendAsync(c =>
+ {
+ c.Request.Headers["not-the-right-header"] = Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData);
+ });
+ }
+
+ [Fact]
+ public async Task VerifyArrHeaderEncodedCertFailsOnBadEncoding()
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddCertificateForwarding(options => { });
+ })
+ .Configure(app =>
+ {
+ app.Use(async (context, next) =>
+ {
+ Assert.Null(context.Connection.ClientCertificate);
+ await next();
+ });
+ app.UseCertificateForwarding();
+ app.Use(async (context, next) =>
+ {
+ Assert.Null(context.Connection.ClientCertificate);
+ await next();
+ });
+ });
+ var server = new TestServer(builder);
+
+ var context = await server.SendAsync(c =>
+ {
+ c.Request.Headers["X-Client-Cert"] = "OOPS" + Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData);
+ });
+ }
+
+ private static class Certificates
+ {
+ public static X509Certificate2 SelfSignedValidWithClientEku { get; private set; } =
+ new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedClientEkuCertificate.cer"));
+
+ public static X509Certificate2 SelfSignedValidWithNoEku { get; private set; } =
+ new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedNoEkuCertificate.cer"));
+
+ public static X509Certificate2 SelfSignedValidWithServerEku { get; private set; } =
+ new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedServerEkuCertificate.cer"));
+
+ public static X509Certificate2 SelfSignedNotYetValid { get; private set; } =
+ new X509Certificate2(GetFullyQualifiedFilePath("selfSignedNoEkuCertificateNotValidYet.cer"));
+
+ public static X509Certificate2 SelfSignedExpired { get; private set; } =
+ new X509Certificate2(GetFullyQualifiedFilePath("selfSignedNoEkuCertificateExpired.cer"));
+
+ private static string GetFullyQualifiedFilePath(string filename)
+ {
+ var filePath = Path.Combine(AppContext.BaseDirectory, filename);
+ if (!File.Exists(filePath))
+ {
+ throw new FileNotFoundException(filePath);
+ }
+ return filePath;
+ }
+ }
+
+ }
+}
diff --git a/src/Middleware/HttpOverrides/test/Microsoft.AspNetCore.HttpOverrides.Tests.csproj b/src/Middleware/HttpOverrides/test/Microsoft.AspNetCore.HttpOverrides.Tests.csproj
index da3cde94cb..c5f9652ddc 100644
--- a/src/Middleware/HttpOverrides/test/Microsoft.AspNetCore.HttpOverrides.Tests.csproj
+++ b/src/Middleware/HttpOverrides/test/Microsoft.AspNetCore.HttpOverrides.Tests.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
@@ -8,6 +8,7 @@
<Reference Include="Microsoft.AspNetCore.HttpOverrides" />
<Reference Include="Microsoft.AspNetCore.TestHost" />
<Reference Include="Microsoft.Extensions.Logging.Testing" />
+ <Content Include="$(SharedSourceRoot)test\Certificates\*.cer" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
</Project>
diff --git a/src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.csproj b/src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.csproj
new file mode 100644
index 0000000000..3cf6d51079
--- /dev/null
+++ b/src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.csproj
@@ -0,0 +1,10 @@
+<!-- This file is automatically generated. -->
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFrameworks>netcoreapp3.0</TargetFrameworks>
+ </PropertyGroup>
+ <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
+ <Compile Include="Microsoft.AspNetCore.Authentication.Certificate.netcoreapp3.0.cs" />
+ <Reference Include="Microsoft.AspNetCore.Authentication" />
+ </ItemGroup>
+</Project>
diff --git a/src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.netcoreapp3.0.cs b/src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.netcoreapp3.0.cs
new file mode 100644
index 0000000000..7fa9e147ab
--- /dev/null
+++ b/src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.netcoreapp3.0.cs
@@ -0,0 +1,59 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Authentication.Certificate
+{
+ public static partial class CertificateAuthenticationDefaults
+ {
+ public const string AuthenticationScheme = "Certificate";
+ }
+ public partial class CertificateAuthenticationEvents
+ {
+ public CertificateAuthenticationEvents() { }
+ public System.Func<Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationFailedContext, System.Threading.Tasks.Task> OnAuthenticationFailed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public System.Func<Microsoft.AspNetCore.Authentication.Certificate.CertificateValidatedContext, System.Threading.Tasks.Task> OnCertificateValidated { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public virtual System.Threading.Tasks.Task AuthenticationFailed(Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationFailedContext context) { throw null; }
+ public virtual System.Threading.Tasks.Task CertificateValidated(Microsoft.AspNetCore.Authentication.Certificate.CertificateValidatedContext context) { throw null; }
+ }
+ public partial class CertificateAuthenticationFailedContext : Microsoft.AspNetCore.Authentication.ResultContext<Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions>
+ {
+ public CertificateAuthenticationFailedContext(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Authentication.AuthenticationScheme scheme, Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions options) : base (default(Microsoft.AspNetCore.Http.HttpContext), default(Microsoft.AspNetCore.Authentication.AuthenticationScheme), default(Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions)) { }
+ public System.Exception Exception { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ }
+ public partial class CertificateAuthenticationOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions
+ {
+ public CertificateAuthenticationOptions() { }
+ public Microsoft.AspNetCore.Authentication.Certificate.CertificateTypes AllowedCertificateTypes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public new Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationEvents Events { get { throw null; } set { } }
+ public System.Security.Cryptography.X509Certificates.X509RevocationFlag RevocationFlag { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public System.Security.Cryptography.X509Certificates.X509RevocationMode RevocationMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public bool ValidateCertificateUse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public bool ValidateValidityPeriod { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ }
+ [System.FlagsAttribute]
+ public enum CertificateTypes
+ {
+ Chained = 1,
+ SelfSigned = 2,
+ All = 3,
+ }
+ public partial class CertificateValidatedContext : Microsoft.AspNetCore.Authentication.ResultContext<Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions>
+ {
+ public CertificateValidatedContext(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Authentication.AuthenticationScheme scheme, Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions options) : base (default(Microsoft.AspNetCore.Http.HttpContext), default(Microsoft.AspNetCore.Authentication.AuthenticationScheme), default(Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions)) { }
+ public System.Security.Cryptography.X509Certificates.X509Certificate2 ClientCertificate { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ }
+ public static partial class X509Certificate2Extensions
+ {
+ public static bool IsSelfSigned(this System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { throw null; }
+ }
+}
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static partial class CertificateAuthenticationAppBuilderExtensions
+ {
+ public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCertificate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder) { throw null; }
+ public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCertificate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, System.Action<Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions> configureOptions) { throw null; }
+ public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCertificate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme) { throw null; }
+ public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCertificate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, System.Action<Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions> configureOptions) { throw null; }
+ }
+}
diff --git a/src/Security/Authentication/Certificate/samples/Certificate.Sample/Certificate.Sample.csproj b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Certificate.Sample.csproj
new file mode 100644
index 0000000000..2f085e5c41
--- /dev/null
+++ b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Certificate.Sample.csproj
@@ -0,0 +1,21 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.0</TargetFramework>
+ <AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Folder Include="wwwroot\" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Reference Include="Microsoft.AspNetCore" />
+ <Reference Include="Microsoft.AspNetCore.Authentication.Certificate" />
+ <Reference Include="Microsoft.AspNetCore.Diagnostics" />
+ <Reference Include="Microsoft.AspNetCore.Hosting" />
+ <Reference Include="Microsoft.AspNetCore.Mvc" />
+ <Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/Authentication/Certificate/samples/Certificate.Sample/Controllers/HomeController.cs b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Controllers/HomeController.cs
new file mode 100644
index 0000000000..60be48074b
--- /dev/null
+++ b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Controllers/HomeController.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Certificate.Sample.Controllers
+{
+ public class HomeController : Controller
+ {
+ public IActionResult Index()
+ {
+ return View();
+ }
+ }
+}
diff --git a/src/Security/Authentication/Certificate/samples/Certificate.Sample/Program.cs b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Program.cs
new file mode 100644
index 0000000000..1c4a2d2958
--- /dev/null
+++ b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Program.cs
@@ -0,0 +1,26 @@
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Server.Kestrel.Https;
+
+namespace Certificate.Sample
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ BuildWebHost(args).Run();
+ }
+
+ public static IWebHost BuildWebHost(string[] args)
+ => WebHost.CreateDefaultBuilder(args)
+ .UseStartup<Startup>()
+ .ConfigureKestrel(options =>
+ {
+ options.ConfigureHttpsDefaults(opt =>
+ {
+ opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
+ });
+ })
+ .Build();
+ }
+}
diff --git a/src/Security/Authentication/Certificate/samples/Certificate.Sample/Properties/launchSettings.json b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Properties/launchSettings.json
new file mode 100644
index 0000000000..e796cb6c7e
--- /dev/null
+++ b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Properties/launchSettings.json
@@ -0,0 +1,20 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "https://localhost:44331/",
+ "sslPort": 44331
+ }
+ },
+ "profiles": {
+ "Certificate.Sample": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:5001/"
+ }
+ }
+}
diff --git a/src/Security/Authentication/Certificate/samples/Certificate.Sample/Startup.cs b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Startup.cs
new file mode 100644
index 0000000000..14e2702e07
--- /dev/null
+++ b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Startup.cs
@@ -0,0 +1,61 @@
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.Certificate;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.Authorization;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Certificate.Sample
+{
+ public class Startup
+ {
+ // This method gets called by the runtime. Use this method to add services to the container.
+ // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
+ .AddCertificate(options =>
+ {
+ options.Events = 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;
+ }
+ };
+ });
+
+ services.AddAuthorization();
+
+ services.AddMvc(config => { });
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ app.UseRouting();
+
+ app.UseStatusCodePages();
+
+ app.UseAuthentication();
+ app.UseAuthorization();
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapDefaultControllerRoute();
+ });
+ }
+ }
+}
diff --git a/src/Security/Authentication/Certificate/samples/Certificate.Sample/Views/Home/Index.cshtml b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Views/Home/Index.cshtml
new file mode 100644
index 0000000000..5247bfe9c6
--- /dev/null
+++ b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Views/Home/Index.cshtml
@@ -0,0 +1 @@
+<h1>Hello @User.Identity.Name</h1> \ No newline at end of file
diff --git a/src/Security/Authentication/Certificate/src/CertificateAuthenticationDefaults.cs b/src/Security/Authentication/Certificate/src/CertificateAuthenticationDefaults.cs
new file mode 100644
index 0000000000..d085dd3b70
--- /dev/null
+++ b/src/Security/Authentication/Certificate/src/CertificateAuthenticationDefaults.cs
@@ -0,0 +1,16 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Authentication.Certificate
+{
+ /// <summary>
+ /// Default values related to certificate authentication middleware
+ /// </summary>
+ public static class CertificateAuthenticationDefaults
+ {
+ /// <summary>
+ /// The default value used for CertificateAuthenticationOptions.AuthenticationScheme
+ /// </summary>
+ public const string AuthenticationScheme = "Certificate";
+ }
+}
diff --git a/src/Security/Authentication/Certificate/src/CertificateAuthenticationExtensions.cs b/src/Security/Authentication/Certificate/src/CertificateAuthenticationExtensions.cs
new file mode 100644
index 0000000000..d49f2c274b
--- /dev/null
+++ b/src/Security/Authentication/Certificate/src/CertificateAuthenticationExtensions.cs
@@ -0,0 +1,55 @@
+// 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 Microsoft.AspNetCore.Authentication;
+
+using Microsoft.AspNetCore.Authentication.Certificate;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ /// <summary>
+ /// Extension methods to add Certificate authentication capabilities to an HTTP application pipeline.
+ /// </summary>
+ public static class CertificateAuthenticationAppBuilderExtensions
+ {
+ /// <summary>
+ /// Adds certificate authentication.
+ /// </summary>
+ /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
+ /// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
+ public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder)
+ => builder.AddCertificate(CertificateAuthenticationDefaults.AuthenticationScheme);
+
+ /// <summary>
+ /// Adds certificate authentication.
+ /// </summary>
+ /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
+ /// <param name="authenticationScheme"></param>
+ /// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
+ public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder, string authenticationScheme)
+ => builder.AddCertificate(authenticationScheme, configureOptions: null);
+
+ /// <summary>
+ /// Adds certificate authentication.
+ /// </summary>
+ /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
+ /// <param name="configureOptions"></param>
+ /// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
+ public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder, Action<CertificateAuthenticationOptions> configureOptions)
+ => builder.AddCertificate(CertificateAuthenticationDefaults.AuthenticationScheme, configureOptions);
+
+ /// <summary>
+ /// Adds certificate authentication.
+ /// </summary>
+ /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
+ /// <param name="authenticationScheme"></param>
+ /// <param name="configureOptions"></param>
+ /// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
+ public static AuthenticationBuilder AddCertificate(
+ this AuthenticationBuilder builder,
+ string authenticationScheme,
+ Action<CertificateAuthenticationOptions> configureOptions)
+ => builder.AddScheme<CertificateAuthenticationOptions, CertificateAuthenticationHandler>(authenticationScheme, configureOptions);
+ }
+}
diff --git a/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs b/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs
new file mode 100644
index 0000000000..68a7abdde0
--- /dev/null
+++ b/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs
@@ -0,0 +1,235 @@
+// 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.Collections.Generic;
+using System.Security.Claims;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Authentication.Certificate
+{
+ internal class CertificateAuthenticationHandler : AuthenticationHandler<CertificateAuthenticationOptions>
+ {
+ private static readonly Oid ClientCertificateOid = new Oid("1.3.6.1.5.5.7.3.2");
+
+ public CertificateAuthenticationHandler(
+ IOptionsMonitor<CertificateAuthenticationOptions> options,
+ ILoggerFactory logger,
+ UrlEncoder encoder,
+ ISystemClock clock) : base(options, logger, encoder, clock)
+ {
+ }
+
+ /// <summary>
+ /// The handler calls methods on the events which give the application control at certain points where processing is occurring.
+ /// If it is not provided a default instance is supplied which does nothing when the methods are called.
+ /// </summary>
+ protected new CertificateAuthenticationEvents Events
+ {
+ get { return (CertificateAuthenticationEvents)base.Events; }
+ set { base.Events = value; }
+ }
+
+ /// <summary>
+ /// Creates a new instance of the events instance.
+ /// </summary>
+ /// <returns>A new instance of the events instance.</returns>
+ protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new CertificateAuthenticationEvents());
+
+ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
+ {
+ // You only get client certificates over HTTPS
+ if (!Context.Request.IsHttps)
+ {
+ return AuthenticateResult.NoResult();
+ }
+
+ try
+ {
+ var clientCertificate = await Context.Connection.GetClientCertificateAsync();
+
+ // This should never be the case, as cert authentication happens long before ASP.NET kicks in.
+ if (clientCertificate == null)
+ {
+ Logger.NoCertificate();
+ return AuthenticateResult.NoResult();
+ }
+
+ // If we have a self signed cert, and they're not allowed, exit early and not bother with
+ // any other validations.
+ if (clientCertificate.IsSelfSigned() &&
+ !Options.AllowedCertificateTypes.HasFlag(CertificateTypes.SelfSigned))
+ {
+ Logger.CertificateRejected("Self signed", clientCertificate.Subject);
+ return AuthenticateResult.Fail("Options do not allow self signed certificates.");
+ }
+
+ // If we have a chained cert, and they're not allowed, exit early and not bother with
+ // any other validations.
+ if (!clientCertificate.IsSelfSigned() &&
+ !Options.AllowedCertificateTypes.HasFlag(CertificateTypes.Chained))
+ {
+ Logger.CertificateRejected("Chained", clientCertificate.Subject);
+ return AuthenticateResult.Fail("Options do not allow chained certificates.");
+ }
+
+ var chainPolicy = BuildChainPolicy(clientCertificate);
+ var chain = new X509Chain
+ {
+ ChainPolicy = chainPolicy
+ };
+
+ var certificateIsValid = chain.Build(clientCertificate);
+ if (!certificateIsValid)
+ {
+ var chainErrors = new List<string>();
+ foreach (var validationFailure in chain.ChainStatus)
+ {
+ chainErrors.Add($"{validationFailure.Status} {validationFailure.StatusInformation}");
+ }
+ Logger.CertificateFailedValidation(clientCertificate.Subject, chainErrors);
+ return AuthenticateResult.Fail("Client certificate failed validation.");
+ }
+
+ var certificateValidatedContext = new CertificateValidatedContext(Context, Scheme, Options)
+ {
+ ClientCertificate = clientCertificate,
+ Principal = CreatePrincipal(clientCertificate)
+ };
+
+ await Events.CertificateValidated(certificateValidatedContext);
+
+ if (certificateValidatedContext.Result != null)
+ {
+ return certificateValidatedContext.Result;
+ }
+
+ certificateValidatedContext.Success();
+ return certificateValidatedContext.Result;
+ }
+ catch (Exception ex)
+ {
+ var authenticationFailedContext = new CertificateAuthenticationFailedContext(Context, Scheme, Options)
+ {
+ Exception = ex
+ };
+
+ await Events.AuthenticationFailed(authenticationFailedContext);
+
+ if (authenticationFailedContext.Result != null)
+ {
+ return authenticationFailedContext.Result;
+ }
+
+ throw;
+ }
+ }
+
+ protected override Task HandleChallengeAsync(AuthenticationProperties properties)
+ {
+ // Certificate authentication takes place at the connection level. We can't prompt once we're in
+ // user code, so the best thing to do is Forbid, not Challenge.
+ return HandleForbiddenAsync(properties);
+ }
+
+ private X509ChainPolicy BuildChainPolicy(X509Certificate2 certificate)
+ {
+ // Now build the chain validation options.
+ X509RevocationFlag revocationFlag = Options.RevocationFlag;
+ X509RevocationMode revocationMode = Options.RevocationMode;
+
+ if (certificate.IsSelfSigned())
+ {
+ // Turn off chain validation, because we have a self signed certificate.
+ revocationFlag = X509RevocationFlag.EntireChain;
+ revocationMode = X509RevocationMode.NoCheck;
+ }
+
+ var chainPolicy = new X509ChainPolicy
+ {
+ RevocationFlag = revocationFlag,
+ RevocationMode = revocationMode,
+ };
+
+ if (Options.ValidateCertificateUse)
+ {
+ chainPolicy.ApplicationPolicy.Add(ClientCertificateOid);
+ }
+
+ if (certificate.IsSelfSigned())
+ {
+ chainPolicy.VerificationFlags |= X509VerificationFlags.AllowUnknownCertificateAuthority;
+ chainPolicy.VerificationFlags |= X509VerificationFlags.IgnoreEndRevocationUnknown;
+ chainPolicy.ExtraStore.Add(certificate);
+ }
+
+ if (!Options.ValidateValidityPeriod)
+ {
+ chainPolicy.VerificationFlags |= X509VerificationFlags.IgnoreNotTimeValid;
+ }
+
+ return chainPolicy;
+ }
+
+ private ClaimsPrincipal CreatePrincipal(X509Certificate2 certificate)
+ {
+ var claims = new List<Claim>();
+
+ var issuer = certificate.Issuer;
+ claims.Add(new Claim("issuer", issuer, ClaimValueTypes.String, Options.ClaimsIssuer));
+
+ var thumbprint = certificate.Thumbprint;
+ claims.Add(new Claim(ClaimTypes.Thumbprint, thumbprint, ClaimValueTypes.Base64Binary, Options.ClaimsIssuer));
+
+ var value = certificate.SubjectName.Name;
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ claims.Add(new Claim(ClaimTypes.X500DistinguishedName, value, ClaimValueTypes.String, Options.ClaimsIssuer));
+ }
+
+ value = certificate.SerialNumber;
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ claims.Add(new Claim(ClaimTypes.SerialNumber, value, ClaimValueTypes.String, Options.ClaimsIssuer));
+ }
+
+ value = certificate.GetNameInfo(X509NameType.DnsName, false);
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ claims.Add(new Claim(ClaimTypes.Dns, value, ClaimValueTypes.String, Options.ClaimsIssuer));
+ }
+
+ value = certificate.GetNameInfo(X509NameType.SimpleName, false);
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ claims.Add(new Claim(ClaimTypes.Name, value, ClaimValueTypes.String, Options.ClaimsIssuer));
+ }
+
+ value = certificate.GetNameInfo(X509NameType.EmailName, false);
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ claims.Add(new Claim(ClaimTypes.Email, value, ClaimValueTypes.String, Options.ClaimsIssuer));
+ }
+
+ value = certificate.GetNameInfo(X509NameType.UpnName, false);
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ claims.Add(new Claim(ClaimTypes.Upn, value, ClaimValueTypes.String, Options.ClaimsIssuer));
+ }
+
+ value = certificate.GetNameInfo(X509NameType.UrlName, false);
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ claims.Add(new Claim(ClaimTypes.Uri, value, ClaimValueTypes.String, Options.ClaimsIssuer));
+ }
+
+ var identity = new ClaimsIdentity(claims, CertificateAuthenticationDefaults.AuthenticationScheme);
+ return new ClaimsPrincipal(identity);
+ }
+ }
+}
diff --git a/src/Security/Authentication/Certificate/src/CertificateAuthenticationOptions.cs b/src/Security/Authentication/Certificate/src/CertificateAuthenticationOptions.cs
new file mode 100644
index 0000000000..1b8eebfa6f
--- /dev/null
+++ b/src/Security/Authentication/Certificate/src/CertificateAuthenticationOptions.cs
@@ -0,0 +1,53 @@
+// 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.Security.Cryptography.X509Certificates;
+
+namespace Microsoft.AspNetCore.Authentication.Certificate
+{
+ /// <summary>
+ /// Options used to configure certificate authentication.
+ /// </summary>
+ public class CertificateAuthenticationOptions : AuthenticationSchemeOptions
+ {
+ /// <summary>
+ /// Value indicating the types of certificates accepted by the authentication middleware.
+ /// </summary>
+ public CertificateTypes AllowedCertificateTypes { get; set; } = CertificateTypes.Chained;
+
+ /// <summary>
+ /// Flag indicating whether the client certificate must be suitable for client
+ /// authentication, either via the Client Authentication EKU, or having no EKUs
+ /// at all. If the certificate chains to a root CA all certificates in the chain must be validate
+ /// for the client authentication EKU.
+ /// </summary>
+ public bool ValidateCertificateUse { get; set; } = true;
+
+ /// <summary>
+ /// Flag indicating whether the client certificate validity period should be checked.
+ /// </summary>
+ public bool ValidateValidityPeriod { get; set; } = true;
+
+ /// <summary>
+ /// Specifies which X509 certificates in the chain should be checked for revocation.
+ /// </summary>
+ public X509RevocationFlag RevocationFlag { get; set; } = X509RevocationFlag.ExcludeRoot;
+
+ /// <summary>
+ /// Specifies conditions under which verification of certificates in the X509 chain should be conducted.
+ /// </summary>
+ public X509RevocationMode RevocationMode { get; set; } = X509RevocationMode.Online;
+
+ /// <summary>
+ /// The object provided by the application to process events raised by the certificate authentication middleware.
+ /// The application may implement the interface fully, or it may create an instance of CertificateAuthenticationEvents
+ /// and assign delegates only to the events it wants to process.
+ /// </summary>
+ public new CertificateAuthenticationEvents Events
+ {
+ get { return (CertificateAuthenticationEvents)base.Events; }
+
+ set { base.Events = value; }
+ }
+ }
+}
diff --git a/src/Security/Authentication/Certificate/src/CertificateTypes.cs b/src/Security/Authentication/Certificate/src/CertificateTypes.cs
new file mode 100644
index 0000000000..ab238ce3e6
--- /dev/null
+++ b/src/Security/Authentication/Certificate/src/CertificateTypes.cs
@@ -0,0 +1,29 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Authentication.Certificate
+{
+ /// <summary>
+ /// Enum representing certificate types.
+ /// </summary>
+ [Flags]
+ public enum CertificateTypes
+ {
+ /// <summary>
+ /// Chained certificates.
+ /// </summary>
+ Chained = 1,
+
+ /// <summary>
+ /// SelfSigned certificates.
+ /// </summary>
+ SelfSigned = 2,
+
+ /// <summary>
+ /// All certificates.
+ /// </summary>
+ All = Chained | SelfSigned
+ }
+}
diff --git a/src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationEvents.cs b/src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationEvents.cs
new file mode 100644
index 0000000000..bf6e559e5f
--- /dev/null
+++ b/src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationEvents.cs
@@ -0,0 +1,45 @@
+// 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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authentication.Certificate
+{
+ /// <summary>
+ /// This default implementation of the IBasicAuthenticationEvents may be used if the
+ /// application only needs to override a few of the interface methods.
+ /// This may be used as a base class or may be instantiated directly.
+ /// </summary>
+ public class CertificateAuthenticationEvents
+ {
+ /// <summary>
+ /// A delegate assigned to this property will be invoked when the authentication fails.
+ /// </summary>
+ public Func<CertificateAuthenticationFailedContext, Task> OnAuthenticationFailed { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// A delegate assigned to this property will be invoked when a certificate has passed basic validation, but where custom validation may be needed.
+ /// </summary>
+ /// <remarks>
+ /// You must provide a delegate for this property for authentication to occur.
+ /// In your delegate you should construct an authentication principal from the user details,
+ /// attach it to the context.Principal property and finally call context.Success();
+ /// </remarks>
+ public Func<CertificateValidatedContext, Task> OnCertificateValidated { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked when a certificate fails authentication.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <returns></returns>
+ public virtual Task AuthenticationFailed(CertificateAuthenticationFailedContext context) => OnAuthenticationFailed(context);
+
+ /// <summary>
+ /// Invoked after a certificate has been validated
+ /// </summary>
+ /// <param name="context"></param>
+ /// <returns></returns>
+ public virtual Task CertificateValidated(CertificateValidatedContext context) => OnCertificateValidated(context);
+ }
+}
diff --git a/src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationFailedContext.cs b/src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationFailedContext.cs
new file mode 100644
index 0000000000..9742a149c2
--- /dev/null
+++ b/src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationFailedContext.cs
@@ -0,0 +1,33 @@
+// 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 Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication.Certificate
+{
+ /// <summary>
+ /// Context used when a failure occurs.
+ /// </summary>
+ public class CertificateAuthenticationFailedContext : ResultContext<CertificateAuthenticationOptions>
+ {
+ /// <summary>
+ /// Constructor.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="scheme"></param>
+ /// <param name="options"></param>
+ public CertificateAuthenticationFailedContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ CertificateAuthenticationOptions options)
+ : base(context, scheme, options)
+ {
+ }
+
+ /// <summary>
+ /// The exception.
+ /// </summary>
+ public Exception Exception { get; set; }
+ }
+}
diff --git a/src/Security/Authentication/Certificate/src/Events/CertificateValidatedContext.cs b/src/Security/Authentication/Certificate/src/Events/CertificateValidatedContext.cs
new file mode 100644
index 0000000000..9a3870b3bd
--- /dev/null
+++ b/src/Security/Authentication/Certificate/src/Events/CertificateValidatedContext.cs
@@ -0,0 +1,33 @@
+// 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.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication.Certificate
+{
+ /// <summary>
+ /// Context used when certificates are being validated.
+ /// </summary>
+ public class CertificateValidatedContext : ResultContext<CertificateAuthenticationOptions>
+ {
+ /// <summary>
+ /// Creates a new instance of <see cref="CertificateValidatedContext"/>.
+ /// </summary>
+ /// <param name="context">The HttpContext the validate context applies too.</param>
+ /// <param name="scheme">The scheme used when the Certificate Authentication handler was registered.</param>
+ /// <param name="options">The <see cref="CertificateAuthenticationOptions"/>.</param>
+ public CertificateValidatedContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ CertificateAuthenticationOptions options)
+ : base(context, scheme, options)
+ {
+ }
+
+ /// <summary>
+ /// The certificate to validate.
+ /// </summary>
+ public X509Certificate2 ClientCertificate { get; set; }
+ }
+}
diff --git a/src/Security/Authentication/Certificate/src/LoggingExtensions.cs b/src/Security/Authentication/Certificate/src/LoggingExtensions.cs
new file mode 100644
index 0000000000..2219a349b6
--- /dev/null
+++ b/src/Security/Authentication/Certificate/src/LoggingExtensions.cs
@@ -0,0 +1,48 @@
+// 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.Collections.Generic;
+
+namespace Microsoft.Extensions.Logging
+{
+ internal static class LoggingExtensions
+ {
+ private static Action<ILogger, Exception> _noCertificate;
+ private static Action<ILogger, string, string, Exception> _certRejected;
+ private static Action<ILogger, string, string, Exception> _certFailedValidation;
+
+ static LoggingExtensions()
+ {
+ _noCertificate = LoggerMessage.Define(
+ eventId: new EventId(0, "NoCertificate"),
+ logLevel: LogLevel.Debug,
+ formatString: "No client certificate found.");
+
+ _certRejected = LoggerMessage.Define<string, string>(
+ eventId: new EventId(1, "CertificateRejected"),
+ logLevel: LogLevel.Warning,
+ formatString: "{CertificateType} certificate rejected, subject was {Subject}.");
+
+ _certFailedValidation = LoggerMessage.Define<string, string>(
+ eventId: new EventId(2, "CertificateFailedValidation"),
+ logLevel: LogLevel.Warning,
+ formatString: "Certificate validation failed, subject was {Subject}." + Environment.NewLine + "{ChainErrors}");
+ }
+
+ public static void NoCertificate(this ILogger logger)
+ {
+ _noCertificate(logger, null);
+ }
+
+ public static void CertificateRejected(this ILogger logger, string certificateType, string subject)
+ {
+ _certRejected(logger, certificateType, subject, null);
+ }
+
+ public static void CertificateFailedValidation(this ILogger logger, string subject, IEnumerable<string> chainedErrors)
+ {
+ _certFailedValidation(logger, subject, String.Join(Environment.NewLine, chainedErrors), null);
+ }
+ }
+}
diff --git a/src/Security/Authentication/Certificate/src/Microsoft.AspNetCore.Authentication.Certificate.csproj b/src/Security/Authentication/Certificate/src/Microsoft.AspNetCore.Authentication.Certificate.csproj
new file mode 100644
index 0000000000..1795d68877
--- /dev/null
+++ b/src/Security/Authentication/Certificate/src/Microsoft.AspNetCore.Authentication.Certificate.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core middleware that enables an application to support certificate authentication.</Description>
+ <TargetFramework>netcoreapp3.0</TargetFramework>
+ <DefineConstants>$(DefineConstants);SECURITY</DefineConstants>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;authentication;security;x509;certificate</PackageTags>
+ <IsShippingPackage>true</IsShippingPackage>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Reference Include="Microsoft.AspNetCore.Authentication" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/Authentication/Certificate/src/README-IISConfig.png b/src/Security/Authentication/Certificate/src/README-IISConfig.png
new file mode 100644
index 0000000000..3af15e9d06
--- /dev/null
+++ b/src/Security/Authentication/Certificate/src/README-IISConfig.png
Binary files differ
diff --git a/src/Security/Authentication/Certificate/src/README.md b/src/Security/Authentication/Certificate/src/README.md
new file mode 100644
index 0000000000..542131fdf1
--- /dev/null
+++ b/src/Security/Authentication/Certificate/src/README.md
@@ -0,0 +1,234 @@
+# Microsoft.AspNetCore.Authentication.Certificate
+
+This project sort of contains an implementation of [Certificate Authentication](https://tools.ietf.org/html/rfc5246#section-7.4.4) for ASP.NET Core.
+Certificate authentication happens at the TLS level, long before it ever gets to ASP.NET Core, so, more accurately this is an authentication handler
+that validates the certificate and then gives you an event where you can resolve that certificate to a ClaimsPrincipal.
+
+You **must** [configure your host](#hostConfiguration) for certificate authentication, be it IIS, Kestrel, Azure Web Applications or whatever else you're using.
+
+## Getting started
+
+First acquire an HTTPS certificate, apply it and then [configure your host](#hostConfiguration) to require certificates.
+
+In your web application add a reference to the package, then in the `ConfigureServices` method in `startup.cs` call
+`app.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).UseCertificateAuthentication(...);` with your options,
+providing a delegate for `OnValidateCertificate` to validate the client certificate sent with requests and turn that information
+into an `ClaimsPrincipal`, set it on the `context.Principal` property and call `context.Success()`.
+
+If you change your scheme name in the options for the authentication handler you need to change the scheme name in
+`AddAuthentication()` to ensure it's used on every request which ends in an endpoint that requires authorization.
+
+If authentication fails this handler will return a `403 (Forbidden)` response rather a `401 (Unauthorized)` as you
+might expect - this is because the authentication should happen during the initial TLS connection - by the time it
+reaches the handler it's too late, and there's no way to actually upgrade the connection from an anonymous connection
+to one with a certificate.
+
+You must also add `app.UseAuthentication();` in the `Configure` method, otherwise nothing will ever get called.
+
+For example;
+
+```c#
+public void ConfigureServices(IServiceCollection services)
+{
+ services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
+ .AddCertificate();
+ // All the other service configuration.
+}
+
+public void Configure(IApplicationBuilder app, IHostingEnvironment env)
+{
+ app.UseAuthentication();
+
+ // All the other app configuration.
+}
+```
+
+In the sample above you can see the default way to add certificate authentication. The handler will construct a user principal using the common certificate properties for you.
+
+## Configuring Certificate Validation
+
+The `CertificateAuthenticationOptions` handler has some built in validations that are the minimium validations you should perform on
+a certificate. Each of these settings are turned on by default.
+
+### ValidateCertificateChain
+
+This check validates that the issuer for the certificate is trusted by the application host OS. If
+you are going to accept self-signed certificates you must disable this check.
+
+### ValidateCertificateUse
+
+This check validates that the certificate presented by the client has the Client Authentication
+extended key use, or no EKUs at all (as the specifications say if no EKU is specified then all EKUs
+are valid).
+
+### ValidateValidityPeriod
+
+This check validates that the certificate is within its validity period. As the handler runs on every
+request this ensures that a certificate that was valid when it was presented has not expired during
+its current session.
+
+### RevocationFlag
+
+A flag which specifies which certificates in the chain are checked for revocation.
+
+Revocation checks are only performed when the certificate is chained to a root certificate.
+
+### RevocationMode
+
+A flag which specifies how revocation checks are performed.
+Specifying an on-line check can result in a long delay while the certificate authority is contacted.
+
+Revocation checks are only performed when the certificate is chained to a root certificate.
+
+### Can I configure my application to require a certificate only on certain paths?
+
+Not possible, remember the certificate exchange is done that the start of the HTTPS conversation,
+it's done by the host, not the application. Kestrel, IIS, Azure Web Apps don't have any configuration for
+this sort of thing.
+
+# Handler events
+
+The handler has two events, `OnAuthenticationFailed()`, which is called if an exception happens during authentication and allows you to react, and `OnValidateCertificate()` which is
+called after certificate has been validated, passed validation, abut before the default principal has been created. This allows you to perform your own validation, for example
+checking if the certificate is one your services knows about, and to construct your own principal. For example,
+
+```c#
+services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
+ .AddCertificate(options =>
+ {
+ options.Events = new CertificateAuthenticationEvents
+ {
+ OnValidateCertificate = 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;
+ }
+ };
+ });
+```
+
+If you find the inbound certificate doesn't meet your extra validation call `context.Fail("failure Reason")` with a failure reason.
+
+For real functionality you will probably want to call a service registered in DI which talks to a database or other type of
+user store. You can grab your service by using the context passed into your delegates, like so
+
+```c#
+services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
+ .AddCertificate(options =>
+ {
+ options.Events = new CertificateAuthenticationEvents
+ {
+ OnCertificateValidated = context =>
+ {
+ var validationService =
+ context.HttpContext.RequestServices.GetService<ICertificateValidationService>();
+
+ if (validationService.ValidateCertificate(context.ClientCertificate))
+ {
+ 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;
+ }
+ };
+ });
+```
+Note that conceptually the validation of the certification is an authorization concern, and putting a check on, for example, an issuer or thumbprint in an authorization policy rather
+than inside OnCertificateValidated() is perfectly acceptable.
+
+## <a name="hostConfiguration"></a>Configuring your host to require certificates
+
+### Kestrel
+
+In program.cs configure `UseKestrel()` as follows.
+
+```c#
+public static IWebHost BuildWebHost(string[] args)
+ => WebHost.CreateDefaultBuilder(args)
+ .UseStartup<Startup>()
+ .ConfigureKestrel(options =>
+ {
+ options.ConfigureHttpsDefaults(opt =>
+ {
+ opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
+ });
+ })
+ .Build();
+```
+You must set the `ClientCertificateValidation` delegate to `CertificateValidator.DisableChannelValidation` in order to stop Kestrel using the default OS certificate validation routine and,
+instead, letting the authentication handler perform the validation.
+
+### IIS
+
+In the IIS Manager
+
+1. Select your Site in the Connections tab.
+2. Double click the SSL Settings in the Features View window.
+3. Check the `Require SSL` Check Box and select the `Require` radio button under Client Certificates.
+
+![Client Certificate Settings in IIS](README-IISConfig.png "Client Certificate Settings in IIS")
+
+### Azure
+
+See the [Azure documentation](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth)
+to configure Azure Web Apps then add the following to your application startup method, `Configure(IApplicationBuilder app)` add the
+following line before the call to `app.UseAuthentication();`
+
+```c#
+app.UseCertificateHeaderForwarding();
+```
+
+### Random custom web proxies
+
+If you're using a proxy which isn't IIS or Azure's Web Apps Application Request Routing you will need to configure your proxy
+to forward the certificate it received in an HTTP header.
+In your application startup method, `Configure(IApplicationBuilder app)`, add the
+following line before the call to `app.UseAuthentication();`
+
+```c#
+app.UseCertificateForwarding();
+```
+
+You will also need to configure the Certificate Forwarding middleware to specify the header name.
+In your service configuration method, `ConfigureServices(IServiceCollection services)` add
+the following code to configure the header the forwarding middleware will build a certificate from;
+
+```c#
+services.AddCertificateForwarding(options =>
+{
+ options.CertificateHeader = "YOUR_CUSTOM_HEADER_NAME";
+});
+```
+
+Finally, if your proxy is doing something weird to pass the header on, rather than base 64 encoding it
+(looking at you nginx (╯°□°)╯︵ ┻━┻) you can override the converter option to be a func that will
+perform the optional conversion, for example
+
+```c#
+services.AddCertificateForwarding(options =>
+{
+ options.CertificateHeader = "YOUR_CUSTOM_HEADER_NAME";
+ options.HeaderConverter = (headerValue) =>
+ {
+ var clientCertificate =
+ /* some weird conversion logic to create an X509Certificate2 */
+ return clientCertificate;
+ }
+});
+```
+
diff --git a/src/Security/Authentication/Certificate/src/X509CertificateExtensions.cs b/src/Security/Authentication/Certificate/src/X509CertificateExtensions.cs
new file mode 100644
index 0000000000..de8f0d3df2
--- /dev/null
+++ b/src/Security/Authentication/Certificate/src/X509CertificateExtensions.cs
@@ -0,0 +1,27 @@
+// 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.Linq;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Microsoft.AspNetCore.Authentication.Certificate
+{
+ /// <summary>
+ /// Extension methods for <see cref="X509Certificate2"/>.
+ /// </summary>
+ public static class X509Certificate2Extensions
+ {
+ /// <summary>
+ /// Determines if the certificate is self signed.
+ /// </summary>
+ /// <param name="certificate">The <see cref="X509Certificate2"/>.</param>
+ /// <returns>True if the certificate is self signed.</returns>
+ public static bool IsSelfSigned(this X509Certificate2 certificate)
+ {
+ Span<byte> subject = certificate.SubjectName.RawData;
+ Span<byte> issuer = certificate.IssuerName.RawData;
+ return subject.SequenceEqual(issuer);
+ }
+ }
+}
diff --git a/src/Security/Authentication/test/CertificateTests.cs b/src/Security/Authentication/test/CertificateTests.cs
new file mode 100644
index 0000000000..a5a2d0294d
--- /dev/null
+++ b/src/Security/Authentication/test/CertificateTests.cs
@@ -0,0 +1,628 @@
+// Copyright (c) Barry Dorrans. 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.Security.Claims;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.Certificate.Test
+{
+ public class ClientCertificateAuthenticationTests
+ {
+
+ [Fact]
+ public async Task VerifySchemeDefaults()
+ {
+ var services = new ServiceCollection();
+ 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()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ AllowedCertificateTypes = CertificateTypes.SelfSigned,
+ Events = sucessfulValidationEvents
+ },
+ Certificates.SelfSignedValidWithClientEku);
+
+ var response = await server.CreateClient().GetAsync("https://example.com/");
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task VerifyValidSelfSignedWithNoEkuAuthenticates()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ AllowedCertificateTypes = CertificateTypes.SelfSigned,
+ Events = sucessfulValidationEvents
+ },
+ Certificates.SelfSignedValidWithNoEku);
+
+ var response = await server.CreateClient().GetAsync("https://example.com/");
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task VerifyValidSelfSignedWithClientEkuFailsWhenSelfSignedCertsNotAllowed()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ AllowedCertificateTypes = CertificateTypes.Chained
+ },
+ Certificates.SelfSignedValidWithClientEku);
+
+ var response = await server.CreateClient().GetAsync("https://example.com/");
+ Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task VerifyValidSelfSignedWithNoEkuFailsWhenSelfSignedCertsNotAllowed()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ AllowedCertificateTypes = CertificateTypes.Chained,
+ Events = sucessfulValidationEvents
+ },
+ Certificates.SelfSignedValidWithNoEku);
+
+ var response = await server.CreateClient().GetAsync("https://example.com/");
+ Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task VerifyValidSelfSignedWithServerFailsEvenIfSelfSignedCertsAreAllowed()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ AllowedCertificateTypes = CertificateTypes.SelfSigned,
+ Events = sucessfulValidationEvents
+ },
+ Certificates.SelfSignedValidWithServerEku);
+
+ var response = await server.CreateClient().GetAsync("https://example.com/");
+ Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task VerifyValidSelfSignedWithServerPassesWhenSelfSignedCertsAreAllowedAndPurposeValidationIsOff()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ AllowedCertificateTypes = CertificateTypes.SelfSigned,
+ ValidateCertificateUse = false,
+ Events = sucessfulValidationEvents
+ },
+ Certificates.SelfSignedValidWithServerEku);
+
+ var response = await server.CreateClient().GetAsync("https://example.com/");
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task VerifyValidSelfSignedWithServerFailsPurposeValidationIsOffButSelfSignedCertsAreNotAllowed()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ AllowedCertificateTypes = CertificateTypes.Chained,
+ ValidateCertificateUse = false,
+ Events = sucessfulValidationEvents
+ },
+ Certificates.SelfSignedValidWithServerEku);
+
+ var response = await server.CreateClient().GetAsync("https://example.com/");
+ Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task VerifyExpiredSelfSignedFails()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ AllowedCertificateTypes = CertificateTypes.SelfSigned,
+ ValidateCertificateUse = false,
+ Events = sucessfulValidationEvents
+ },
+ Certificates.SelfSignedExpired);
+
+ var response = await server.CreateClient().GetAsync("https://example.com/");
+ Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task VerifyExpiredSelfSignedPassesIfDateRangeValidationIsDisabled()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ AllowedCertificateTypes = CertificateTypes.SelfSigned,
+ ValidateValidityPeriod = false,
+ Events = sucessfulValidationEvents
+ },
+ Certificates.SelfSignedExpired);
+
+ var response = await server.CreateClient().GetAsync("https://example.com/");
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task VerifyNotYetValidSelfSignedFails()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ AllowedCertificateTypes = CertificateTypes.SelfSigned,
+ ValidateCertificateUse = false,
+ Events = sucessfulValidationEvents
+ },
+ Certificates.SelfSignedNotYetValid);
+
+ var response = await server.CreateClient().GetAsync("https://example.com/");
+ Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task VerifyNotYetValidSelfSignedPassesIfDateRangeValidationIsDisabled()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ AllowedCertificateTypes = CertificateTypes.SelfSigned,
+ ValidateValidityPeriod = false,
+ Events = sucessfulValidationEvents
+ },
+ Certificates.SelfSignedNotYetValid);
+
+ var response = await server.CreateClient().GetAsync("https://example.com/");
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task VerifyFailingInTheValidationEventReturnsForbidden()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ ValidateCertificateUse = false,
+ Events = failedValidationEvents
+ },
+ Certificates.SelfSignedValidWithServerEku);
+
+ var response = await server.CreateClient().GetAsync("https://example.com/");
+ Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task DoingNothingInTheValidationEventReturnsOK()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ AllowedCertificateTypes = CertificateTypes.SelfSigned,
+ ValidateCertificateUse = false,
+ Events = unprocessedValidationEvents
+ },
+ Certificates.SelfSignedValidWithServerEku);
+
+ var response = await server.CreateClient().GetAsync("https://example.com/");
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task VerifyNotSendingACertificateEndsUpInForbidden()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ Events = sucessfulValidationEvents
+ });
+
+ var response = await server.CreateClient().GetAsync("https://example.com/");
+ Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task VerifyHeaderIsUsedIfCertIsNotPresent()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ AllowedCertificateTypes = CertificateTypes.SelfSigned,
+ Events = sucessfulValidationEvents
+ },
+ wireUpHeaderMiddleware : true);
+
+ 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()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ Events = sucessfulValidationEvents
+ },
+ wireUpHeaderMiddleware: true);
+
+ 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()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ AllowedCertificateTypes = CertificateTypes.SelfSigned,
+ Events = sucessfulValidationEvents
+ },
+ wireUpHeaderMiddleware: true,
+ headerName: "X-ARR-ClientCert");
+
+ 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()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ Events = sucessfulValidationEvents
+ },
+ wireUpHeaderMiddleware: true,
+ headerName: "X-ARR-ClientCert");
+
+ 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()
+ {
+ var server = CreateServer(
+ new CertificateAuthenticationOptions
+ {
+ AllowedCertificateTypes = CertificateTypes.SelfSigned
+ },
+ Certificates.SelfSignedValidWithNoEku);
+
+ 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.Count() > 0)
+ {
+ 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.Count() > 0)
+ {
+ 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.Count() > 0)
+ {
+ 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.Count() > 0)
+ {
+ 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.Count() > 0)
+ {
+ 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.Count() > 0)
+ {
+ 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.Count() > 0)
+ {
+ Assert.Single(actual);
+ Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UrlName, false), actual.First().Value);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task VerifyValidationEventPrincipalIsPropogated()
+ {
+ const string Expected = "John Doe";
+
+ var server = CreateServer(
+ 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);
+
+ 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"));
+ }
+
+ private static TestServer CreateServer(
+ CertificateAuthenticationOptions configureOptions,
+ X509Certificate2 clientCertificate = null,
+ Func<HttpContext, bool> handler = null,
+ Uri baseAddress = null,
+ bool wireUpHeaderMiddleware = false,
+ string headerName = "")
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.Use((context, next) =>
+ {
+ if (clientCertificate != null)
+ {
+ context.Connection.ClientCertificate = clientCertificate;
+ }
+ return next();
+ });
+
+
+ if (wireUpHeaderMiddleware)
+ {
+ app.UseCertificateForwarding();
+ }
+
+ app.UseAuthentication();
+
+ app.Use(async (context, next) =>
+ {
+ 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 =>
+ {
+ if (configureOptions != null)
+ {
+ services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(options =>
+ {
+ options.AllowedCertificateTypes = configureOptions.AllowedCertificateTypes;
+ options.Events = configureOptions.Events;
+ options.ValidateCertificateUse = configureOptions.ValidateCertificateUse;
+ options.RevocationFlag = options.RevocationFlag;
+ options.RevocationMode = options.RevocationMode;
+ options.ValidateValidityPeriod = configureOptions.ValidateValidityPeriod;
+ });
+ }
+ else
+ {
+ services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate();
+ }
+
+ if (wireUpHeaderMiddleware && !string.IsNullOrEmpty(headerName))
+ {
+ services.AddCertificateForwarding(options =>
+ {
+ options.CertificateHeader = headerName;
+ });
+ }
+ });
+
+ var server = new TestServer(builder)
+ {
+ BaseAddress = baseAddress
+ };
+
+ return server;
+ }
+
+ private CertificateAuthenticationEvents sucessfulValidationEvents = 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 CertificateAuthenticationEvents failedValidationEvents = new CertificateAuthenticationEvents()
+ {
+ OnCertificateValidated = context =>
+ {
+ context.Fail("Not validated");
+ return Task.CompletedTask;
+ }
+ };
+
+ private CertificateAuthenticationEvents unprocessedValidationEvents = new CertificateAuthenticationEvents()
+ {
+ OnCertificateValidated = context =>
+ {
+ return Task.CompletedTask;
+ }
+ };
+
+ private static class Certificates
+ {
+ public static X509Certificate2 SelfSignedValidWithClientEku { get; private set; } =
+ new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedClientEkuCertificate.cer"));
+
+ public static X509Certificate2 SelfSignedValidWithNoEku { get; private set; } =
+ new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedNoEkuCertificate.cer"));
+
+ public static X509Certificate2 SelfSignedValidWithServerEku { get; private set; } =
+ new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedServerEkuCertificate.cer"));
+
+ public static X509Certificate2 SelfSignedNotYetValid { get; private set; } =
+ new X509Certificate2(GetFullyQualifiedFilePath("selfSignedNoEkuCertificateNotValidYet.cer"));
+
+ public static X509Certificate2 SelfSignedExpired { get; private set; } =
+ new X509Certificate2(GetFullyQualifiedFilePath("selfSignedNoEkuCertificateExpired.cer"));
+
+ private static string GetFullyQualifiedFilePath(string filename)
+ {
+ var filePath = Path.Combine(AppContext.BaseDirectory, filename);
+ if (!File.Exists(filePath))
+ {
+ throw new FileNotFoundException(filePath);
+ }
+ return filePath;
+ }
+ }
+ }
+}
diff --git a/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj b/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj
index f575bfa0cf..fd033a9169 100644
--- a/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj
+++ b/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj
@@ -22,6 +22,10 @@
<Content Include="WsFederation\ValidToken.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="$(SharedSourceRoot)test\Certificates\*.cer">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+ </Content>
</ItemGroup>
<ItemGroup>
@@ -34,6 +38,7 @@
</ItemGroup>
<ItemGroup>
+ <Reference Include="Microsoft.AspNetCore.Authentication.Certificate" />
<Reference Include="Microsoft.AspNetCore.Authentication.Cookies" />
<Reference Include="Microsoft.AspNetCore.Authentication.Facebook" />
<Reference Include="Microsoft.AspNetCore.Authentication.Google" />
@@ -42,6 +47,7 @@
<Reference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
<Reference Include="Microsoft.AspNetCore.Authentication.Twitter" />
<Reference Include="Microsoft.AspNetCore.Authentication.WsFederation" />
+ <Reference Include="Microsoft.AspNetCore.HttpOverrides" />
<Reference Include="Microsoft.AspNetCore.TestHost" />
</ItemGroup>
diff --git a/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateExpired.cer b/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateExpired.cer
new file mode 100644
index 0000000000..81b6326d6f
--- /dev/null
+++ b/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateExpired.cer
Binary files differ
diff --git a/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateNotValidYet.cer b/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateNotValidYet.cer
new file mode 100644
index 0000000000..9c8cf9d71b
--- /dev/null
+++ b/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateNotValidYet.cer
Binary files differ
diff --git a/src/Security/Authentication/test/TestCertificates/validSelfSignedClientEkuCertificate.cer b/src/Security/Authentication/test/TestCertificates/validSelfSignedClientEkuCertificate.cer
new file mode 100644
index 0000000000..db4bb5b90a
--- /dev/null
+++ b/src/Security/Authentication/test/TestCertificates/validSelfSignedClientEkuCertificate.cer
Binary files differ
diff --git a/src/Security/Authentication/test/TestCertificates/validSelfSignedNoEkuCertificate.cer b/src/Security/Authentication/test/TestCertificates/validSelfSignedNoEkuCertificate.cer
new file mode 100644
index 0000000000..2be2a46d79
--- /dev/null
+++ b/src/Security/Authentication/test/TestCertificates/validSelfSignedNoEkuCertificate.cer
Binary files differ
diff --git a/src/Security/Authentication/test/TestCertificates/validSelfSignedServerEkuCertificate.cer b/src/Security/Authentication/test/TestCertificates/validSelfSignedServerEkuCertificate.cer
new file mode 100644
index 0000000000..823000d4a1
--- /dev/null
+++ b/src/Security/Authentication/test/TestCertificates/validSelfSignedServerEkuCertificate.cer
Binary files differ
diff --git a/src/Security/Security.sln b/src/Security/Security.sln
index 0e10f4be5e..1405fe94dd 100644
--- a/src/Security/Security.sln
+++ b/src/Security/Security.sln
@@ -153,6 +153,13 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc", "..\Mvc\Mvc\src\Microsoft.AspNetCore.Mvc.csproj", "{27B5D7B5-75A6-4BE6-BD09-597044D06970}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Core", "..\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj", "{553F8C79-13AF-4993-99C1-D70F2143AD8E}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Certificate", "Certificate", "{4DF524BF-D9A9-46F2-882C-68C48FF5FF33}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Certificate", "Authentication\Certificate\src\Microsoft.AspNetCore.Authentication.Certificate.csproj", "{2B88E3EA-6FBE-4690-A56E-0744FFAC9870}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Certificate.Sample", "Authentication\Certificate\samples\Certificate.Sample\Certificate.Sample.csproj", "{11F3B44F-DE5F-42C4-8EC9-1AA51FB89158}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HeaderPropagation", "..\Middleware\HeaderPropagation\ref\Microsoft.AspNetCore.HeaderPropagation.csproj", "{9F9CBDD0-C8B3-4E79-B2B3-9ADE4AE08AEA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -400,6 +407,18 @@ Global
{553F8C79-13AF-4993-99C1-D70F2143AD8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{553F8C79-13AF-4993-99C1-D70F2143AD8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{553F8C79-13AF-4993-99C1-D70F2143AD8E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2B88E3EA-6FBE-4690-A56E-0744FFAC9870}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2B88E3EA-6FBE-4690-A56E-0744FFAC9870}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2B88E3EA-6FBE-4690-A56E-0744FFAC9870}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2B88E3EA-6FBE-4690-A56E-0744FFAC9870}.Release|Any CPU.Build.0 = Release|Any CPU
+ {11F3B44F-DE5F-42C4-8EC9-1AA51FB89158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {11F3B44F-DE5F-42C4-8EC9-1AA51FB89158}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {11F3B44F-DE5F-42C4-8EC9-1AA51FB89158}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {11F3B44F-DE5F-42C4-8EC9-1AA51FB89158}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9F9CBDD0-C8B3-4E79-B2B3-9ADE4AE08AEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9F9CBDD0-C8B3-4E79-B2B3-9ADE4AE08AEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9F9CBDD0-C8B3-4E79-B2B3-9ADE4AE08AEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9F9CBDD0-C8B3-4E79-B2B3-9ADE4AE08AEA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -476,6 +495,10 @@ Global
{8771B5C8-4B96-4A40-A3FC-8CC7E16D7A82} = {A482E4FD-51C2-4061-8357-1E4757D6CF27}
{27B5D7B5-75A6-4BE6-BD09-597044D06970} = {A3766414-EB5C-40F7-B031-121804ED5D0A}
{553F8C79-13AF-4993-99C1-D70F2143AD8E} = {A3766414-EB5C-40F7-B031-121804ED5D0A}
+ {4DF524BF-D9A9-46F2-882C-68C48FF5FF33} = {79C549BA-2932-450A-B87D-635879361343}
+ {2B88E3EA-6FBE-4690-A56E-0744FFAC9870} = {4DF524BF-D9A9-46F2-882C-68C48FF5FF33}
+ {11F3B44F-DE5F-42C4-8EC9-1AA51FB89158} = {4DF524BF-D9A9-46F2-882C-68C48FF5FF33}
+ {9F9CBDD0-C8B3-4E79-B2B3-9ADE4AE08AEA} = {A3766414-EB5C-40F7-B031-121804ED5D0A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {ABF8089E-43D0-4010-84A7-7A9DCFE49357}
diff --git a/src/Shared/test/Certificates/selfSignedNoEkuCertificateExpired.cer b/src/Shared/test/Certificates/selfSignedNoEkuCertificateExpired.cer
new file mode 100644
index 0000000000..81b6326d6f
--- /dev/null
+++ b/src/Shared/test/Certificates/selfSignedNoEkuCertificateExpired.cer
Binary files differ
diff --git a/src/Shared/test/Certificates/selfSignedNoEkuCertificateNotValidYet.cer b/src/Shared/test/Certificates/selfSignedNoEkuCertificateNotValidYet.cer
new file mode 100644
index 0000000000..9c8cf9d71b
--- /dev/null
+++ b/src/Shared/test/Certificates/selfSignedNoEkuCertificateNotValidYet.cer
Binary files differ
diff --git a/src/Shared/test/Certificates/validSelfSignedClientEkuCertificate.cer b/src/Shared/test/Certificates/validSelfSignedClientEkuCertificate.cer
new file mode 100644
index 0000000000..db4bb5b90a
--- /dev/null
+++ b/src/Shared/test/Certificates/validSelfSignedClientEkuCertificate.cer
Binary files differ
diff --git a/src/Shared/test/Certificates/validSelfSignedNoEkuCertificate.cer b/src/Shared/test/Certificates/validSelfSignedNoEkuCertificate.cer
new file mode 100644
index 0000000000..2be2a46d79
--- /dev/null
+++ b/src/Shared/test/Certificates/validSelfSignedNoEkuCertificate.cer
Binary files differ
diff --git a/src/Shared/test/Certificates/validSelfSignedServerEkuCertificate.cer b/src/Shared/test/Certificates/validSelfSignedServerEkuCertificate.cer
new file mode 100644
index 0000000000..823000d4a1
--- /dev/null
+++ b/src/Shared/test/Certificates/validSelfSignedServerEkuCertificate.cer
Binary files differ