From b75b892eac51be8b2f0eb9dc9b47537fc02001c9 Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Fri, 31 May 2019 22:49:40 -0700 Subject: [PATCH] Add CertificateAuthentication (#9756) --- eng/ProjectReferences.props | 1 + ....AspNetCore.HttpOverrides.netcoreapp3.0.cs | 22 + .../CertificateForwardingBuilderExtensions.cs | 30 + .../src/CertificateForwardingFeature.cs | 51 ++ .../src/CertificateForwardingMiddleware.cs | 66 ++ .../src/CertificateForwardingOptions.cs | 30 + .../CertificateForwardingServiceExtensions.cs | 38 ++ .../HttpOverrides/src/LoggingExtensions.cs | 25 + .../test/CertificateForwardingTest.cs | 222 +++++++ ...soft.AspNetCore.HttpOverrides.Tests.csproj | 3 +- ...pNetCore.Authentication.Certificate.csproj | 10 + ...uthentication.Certificate.netcoreapp3.0.cs | 59 ++ .../Certificate.Sample.csproj | 21 + .../Controllers/HomeController.cs | 12 + .../samples/Certificate.Sample/Program.cs | 26 + .../Properties/launchSettings.json | 20 + .../samples/Certificate.Sample/Startup.cs | 61 ++ .../Views/Home/Index.cshtml | 1 + .../src/CertificateAuthenticationDefaults.cs | 16 + .../CertificateAuthenticationExtensions.cs | 55 ++ .../src/CertificateAuthenticationHandler.cs | 235 +++++++ .../src/CertificateAuthenticationOptions.cs | 53 ++ .../Certificate/src/CertificateTypes.cs | 29 + .../Events/CertificateAuthenticationEvents.cs | 45 ++ .../CertificateAuthenticationFailedContext.cs | 33 + .../src/Events/CertificateValidatedContext.cs | 33 + .../Certificate/src/LoggingExtensions.cs | 48 ++ ...pNetCore.Authentication.Certificate.csproj | 16 + .../Certificate/src/README-IISConfig.png | Bin 0 -> 10168 bytes .../Authentication/Certificate/src/README.md | 234 +++++++ .../src/X509CertificateExtensions.cs | 27 + .../Authentication/test/CertificateTests.cs | 628 ++++++++++++++++++ ...soft.AspNetCore.Authentication.Test.csproj | 6 + .../selfSignedNoEkuCertificateExpired.cer | Bin 0 -> 920 bytes .../selfSignedNoEkuCertificateNotValidYet.cer | Bin 0 -> 932 bytes .../validSelfSignedClientEkuCertificate.cer | Bin 0 -> 928 bytes .../validSelfSignedNoEkuCertificate.cer | Bin 0 -> 930 bytes .../validSelfSignedServerEkuCertificate.cer | Bin 0 -> 928 bytes src/Security/Security.sln | 23 + .../selfSignedNoEkuCertificateExpired.cer | Bin 0 -> 920 bytes .../selfSignedNoEkuCertificateNotValidYet.cer | Bin 0 -> 932 bytes .../validSelfSignedClientEkuCertificate.cer | Bin 0 -> 928 bytes .../validSelfSignedNoEkuCertificate.cer | Bin 0 -> 930 bytes .../validSelfSignedServerEkuCertificate.cer | Bin 0 -> 928 bytes 44 files changed, 2178 insertions(+), 1 deletion(-) create mode 100644 src/Middleware/HttpOverrides/src/CertificateForwardingBuilderExtensions.cs create mode 100644 src/Middleware/HttpOverrides/src/CertificateForwardingFeature.cs create mode 100644 src/Middleware/HttpOverrides/src/CertificateForwardingMiddleware.cs create mode 100644 src/Middleware/HttpOverrides/src/CertificateForwardingOptions.cs create mode 100644 src/Middleware/HttpOverrides/src/CertificateForwardingServiceExtensions.cs create mode 100644 src/Middleware/HttpOverrides/src/LoggingExtensions.cs create mode 100644 src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs create mode 100644 src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.csproj create mode 100644 src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.netcoreapp3.0.cs create mode 100644 src/Security/Authentication/Certificate/samples/Certificate.Sample/Certificate.Sample.csproj create mode 100644 src/Security/Authentication/Certificate/samples/Certificate.Sample/Controllers/HomeController.cs create mode 100644 src/Security/Authentication/Certificate/samples/Certificate.Sample/Program.cs create mode 100644 src/Security/Authentication/Certificate/samples/Certificate.Sample/Properties/launchSettings.json create mode 100644 src/Security/Authentication/Certificate/samples/Certificate.Sample/Startup.cs create mode 100644 src/Security/Authentication/Certificate/samples/Certificate.Sample/Views/Home/Index.cshtml create mode 100644 src/Security/Authentication/Certificate/src/CertificateAuthenticationDefaults.cs create mode 100644 src/Security/Authentication/Certificate/src/CertificateAuthenticationExtensions.cs create mode 100644 src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs create mode 100644 src/Security/Authentication/Certificate/src/CertificateAuthenticationOptions.cs create mode 100644 src/Security/Authentication/Certificate/src/CertificateTypes.cs create mode 100644 src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationEvents.cs create mode 100644 src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationFailedContext.cs create mode 100644 src/Security/Authentication/Certificate/src/Events/CertificateValidatedContext.cs create mode 100644 src/Security/Authentication/Certificate/src/LoggingExtensions.cs create mode 100644 src/Security/Authentication/Certificate/src/Microsoft.AspNetCore.Authentication.Certificate.csproj create mode 100644 src/Security/Authentication/Certificate/src/README-IISConfig.png create mode 100644 src/Security/Authentication/Certificate/src/README.md create mode 100644 src/Security/Authentication/Certificate/src/X509CertificateExtensions.cs create mode 100644 src/Security/Authentication/test/CertificateTests.cs create mode 100644 src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateExpired.cer create mode 100644 src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateNotValidYet.cer create mode 100644 src/Security/Authentication/test/TestCertificates/validSelfSignedClientEkuCertificate.cer create mode 100644 src/Security/Authentication/test/TestCertificates/validSelfSignedNoEkuCertificate.cer create mode 100644 src/Security/Authentication/test/TestCertificates/validSelfSignedServerEkuCertificate.cer create mode 100644 src/Shared/test/Certificates/selfSignedNoEkuCertificateExpired.cer create mode 100644 src/Shared/test/Certificates/selfSignedNoEkuCertificateNotValidYet.cer create mode 100644 src/Shared/test/Certificates/validSelfSignedClientEkuCertificate.cer create mode 100644 src/Shared/test/Certificates/validSelfSignedNoEkuCertificate.cer create mode 100644 src/Shared/test/Certificates/validSelfSignedServerEkuCertificate.cer 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 @@ + 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 options) { } + public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext httpContext) { throw null; } + } + public partial class CertificateForwardingOptions + { + public System.Func 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 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 +{ + /// + /// Extension methods for using certificate fowarding. + /// + public static class CertificateForwardingBuilderExtensions + { + /// + /// Adds a middleware to the pipeline that will look for a certificate in a request header + /// decode it, and updates HttpContext.Connection.ClientCertificate. + /// + /// + /// + public static IApplicationBuilder UseCertificateForwarding(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + } +} 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 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 +{ + /// + /// Middleware that converts a forward header into a client certificate if found. + /// + public class CertificateForwardingMiddleware + { + private readonly RequestDelegate _next; + private readonly CertificateForwardingOptions _options; + private readonly ILogger _logger; + + /// + /// Constructor. + /// + /// + /// + /// + public CertificateForwardingMiddleware( + RequestDelegate next, + ILoggerFactory loggerFactory, + IOptions 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(); + } + + /// + /// Looks for the presence of a header in the request, + /// if found, converts this header to a ClientCertificate set on the connection. + /// + /// The . + /// A . + public Task Invoke(HttpContext httpContext) + { + var header = httpContext.Request.Headers[_options.CertificateHeader]; + if (!StringValues.IsNullOrEmpty(header)) + { + httpContext.Features.Set(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 +{ + /// + /// Used to configure the . + /// + public class CertificateForwardingOptions + { + /// + /// The name of the header containing the client certificate. + /// + /// + /// This defaults to X-Client-Cert + /// + public string CertificateHeader { get; set; } = "X-Client-Cert"; + + /// + /// The function used to convert the header to an instance of . + /// + /// + /// This defaults to a conversion from a base64 encoded string. + /// + public Func 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 +{ + /// + /// Extension methods for using certificate fowarding. + /// + public static class CertificateForwardingServiceExtensions + { + /// + /// Adds certificate forwarding to the specified . + /// + /// The . + /// An action delegate to configure the provided . + /// The so that additional calls can be chained. + public static IServiceCollection AddCertificateForwarding( + this IServiceCollection services, + Action configure) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + services.AddOptions().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 _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>(); + Assert.Throws(() => options.Value); + } + + [Fact] + public void VerifySettingEmptyHeaderOptionThrows() + { + var services = new ServiceCollection() + .AddOptions() + .AddCertificateForwarding(o => o.CertificateHeader = ""); + var options = services.BuildServiceProvider().GetRequiredService>(); + Assert.Throws(() => 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 @@ - + netcoreapp3.0 @@ -8,6 +8,7 @@ + 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 @@ + + + + netcoreapp3.0 + + + + + + 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 OnAuthenticationFailed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public System.Func 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 + { + 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 + { + 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 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 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 @@ + + + + netcoreapp3.0 + OutOfProcess + + + + + + + + + + + + + + + + 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() + .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 @@ +

Hello @User.Identity.Name

\ 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 +{ + /// + /// Default values related to certificate authentication middleware + /// + public static class CertificateAuthenticationDefaults + { + /// + /// The default value used for CertificateAuthenticationOptions.AuthenticationScheme + /// + 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 +{ + /// + /// Extension methods to add Certificate authentication capabilities to an HTTP application pipeline. + /// + public static class CertificateAuthenticationAppBuilderExtensions + { + /// + /// Adds certificate authentication. + /// + /// The . + /// The . + public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder) + => builder.AddCertificate(CertificateAuthenticationDefaults.AuthenticationScheme); + + /// + /// Adds certificate authentication. + /// + /// The . + /// + /// The . + public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder, string authenticationScheme) + => builder.AddCertificate(authenticationScheme, configureOptions: null); + + /// + /// Adds certificate authentication. + /// + /// The . + /// + /// The . + public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddCertificate(CertificateAuthenticationDefaults.AuthenticationScheme, configureOptions); + + /// + /// Adds certificate authentication. + /// + /// The . + /// + /// + /// The . + public static AuthenticationBuilder AddCertificate( + this AuthenticationBuilder builder, + string authenticationScheme, + Action configureOptions) + => builder.AddScheme(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 + { + private static readonly Oid ClientCertificateOid = new Oid("1.3.6.1.5.5.7.3.2"); + + public CertificateAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) : base(options, logger, encoder, clock) + { + } + + /// + /// 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. + /// + protected new CertificateAuthenticationEvents Events + { + get { return (CertificateAuthenticationEvents)base.Events; } + set { base.Events = value; } + } + + /// + /// Creates a new instance of the events instance. + /// + /// A new instance of the events instance. + protected override Task CreateEventsAsync() => Task.FromResult(new CertificateAuthenticationEvents()); + + protected override async Task 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(); + 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(); + + 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 +{ + /// + /// Options used to configure certificate authentication. + /// + public class CertificateAuthenticationOptions : AuthenticationSchemeOptions + { + /// + /// Value indicating the types of certificates accepted by the authentication middleware. + /// + public CertificateTypes AllowedCertificateTypes { get; set; } = CertificateTypes.Chained; + + /// + /// 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. + /// + public bool ValidateCertificateUse { get; set; } = true; + + /// + /// Flag indicating whether the client certificate validity period should be checked. + /// + public bool ValidateValidityPeriod { get; set; } = true; + + /// + /// Specifies which X509 certificates in the chain should be checked for revocation. + /// + public X509RevocationFlag RevocationFlag { get; set; } = X509RevocationFlag.ExcludeRoot; + + /// + /// Specifies conditions under which verification of certificates in the X509 chain should be conducted. + /// + public X509RevocationMode RevocationMode { get; set; } = X509RevocationMode.Online; + + /// + /// 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. + /// + 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 +{ + /// + /// Enum representing certificate types. + /// + [Flags] + public enum CertificateTypes + { + /// + /// Chained certificates. + /// + Chained = 1, + + /// + /// SelfSigned certificates. + /// + SelfSigned = 2, + + /// + /// All certificates. + /// + 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 +{ + /// + /// 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. + /// + public class CertificateAuthenticationEvents + { + /// + /// A delegate assigned to this property will be invoked when the authentication fails. + /// + public Func OnAuthenticationFailed { get; set; } = context => Task.CompletedTask; + + /// + /// A delegate assigned to this property will be invoked when a certificate has passed basic validation, but where custom validation may be needed. + /// + /// + /// 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(); + /// + public Func OnCertificateValidated { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked when a certificate fails authentication. + /// + /// + /// + public virtual Task AuthenticationFailed(CertificateAuthenticationFailedContext context) => OnAuthenticationFailed(context); + + /// + /// Invoked after a certificate has been validated + /// + /// + /// + 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 +{ + /// + /// Context used when a failure occurs. + /// + public class CertificateAuthenticationFailedContext : ResultContext + { + /// + /// Constructor. + /// + /// + /// + /// + public CertificateAuthenticationFailedContext( + HttpContext context, + AuthenticationScheme scheme, + CertificateAuthenticationOptions options) + : base(context, scheme, options) + { + } + + /// + /// The exception. + /// + 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 +{ + /// + /// Context used when certificates are being validated. + /// + public class CertificateValidatedContext : ResultContext + { + /// + /// Creates a new instance of . + /// + /// The HttpContext the validate context applies too. + /// The scheme used when the Certificate Authentication handler was registered. + /// The . + public CertificateValidatedContext( + HttpContext context, + AuthenticationScheme scheme, + CertificateAuthenticationOptions options) + : base(context, scheme, options) + { + } + + /// + /// The certificate to validate. + /// + 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 _noCertificate; + private static Action _certRejected; + private static Action _certFailedValidation; + + static LoggingExtensions() + { + _noCertificate = LoggerMessage.Define( + eventId: new EventId(0, "NoCertificate"), + logLevel: LogLevel.Debug, + formatString: "No client certificate found."); + + _certRejected = LoggerMessage.Define( + eventId: new EventId(1, "CertificateRejected"), + logLevel: LogLevel.Warning, + formatString: "{CertificateType} certificate rejected, subject was {Subject}."); + + _certFailedValidation = LoggerMessage.Define( + 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 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 @@ + + + + ASP.NET Core middleware that enables an application to support certificate authentication. + netcoreapp3.0 + $(DefineConstants);SECURITY + true + aspnetcore;authentication;security;x509;certificate + true + + + + + + + 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 0000000000000000000000000000000000000000..3af15e9d0652fba1a66b1704d7990e68ba98bba3 GIT binary patch literal 10168 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 M%34b03g)5z3t&Zgy#N3J literal 0 HcmV?d00001 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(); + + 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. + +## 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() + .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 +{ + /// + /// Extension methods for . + /// + public static class X509Certificate2Extensions + { + /// + /// Determines if the certificate is self signed. + /// + /// The . + /// True if the certificate is self signed. + public static bool IsSelfSigned(this X509Certificate2 certificate) + { + Span subject = certificate.SubjectName.RawData; + Span 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(); + 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 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(""); + foreach (Claim claim in context.User.Claims) + { + await response.WriteAsync($"{claim.Value}"); + } + await response.WriteAsync(""); + } + 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 @@ PreserveNewest + + PreserveNewest + PreserveNewest + @@ -34,6 +38,7 @@ + @@ -42,6 +47,7 @@ + diff --git a/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateExpired.cer b/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateExpired.cer new file mode 100644 index 0000000000000000000000000000000000000000..81b6326d6f1e2558a910ca33b17b606fbc1b30fe GIT binary patch literal 920 zcmXqLVxD5q#8k6@nTe5!NkICu*1snU-f#8TSI6l2aWN;i};kU$aS%1kNE%gfhINi8$vG2mij4rSryVFvMJ z45UCjW*%YJih|6d)D(r_)SNVh;LP;A)D#0bab80U15-nDBO_x=BeN(V*AmDzgK}wY zeG{V+vWFR28JL?G`56qF7`d357#SJvJ@~SgF;l+z&Fksnv#VITO1P(-l2Oxhv-yAU zgyU^Jr^^Rd+SjfV(eYC#r1Xd$W5i8dm zVUSViye?2`_3V}*cYpUWN&i(dr__~l*K;Sm(Vy?p$;8aaz_>WTz|VjW7>Tm{jEw(T zSeTiZSPW!Ad{q`P0}(b3Z8k<$R(574IExV?&Br3fBH~`e)AjbIq_iPRFjGveT#nNM z%X;K62PQILm@_hLh$#7TT+6DizjpUSzk~O#%Ntyhz8m!a$c$|hO}dRFf^YviJFWV^ z%$arEP7k|MK9m$p5BYogLLa-MMSZ=!jV@o^mOb)H$tS|rF}~gVbdA8*J#xFY-%VSw z>{f36Y(3RC#~$6gsIXRYR%A^;e)8;^Eo+xWuk`lvo!+3isJ=*E_@U~>47Lwv_a4(J z)QdQBXY0SA`zP0)DVT0{SXYMDwWGt)OZMDUEgSn2Y aL5XX_&cCycZ&yeTVh*lOs#HHvnFIi>%3gZ_ literal 0 HcmV?d00001 diff --git a/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateNotValidYet.cer b/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateNotValidYet.cer new file mode 100644 index 0000000000000000000000000000000000000000..9c8cf9d71b2218acdd8c6b208677a0e6aea68ade GIT binary patch literal 932 zcmXqLVqRd-#MH5XnTe5!Nx(Q=>**rqs6JPfj1S))AG&P7%f_kI=F#?@mywa1mBAp{ zP}o3_jf-=VTIb|XIw_2jV$AtP>4s7U5-5UPnJJ}tdHK32sbz*d23%~+p)A}y%pjhU zfdYuf%p>WSU!o9}n3I{J5Sdz{5S*HmrVyN&o|l?pAScdiWN2V&Xl`U=Y-wZ`1>_nb zap~iTCPpP>&oiExw?5;6y@y*rsP4hCpR9U!vn=&!>Z-91&%G$kV)3$joeK(K$8nchON%q+_m)lR| zg%y2U`R<~9vhXU8<0d6j)=VlWK6>tQue0ulzs%vWrJvL{8YI=XRR`a`7jMauU3GiI zq6ZSJ2hOW{CT*5vJ6bw5#wIblRjMpRLL&UD|62V=AI0o1ZV5Wy>f(J|<9=1*%`e_H zYZm_gE~t4iPGIl#e-qE~vv%y;eWQ3oYC_(ZrI|6NTV~2!D3Hqg-?6Mf-A-X{K>o~z zjhFQ^e6@Y~l-ULE+vRPDU%ouA%EI)dRba2<)b3S#WXbIX~X zwSiHae2~K&nBah6&d5-{_~@=-2ZZ&o7V*|7CG8-Gjh3)K(C2f={2^0`Zk6)&a9PB-m^M=(sK2( zqPy;*>kN(^cx`|8jF8m<9>EnQC*@AOWpmHcR#E&rX?AL@Vry{OBZb`9iwC3@PCYJb zAj4GtB7NG)9OYycjpCT^>rOwKc=OuIx$7qAJIX%mxgDjoIr%{FwGAfkmz;2XBbt9n^ eS4DHri(Z%*JA)~$t^O)kHpjKhrL|93J^}zP)?Fw7 literal 0 HcmV?d00001 diff --git a/src/Security/Authentication/test/TestCertificates/validSelfSignedClientEkuCertificate.cer b/src/Security/Authentication/test/TestCertificates/validSelfSignedClientEkuCertificate.cer new file mode 100644 index 0000000000000000000000000000000000000000..db4bb5b90a07ee564e3dbf44db557fb2e3be75ed GIT binary patch literal 928 zcmXqLVxD8r#MH8YnTe5!NkAnob;}(iF{Ft#k4xofJk%G3NZDbVDfv2^2xD%#_l+ynNl1)G|XJ11>h^P!?_;W)M%+ zKpDhi=8*|Y%*jkq2u{sOQwYvX&r3~FaL&n0%_~uG^$sO^iy&LBPn$z}&>h&tTBR$i>ve$jGocyz6dv_dV6PYdl|G+r3iFzmv^8 zpQB3a$374D?>p{@Km6PJd|r31&J@X$)%$8r_Wt$g_Y8j@Jtbssq3q#~qBUzGmUPv= zKC_oW#mztEWp!lYy_->jt~cl8_OQu3>sZxs&dPtG#D4Q7-r1sRr!QaQ{pa-CVWX;Q z?3tJzU$b)4rHqr0{IaS&x*?J6&5ZBM`Ikn&@Dn~O`mc1dsp`{TX&3hOACkHED8cI+ z562vHj#I~YyF}-nUGzjuWYO>Q=20iY+TCXg{d#_c&3p2h?Ppd$4?L3>EK|?R$$nwa zF=akEfB92y7ntz!opx1of0*ZX*Id2FAs11}+AC zz-X1_XJq`(!otkN#9|-};;XWNVxL2sjggg=oteo%79_~WBE}-Z8X9?f=7e6Elak3C z0uOtw{gZeckV6`n*nlC;$l!6{@kZe(F>Fl*7R_m*XL%as*Sjt+uLMt@go}65L{B{g)`laJ@ zZPs0IvRGkvSwk~^wBL{4(?>kYgrh`v-{!E={8?p-0~c;F{Ft#k4xofJk%G3NZDbVDfv2^2xD%#_l+ynNl1)G|XJ11>h^P!?_;W)M%& zKpwL5B&!|2G`BtT!|!*h zdQ-fTFP)k4e?x>gyOZ*ct1q6+THLnd2WMc~<+6xY>Ew`)j1MPUEnPKdRgcTF+mjk< zIo6*s=05gliAPw%wXPa0;6{(vNs+0y(jq#hmnYY;XIX_<^O|QN^B(Oy?kKzobB2?8NZqat;Cbdg^sGG=Jl?fA_iGSe{%c zIJCi(A>7>TNZTH$P5g%D?0((lL#d=IUNMr|bA0 znbRc|T3M}bzjKG;#hc34LQ)E*@15cJCEp`X?0N0E9mh&SA8x#0$A*xDz zn^GO0KaJUC%Ch?9!)BH5JJW4+GA~TLy(EdlBH_tV{R5Zx3bTeSJ#_Wg!%ZufR5yKy foUuix*2(c=(miwK^_wc!9KD*j=ZS{(-gis@(~V)} literal 0 HcmV?d00001 diff --git a/src/Security/Authentication/test/TestCertificates/validSelfSignedServerEkuCertificate.cer b/src/Security/Authentication/test/TestCertificates/validSelfSignedServerEkuCertificate.cer new file mode 100644 index 0000000000000000000000000000000000000000..823000d4a1474df454b8468a7f4052bb64c45460 GIT binary patch literal 928 zcmXqLVxD8r#MH8YnTe5!NuVt5gR29pc88Pv;k^&zPku1qW#iOp^Jx3d%gD&h%3zRY zC~P3e#>F{Ft#k4xofJk%G3NZDbVDfv2^2xD%#_l+ynNl1)G|XJ11>h^P!?_;W)M%+ zKpDhi=8*|Y%*jkq2u{sOQwYvX&r1cdi^@`q6kNSS4dldm4J{2!4b6>=j4h2!qr`cQ zOpv&Ab4L@S5^@kQvNA9?G4eAQG%<29H8CJzWL|eA}+}jtJ7bc)IM5t>-#M6DXX9B%V&SgX;Zzc_I>G^ zaIpaGH!hEj56yd{CckjyiMQt;Puuz=*88|dWr>}2Ou@>1-S13A5BW^4>T9r4P>k7K zcrIwg-_y$*HVP~h{`7vP?uGo>+eOVDdLA&gK6&Q8+lBMH&KPv|7M+_R5MInUd$;Xe zJ8Axx4>J6x6&&l~tvl|x>yhr|xofv=`0;Ar!`0{BY+6vd=gZeAIZ2N%Yx`-YuI~Nj zB(W;)bmaOS$+D4!x(n_zrXOVf`fO7_x2cbBw`71x!MCOJKfV{c%EZjbz_{4Wz{P+M z7_GAWjEw(TSeTiZSPX~mcE5Da$LE*hYNf9CyX`ZT=wk1a(23k8Wqc{nCeA5U zontna$kl-0>K5Pi8GAe43su~jV0^1ltoL=(kL^EOKMKuz#pTDj)MLFvPJmc?8-*YM3Xf@w literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..81b6326d6f1e2558a910ca33b17b606fbc1b30fe GIT binary patch literal 920 zcmXqLVxD5q#8k6@nTe5!NkICu*1snU-f#8TSI6l2aWN;i};kU$aS%1kNE%gfhINi8$vG2mij4rSryVFvMJ z45UCjW*%YJih|6d)D(r_)SNVh;LP;A)D#0bab80U15-nDBO_x=BeN(V*AmDzgK}wY zeG{V+vWFR28JL?G`56qF7`d357#SJvJ@~SgF;l+z&Fksnv#VITO1P(-l2Oxhv-yAU zgyU^Jr^^Rd+SjfV(eYC#r1Xd$W5i8dm zVUSViye?2`_3V}*cYpUWN&i(dr__~l*K;Sm(Vy?p$;8aaz_>WTz|VjW7>Tm{jEw(T zSeTiZSPW!Ad{q`P0}(b3Z8k<$R(574IExV?&Br3fBH~`e)AjbIq_iPRFjGveT#nNM z%X;K62PQILm@_hLh$#7TT+6DizjpUSzk~O#%Ntyhz8m!a$c$|hO}dRFf^YviJFWV^ z%$arEP7k|MK9m$p5BYogLLa-MMSZ=!jV@o^mOb)H$tS|rF}~gVbdA8*J#xFY-%VSw z>{f36Y(3RC#~$6gsIXRYR%A^;e)8;^Eo+xWuk`lvo!+3isJ=*E_@U~>47Lwv_a4(J z)QdQBXY0SA`zP0)DVT0{SXYMDwWGt)OZMDUEgSn2Y aL5XX_&cCycZ&yeTVh*lOs#HHvnFIi>%3gZ_ literal 0 HcmV?d00001 diff --git a/src/Shared/test/Certificates/selfSignedNoEkuCertificateNotValidYet.cer b/src/Shared/test/Certificates/selfSignedNoEkuCertificateNotValidYet.cer new file mode 100644 index 0000000000000000000000000000000000000000..9c8cf9d71b2218acdd8c6b208677a0e6aea68ade GIT binary patch literal 932 zcmXqLVqRd-#MH5XnTe5!Nx(Q=>**rqs6JPfj1S))AG&P7%f_kI=F#?@mywa1mBAp{ zP}o3_jf-=VTIb|XIw_2jV$AtP>4s7U5-5UPnJJ}tdHK32sbz*d23%~+p)A}y%pjhU zfdYuf%p>WSU!o9}n3I{J5Sdz{5S*HmrVyN&o|l?pAScdiWN2V&Xl`U=Y-wZ`1>_nb zap~iTCPpP>&oiExw?5;6y@y*rsP4hCpR9U!vn=&!>Z-91&%G$kV)3$joeK(K$8nchON%q+_m)lR| zg%y2U`R<~9vhXU8<0d6j)=VlWK6>tQue0ulzs%vWrJvL{8YI=XRR`a`7jMauU3GiI zq6ZSJ2hOW{CT*5vJ6bw5#wIblRjMpRLL&UD|62V=AI0o1ZV5Wy>f(J|<9=1*%`e_H zYZm_gE~t4iPGIl#e-qE~vv%y;eWQ3oYC_(ZrI|6NTV~2!D3Hqg-?6Mf-A-X{K>o~z zjhFQ^e6@Y~l-ULE+vRPDU%ouA%EI)dRba2<)b3S#WXbIX~X zwSiHae2~K&nBah6&d5-{_~@=-2ZZ&o7V*|7CG8-Gjh3)K(C2f={2^0`Zk6)&a9PB-m^M=(sK2( zqPy;*>kN(^cx`|8jF8m<9>EnQC*@AOWpmHcR#E&rX?AL@Vry{OBZb`9iwC3@PCYJb zAj4GtB7NG)9OYycjpCT^>rOwKc=OuIx$7qAJIX%mxgDjoIr%{FwGAfkmz;2XBbt9n^ eS4DHri(Z%*JA)~$t^O)kHpjKhrL|93J^}zP)?Fw7 literal 0 HcmV?d00001 diff --git a/src/Shared/test/Certificates/validSelfSignedClientEkuCertificate.cer b/src/Shared/test/Certificates/validSelfSignedClientEkuCertificate.cer new file mode 100644 index 0000000000000000000000000000000000000000..db4bb5b90a07ee564e3dbf44db557fb2e3be75ed GIT binary patch literal 928 zcmXqLVxD8r#MH8YnTe5!NkAnob;}(iF{Ft#k4xofJk%G3NZDbVDfv2^2xD%#_l+ynNl1)G|XJ11>h^P!?_;W)M%+ zKpDhi=8*|Y%*jkq2u{sOQwYvX&r3~FaL&n0%_~uG^$sO^iy&LBPn$z}&>h&tTBR$i>ve$jGocyz6dv_dV6PYdl|G+r3iFzmv^8 zpQB3a$374D?>p{@Km6PJd|r31&J@X$)%$8r_Wt$g_Y8j@Jtbssq3q#~qBUzGmUPv= zKC_oW#mztEWp!lYy_->jt~cl8_OQu3>sZxs&dPtG#D4Q7-r1sRr!QaQ{pa-CVWX;Q z?3tJzU$b)4rHqr0{IaS&x*?J6&5ZBM`Ikn&@Dn~O`mc1dsp`{TX&3hOACkHED8cI+ z562vHj#I~YyF}-nUGzjuWYO>Q=20iY+TCXg{d#_c&3p2h?Ppd$4?L3>EK|?R$$nwa zF=akEfB92y7ntz!opx1of0*ZX*Id2FAs11}+AC zz-X1_XJq`(!otkN#9|-};;XWNVxL2sjggg=oteo%79_~WBE}-Z8X9?f=7e6Elak3C z0uOtw{gZeckV6`n*nlC;$l!6{@kZe(F>Fl*7R_m*XL%as*Sjt+uLMt@go}65L{B{g)`laJ@ zZPs0IvRGkvSwk~^wBL{4(?>kYgrh`v-{!E={8?p-0~c;F{Ft#k4xofJk%G3NZDbVDfv2^2xD%#_l+ynNl1)G|XJ11>h^P!?_;W)M%& zKpwL5B&!|2G`BtT!|!*h zdQ-fTFP)k4e?x>gyOZ*ct1q6+THLnd2WMc~<+6xY>Ew`)j1MPUEnPKdRgcTF+mjk< zIo6*s=05gliAPw%wXPa0;6{(vNs+0y(jq#hmnYY;XIX_<^O|QN^B(Oy?kKzobB2?8NZqat;Cbdg^sGG=Jl?fA_iGSe{%c zIJCi(A>7>TNZTH$P5g%D?0((lL#d=IUNMr|bA0 znbRc|T3M}bzjKG;#hc34LQ)E*@15cJCEp`X?0N0E9mh&SA8x#0$A*xDz zn^GO0KaJUC%Ch?9!)BH5JJW4+GA~TLy(EdlBH_tV{R5Zx3bTeSJ#_Wg!%ZufR5yKy foUuix*2(c=(miwK^_wc!9KD*j=ZS{(-gis@(~V)} literal 0 HcmV?d00001 diff --git a/src/Shared/test/Certificates/validSelfSignedServerEkuCertificate.cer b/src/Shared/test/Certificates/validSelfSignedServerEkuCertificate.cer new file mode 100644 index 0000000000000000000000000000000000000000..823000d4a1474df454b8468a7f4052bb64c45460 GIT binary patch literal 928 zcmXqLVxD8r#MH8YnTe5!NuVt5gR29pc88Pv;k^&zPku1qW#iOp^Jx3d%gD&h%3zRY zC~P3e#>F{Ft#k4xofJk%G3NZDbVDfv2^2xD%#_l+ynNl1)G|XJ11>h^P!?_;W)M%+ zKpDhi=8*|Y%*jkq2u{sOQwYvX&r1cdi^@`q6kNSS4dldm4J{2!4b6>=j4h2!qr`cQ zOpv&Ab4L@S5^@kQvNA9?G4eAQG%<29H8CJzWL|eA}+}jtJ7bc)IM5t>-#M6DXX9B%V&SgX;Zzc_I>G^ zaIpaGH!hEj56yd{CckjyiMQt;Puuz=*88|dWr>}2Ou@>1-S13A5BW^4>T9r4P>k7K zcrIwg-_y$*HVP~h{`7vP?uGo>+eOVDdLA&gK6&Q8+lBMH&KPv|7M+_R5MInUd$;Xe zJ8Axx4>J6x6&&l~tvl|x>yhr|xofv=`0;Ar!`0{BY+6vd=gZeAIZ2N%Yx`-YuI~Nj zB(W;)bmaOS$+D4!x(n_zrXOVf`fO7_x2cbBw`71x!MCOJKfV{c%EZjbz_{4Wz{P+M z7_GAWjEw(TSeTiZSPX~mcE5Da$LE*hYNf9CyX`ZT=wk1a(23k8Wqc{nCeA5U zontna$kl-0>K5Pi8GAe43su~jV0^1ltoL=(kL^EOKMKuz#pTDj)MLFvPJmc?8-*YM3Xf@w literal 0 HcmV?d00001