Add CertificateAuthentication (#9756)

This commit is contained in:
Hao Kung 2019-05-31 22:49:40 -07:00 committed by GitHub
parent 4dde8b9461
commit b75b892eac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 2178 additions and 1 deletions

View File

@ -57,6 +57,7 @@
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions" ProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Abstractions\src\Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj" RefProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Abstractions\ref\Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" ProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Libuv\src\Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.csproj" RefProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Libuv\ref\Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" ProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Sockets\src\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj" RefProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Sockets\ref\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.Certificate" ProjectPath="$(RepoRoot)src\Security\Authentication\Certificate\src\Microsoft.AspNetCore.Authentication.Certificate.csproj" RefProjectPath="$(RepoRoot)src\Security\Authentication\Certificate\ref\Microsoft.AspNetCore.Authentication.Certificate.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.Cookies" ProjectPath="$(RepoRoot)src\Security\Authentication\Cookies\src\Microsoft.AspNetCore.Authentication.Cookies.csproj" RefProjectPath="$(RepoRoot)src\Security\Authentication\Cookies\ref\Microsoft.AspNetCore.Authentication.Cookies.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication" ProjectPath="$(RepoRoot)src\Security\Authentication\Core\src\Microsoft.AspNetCore.Authentication.csproj" RefProjectPath="$(RepoRoot)src\Security\Authentication\Core\ref\Microsoft.AspNetCore.Authentication.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.Facebook" ProjectPath="$(RepoRoot)src\Security\Authentication\Facebook\src\Microsoft.AspNetCore.Authentication.Facebook.csproj" RefProjectPath="$(RepoRoot)src\Security\Authentication\Facebook\ref\Microsoft.AspNetCore.Authentication.Facebook.csproj" />

View File

@ -3,6 +3,10 @@
namespace Microsoft.AspNetCore.Builder
{
public static partial class CertificateForwardingBuilderExtensions
{
public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseCertificateForwarding(this Microsoft.AspNetCore.Builder.IApplicationBuilder app) { throw null; }
}
public static partial class ForwardedHeadersExtensions
{
public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseForwardedHeaders(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder) { throw null; }
@ -37,6 +41,17 @@ namespace Microsoft.AspNetCore.Builder
}
namespace Microsoft.AspNetCore.HttpOverrides
{
public partial class CertificateForwardingMiddleware
{
public CertificateForwardingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.HttpOverrides.CertificateForwardingOptions> options) { }
public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext httpContext) { throw null; }
}
public partial class CertificateForwardingOptions
{
public System.Func<string, System.Security.Cryptography.X509Certificates.X509Certificate2> HeaderConverter;
public CertificateForwardingOptions() { }
public string CertificateHeader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
[System.FlagsAttribute]
public enum ForwardedHeaders
{
@ -75,3 +90,10 @@ namespace Microsoft.AspNetCore.HttpOverrides
public bool Contains(System.Net.IPAddress address) { throw null; }
}
}
namespace Microsoft.Extensions.DependencyInjection
{
public static partial class CertificateForwardingServiceExtensions
{
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddCertificateForwarding(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<Microsoft.AspNetCore.HttpOverrides.CertificateForwardingOptions> configure) { throw null; }
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.HttpOverrides;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Extension methods for using certificate fowarding.
/// </summary>
public static class CertificateForwardingBuilderExtensions
{
/// <summary>
/// Adds a middleware to the pipeline that will look for a certificate in a request header
/// decode it, and updates HttpContext.Connection.ClientCertificate.
/// </summary>
/// <param name="app"></param>
/// <returns></returns>
public static IApplicationBuilder UseCertificateForwarding(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
return app.UseMiddleware<CertificateForwardingMiddleware>();
}
}
}

View File

@ -0,0 +1,51 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.HttpOverrides
{
internal class CertificateForwardingFeature : ITlsConnectionFeature
{
private ILogger _logger;
private StringValues _header;
private CertificateForwardingOptions _options;
private X509Certificate2 _certificate;
public CertificateForwardingFeature(ILogger logger, StringValues header, CertificateForwardingOptions options)
{
_logger = logger;
_options = options;
_header = header;
}
public X509Certificate2 ClientCertificate
{
get
{
if (_certificate == null)
{
try
{
_certificate = _options.HeaderConverter(_header);
}
catch (Exception e)
{
_logger.NoCertificate(e);
}
}
return _certificate;
}
set => _certificate = value;
}
public Task<X509Certificate2> GetClientCertificateAsync(CancellationToken cancellationToken)
=> Task.FromResult(ClientCertificate);
}
}

View File

@ -0,0 +1,66 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.HttpOverrides
{
/// <summary>
/// Middleware that converts a forward header into a client certificate if found.
/// </summary>
public class CertificateForwardingMiddleware
{
private readonly RequestDelegate _next;
private readonly CertificateForwardingOptions _options;
private readonly ILogger _logger;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="next"></param>
/// <param name="loggerFactory"></param>
/// <param name="options"></param>
public CertificateForwardingMiddleware(
RequestDelegate next,
ILoggerFactory loggerFactory,
IOptions<CertificateForwardingOptions> options)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
if (loggerFactory == null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_options = options.Value;
_logger = loggerFactory.CreateLogger<CertificateForwardingMiddleware>();
}
/// <summary>
/// Looks for the presence of a <see cref="CertificateForwardingOptions.CertificateHeader"/> header in the request,
/// if found, converts this header to a ClientCertificate set on the connection.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <returns>A <see cref="Task"/>.</returns>
public Task Invoke(HttpContext httpContext)
{
var header = httpContext.Request.Headers[_options.CertificateHeader];
if (!StringValues.IsNullOrEmpty(header))
{
httpContext.Features.Set<ITlsConnectionFeature>(new CertificateForwardingFeature(_logger, header, _options));
}
return _next(httpContext);
}
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Security.Cryptography.X509Certificates;
namespace Microsoft.AspNetCore.HttpOverrides
{
/// <summary>
/// Used to configure the <see cref="CertificateForwardingMiddleware"/>.
/// </summary>
public class CertificateForwardingOptions
{
/// <summary>
/// The name of the header containing the client certificate.
/// </summary>
/// <remarks>
/// This defaults to X-Client-Cert
/// </remarks>
public string CertificateHeader { get; set; } = "X-Client-Cert";
/// <summary>
/// The function used to convert the header to an instance of <see cref="X509Certificate2"/>.
/// </summary>
/// <remarks>
/// This defaults to a conversion from a base64 encoded string.
/// </remarks>
public Func<string, X509Certificate2> HeaderConverter = (headerValue) => new X509Certificate2(Convert.FromBase64String(headerValue));
}
}

View File

@ -0,0 +1,38 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.HttpOverrides;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extension methods for using certificate fowarding.
/// </summary>
public static class CertificateForwardingServiceExtensions
{
/// <summary>
/// Adds certificate forwarding to the specified <see cref="IServiceCollection" />.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <param name="configure">An action delegate to configure the provided <see cref="CertificateForwardingOptions"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddCertificateForwarding(
this IServiceCollection services,
Action<CertificateForwardingOptions> configure)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}
services.AddOptions<CertificateForwardingOptions>().Validate(o => !string.IsNullOrEmpty(o.CertificateHeader), "CertificateForwarderOptions.CertificateHeader cannot be null or empty.");
return services.Configure(configure);
}
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.Extensions.Logging
{
internal static class LoggingExtensions
{
private static Action<ILogger, Exception> _noCertificate;
static LoggingExtensions()
{
_noCertificate = LoggerMessage.Define(
eventId: new EventId(0, "NoCertificate"),
logLevel: LogLevel.Warning,
formatString: "Could not read certificate from header.");
}
public static void NoCertificate(this ILogger logger, Exception exception)
{
_noCertificate(logger, exception);
}
}
}

View File

@ -0,0 +1,222 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNetCore.HttpOverrides
{
public class CertificateForwardingTests
{
[Fact]
public void VerifySettingNullHeaderOptionThrows()
{
var services = new ServiceCollection()
.AddOptions()
.AddCertificateForwarding(o => o.CertificateHeader = null);
var options = services.BuildServiceProvider().GetRequiredService<IOptions<CertificateForwardingOptions>>();
Assert.Throws<OptionsValidationException>(() => options.Value);
}
[Fact]
public void VerifySettingEmptyHeaderOptionThrows()
{
var services = new ServiceCollection()
.AddOptions()
.AddCertificateForwarding(o => o.CertificateHeader = "");
var options = services.BuildServiceProvider().GetRequiredService<IOptions<CertificateForwardingOptions>>();
Assert.Throws<OptionsValidationException>(() => options.Value);
}
[Fact]
public async Task VerifyHeaderIsUsedIfNoCertificateAlreadySet()
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddCertificateForwarding(options => { });
})
.Configure(app =>
{
app.Use(async (context, next) =>
{
Assert.Null(context.Connection.ClientCertificate);
await next();
});
app.UseCertificateForwarding();
app.Use(async (context, next) =>
{
Assert.Equal(context.Connection.ClientCertificate, Certificates.SelfSignedValidWithNoEku);
await next();
});
});
var server = new TestServer(builder);
var context = await server.SendAsync(c =>
{
c.Request.Headers["X-Client-Cert"] = Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData);
});
}
[Fact]
public async Task VerifyHeaderOverridesCertificateEvenAlreadySet()
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddCertificateForwarding(options => { });
})
.Configure(app =>
{
app.Use(async (context, next) =>
{
Assert.Null(context.Connection.ClientCertificate);
context.Connection.ClientCertificate = Certificates.SelfSignedNotYetValid;
await next();
});
app.UseCertificateForwarding();
app.Use(async (context, next) =>
{
Assert.Equal(context.Connection.ClientCertificate, Certificates.SelfSignedValidWithNoEku);
await next();
});
});
var server = new TestServer(builder);
var context = await server.SendAsync(c =>
{
c.Request.Headers["X-Client-Cert"] = Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData);
});
}
[Fact]
public async Task VerifySettingTheAzureHeaderOnTheForwarderOptionsWorks()
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddCertificateForwarding(options => options.CertificateHeader = "X-ARR-ClientCert");
})
.Configure(app =>
{
app.Use(async (context, next) =>
{
Assert.Null(context.Connection.ClientCertificate);
await next();
});
app.UseCertificateForwarding();
app.Use(async (context, next) =>
{
Assert.Equal(context.Connection.ClientCertificate, Certificates.SelfSignedValidWithNoEku);
await next();
});
});
var server = new TestServer(builder);
var context = await server.SendAsync(c =>
{
c.Request.Headers["X-ARR-ClientCert"] = Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData);
});
}
[Fact]
public async Task VerifyACustomHeaderFailsIfTheHeaderIsNotPresent()
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddCertificateForwarding(options => options.CertificateHeader = "some-random-header");
})
.Configure(app =>
{
app.Use(async (context, next) =>
{
Assert.Null(context.Connection.ClientCertificate);
await next();
});
app.UseCertificateForwarding();
app.Use(async (context, next) =>
{
Assert.Null(context.Connection.ClientCertificate);
await next();
});
});
var server = new TestServer(builder);
var context = await server.SendAsync(c =>
{
c.Request.Headers["not-the-right-header"] = Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData);
});
}
[Fact]
public async Task VerifyArrHeaderEncodedCertFailsOnBadEncoding()
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddCertificateForwarding(options => { });
})
.Configure(app =>
{
app.Use(async (context, next) =>
{
Assert.Null(context.Connection.ClientCertificate);
await next();
});
app.UseCertificateForwarding();
app.Use(async (context, next) =>
{
Assert.Null(context.Connection.ClientCertificate);
await next();
});
});
var server = new TestServer(builder);
var context = await server.SendAsync(c =>
{
c.Request.Headers["X-Client-Cert"] = "OOPS" + Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData);
});
}
private static class Certificates
{
public static X509Certificate2 SelfSignedValidWithClientEku { get; private set; } =
new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedClientEkuCertificate.cer"));
public static X509Certificate2 SelfSignedValidWithNoEku { get; private set; } =
new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedNoEkuCertificate.cer"));
public static X509Certificate2 SelfSignedValidWithServerEku { get; private set; } =
new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedServerEkuCertificate.cer"));
public static X509Certificate2 SelfSignedNotYetValid { get; private set; } =
new X509Certificate2(GetFullyQualifiedFilePath("selfSignedNoEkuCertificateNotValidYet.cer"));
public static X509Certificate2 SelfSignedExpired { get; private set; } =
new X509Certificate2(GetFullyQualifiedFilePath("selfSignedNoEkuCertificateExpired.cer"));
private static string GetFullyQualifiedFilePath(string filename)
{
var filePath = Path.Combine(AppContext.BaseDirectory, filename);
if (!File.Exists(filePath))
{
throw new FileNotFoundException(filePath);
}
return filePath;
}
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
@ -8,6 +8,7 @@
<Reference Include="Microsoft.AspNetCore.HttpOverrides" />
<Reference Include="Microsoft.AspNetCore.TestHost" />
<Reference Include="Microsoft.Extensions.Logging.Testing" />
<Content Include="$(SharedSourceRoot)test\Certificates\*.cer" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,10 @@
<!-- This file is automatically generated. -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
<Compile Include="Microsoft.AspNetCore.Authentication.Certificate.netcoreapp3.0.cs" />
<Reference Include="Microsoft.AspNetCore.Authentication" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,59 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Authentication.Certificate
{
public static partial class CertificateAuthenticationDefaults
{
public const string AuthenticationScheme = "Certificate";
}
public partial class CertificateAuthenticationEvents
{
public CertificateAuthenticationEvents() { }
public System.Func<Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationFailedContext, System.Threading.Tasks.Task> OnAuthenticationFailed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.Func<Microsoft.AspNetCore.Authentication.Certificate.CertificateValidatedContext, System.Threading.Tasks.Task> OnCertificateValidated { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public virtual System.Threading.Tasks.Task AuthenticationFailed(Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationFailedContext context) { throw null; }
public virtual System.Threading.Tasks.Task CertificateValidated(Microsoft.AspNetCore.Authentication.Certificate.CertificateValidatedContext context) { throw null; }
}
public partial class CertificateAuthenticationFailedContext : Microsoft.AspNetCore.Authentication.ResultContext<Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions>
{
public CertificateAuthenticationFailedContext(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Authentication.AuthenticationScheme scheme, Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions options) : base (default(Microsoft.AspNetCore.Http.HttpContext), default(Microsoft.AspNetCore.Authentication.AuthenticationScheme), default(Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions)) { }
public System.Exception Exception { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
public partial class CertificateAuthenticationOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions
{
public CertificateAuthenticationOptions() { }
public Microsoft.AspNetCore.Authentication.Certificate.CertificateTypes AllowedCertificateTypes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public new Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationEvents Events { get { throw null; } set { } }
public System.Security.Cryptography.X509Certificates.X509RevocationFlag RevocationFlag { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.Security.Cryptography.X509Certificates.X509RevocationMode RevocationMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool ValidateCertificateUse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool ValidateValidityPeriod { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
[System.FlagsAttribute]
public enum CertificateTypes
{
Chained = 1,
SelfSigned = 2,
All = 3,
}
public partial class CertificateValidatedContext : Microsoft.AspNetCore.Authentication.ResultContext<Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions>
{
public CertificateValidatedContext(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Authentication.AuthenticationScheme scheme, Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions options) : base (default(Microsoft.AspNetCore.Http.HttpContext), default(Microsoft.AspNetCore.Authentication.AuthenticationScheme), default(Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions)) { }
public System.Security.Cryptography.X509Certificates.X509Certificate2 ClientCertificate { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
public static partial class X509Certificate2Extensions
{
public static bool IsSelfSigned(this System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { throw null; }
}
}
namespace Microsoft.Extensions.DependencyInjection
{
public static partial class CertificateAuthenticationAppBuilderExtensions
{
public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCertificate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder) { throw null; }
public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCertificate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, System.Action<Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions> configureOptions) { throw null; }
public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCertificate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme) { throw null; }
public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCertificate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, System.Action<Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions> configureOptions) { throw null; }
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.Authentication.Certificate" />
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
<Reference Include="Microsoft.AspNetCore.Hosting" />
<Reference Include="Microsoft.AspNetCore.Mvc" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
namespace Certificate.Sample.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}
}

View File

@ -0,0 +1,26 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Https;
namespace Certificate.Sample
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)
=> WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(opt =>
{
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
})
.Build();
}
}

View File

@ -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/"
}
}
}

View File

@ -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();
});
}
}
}

View File

@ -0,0 +1 @@
<h1>Hello @User.Identity.Name</h1>

View File

@ -0,0 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Authentication.Certificate
{
/// <summary>
/// Default values related to certificate authentication middleware
/// </summary>
public static class CertificateAuthenticationDefaults
{
/// <summary>
/// The default value used for CertificateAuthenticationOptions.AuthenticationScheme
/// </summary>
public const string AuthenticationScheme = "Certificate";
}
}

View File

@ -0,0 +1,55 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Certificate;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extension methods to add Certificate authentication capabilities to an HTTP application pipeline.
/// </summary>
public static class CertificateAuthenticationAppBuilderExtensions
{
/// <summary>
/// Adds certificate authentication.
/// </summary>
/// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder)
=> builder.AddCertificate(CertificateAuthenticationDefaults.AuthenticationScheme);
/// <summary>
/// Adds certificate authentication.
/// </summary>
/// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
/// <param name="authenticationScheme"></param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder, string authenticationScheme)
=> builder.AddCertificate(authenticationScheme, configureOptions: null);
/// <summary>
/// Adds certificate authentication.
/// </summary>
/// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
/// <param name="configureOptions"></param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder, Action<CertificateAuthenticationOptions> configureOptions)
=> builder.AddCertificate(CertificateAuthenticationDefaults.AuthenticationScheme, configureOptions);
/// <summary>
/// Adds certificate authentication.
/// </summary>
/// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
/// <param name="authenticationScheme"></param>
/// <param name="configureOptions"></param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddCertificate(
this AuthenticationBuilder builder,
string authenticationScheme,
Action<CertificateAuthenticationOptions> configureOptions)
=> builder.AddScheme<CertificateAuthenticationOptions, CertificateAuthenticationHandler>(authenticationScheme, configureOptions);
}
}

View File

@ -0,0 +1,235 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Authentication.Certificate
{
internal class CertificateAuthenticationHandler : AuthenticationHandler<CertificateAuthenticationOptions>
{
private static readonly Oid ClientCertificateOid = new Oid("1.3.6.1.5.5.7.3.2");
public CertificateAuthenticationHandler(
IOptionsMonitor<CertificateAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
/// <summary>
/// The handler calls methods on the events which give the application control at certain points where processing is occurring.
/// If it is not provided a default instance is supplied which does nothing when the methods are called.
/// </summary>
protected new CertificateAuthenticationEvents Events
{
get { return (CertificateAuthenticationEvents)base.Events; }
set { base.Events = value; }
}
/// <summary>
/// Creates a new instance of the events instance.
/// </summary>
/// <returns>A new instance of the events instance.</returns>
protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new CertificateAuthenticationEvents());
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// You only get client certificates over HTTPS
if (!Context.Request.IsHttps)
{
return AuthenticateResult.NoResult();
}
try
{
var clientCertificate = await Context.Connection.GetClientCertificateAsync();
// This should never be the case, as cert authentication happens long before ASP.NET kicks in.
if (clientCertificate == null)
{
Logger.NoCertificate();
return AuthenticateResult.NoResult();
}
// If we have a self signed cert, and they're not allowed, exit early and not bother with
// any other validations.
if (clientCertificate.IsSelfSigned() &&
!Options.AllowedCertificateTypes.HasFlag(CertificateTypes.SelfSigned))
{
Logger.CertificateRejected("Self signed", clientCertificate.Subject);
return AuthenticateResult.Fail("Options do not allow self signed certificates.");
}
// If we have a chained cert, and they're not allowed, exit early and not bother with
// any other validations.
if (!clientCertificate.IsSelfSigned() &&
!Options.AllowedCertificateTypes.HasFlag(CertificateTypes.Chained))
{
Logger.CertificateRejected("Chained", clientCertificate.Subject);
return AuthenticateResult.Fail("Options do not allow chained certificates.");
}
var chainPolicy = BuildChainPolicy(clientCertificate);
var chain = new X509Chain
{
ChainPolicy = chainPolicy
};
var certificateIsValid = chain.Build(clientCertificate);
if (!certificateIsValid)
{
var chainErrors = new List<string>();
foreach (var validationFailure in chain.ChainStatus)
{
chainErrors.Add($"{validationFailure.Status} {validationFailure.StatusInformation}");
}
Logger.CertificateFailedValidation(clientCertificate.Subject, chainErrors);
return AuthenticateResult.Fail("Client certificate failed validation.");
}
var certificateValidatedContext = new CertificateValidatedContext(Context, Scheme, Options)
{
ClientCertificate = clientCertificate,
Principal = CreatePrincipal(clientCertificate)
};
await Events.CertificateValidated(certificateValidatedContext);
if (certificateValidatedContext.Result != null)
{
return certificateValidatedContext.Result;
}
certificateValidatedContext.Success();
return certificateValidatedContext.Result;
}
catch (Exception ex)
{
var authenticationFailedContext = new CertificateAuthenticationFailedContext(Context, Scheme, Options)
{
Exception = ex
};
await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null)
{
return authenticationFailedContext.Result;
}
throw;
}
}
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
// Certificate authentication takes place at the connection level. We can't prompt once we're in
// user code, so the best thing to do is Forbid, not Challenge.
return HandleForbiddenAsync(properties);
}
private X509ChainPolicy BuildChainPolicy(X509Certificate2 certificate)
{
// Now build the chain validation options.
X509RevocationFlag revocationFlag = Options.RevocationFlag;
X509RevocationMode revocationMode = Options.RevocationMode;
if (certificate.IsSelfSigned())
{
// Turn off chain validation, because we have a self signed certificate.
revocationFlag = X509RevocationFlag.EntireChain;
revocationMode = X509RevocationMode.NoCheck;
}
var chainPolicy = new X509ChainPolicy
{
RevocationFlag = revocationFlag,
RevocationMode = revocationMode,
};
if (Options.ValidateCertificateUse)
{
chainPolicy.ApplicationPolicy.Add(ClientCertificateOid);
}
if (certificate.IsSelfSigned())
{
chainPolicy.VerificationFlags |= X509VerificationFlags.AllowUnknownCertificateAuthority;
chainPolicy.VerificationFlags |= X509VerificationFlags.IgnoreEndRevocationUnknown;
chainPolicy.ExtraStore.Add(certificate);
}
if (!Options.ValidateValidityPeriod)
{
chainPolicy.VerificationFlags |= X509VerificationFlags.IgnoreNotTimeValid;
}
return chainPolicy;
}
private ClaimsPrincipal CreatePrincipal(X509Certificate2 certificate)
{
var claims = new List<Claim>();
var issuer = certificate.Issuer;
claims.Add(new Claim("issuer", issuer, ClaimValueTypes.String, Options.ClaimsIssuer));
var thumbprint = certificate.Thumbprint;
claims.Add(new Claim(ClaimTypes.Thumbprint, thumbprint, ClaimValueTypes.Base64Binary, Options.ClaimsIssuer));
var value = certificate.SubjectName.Name;
if (!string.IsNullOrWhiteSpace(value))
{
claims.Add(new Claim(ClaimTypes.X500DistinguishedName, value, ClaimValueTypes.String, Options.ClaimsIssuer));
}
value = certificate.SerialNumber;
if (!string.IsNullOrWhiteSpace(value))
{
claims.Add(new Claim(ClaimTypes.SerialNumber, value, ClaimValueTypes.String, Options.ClaimsIssuer));
}
value = certificate.GetNameInfo(X509NameType.DnsName, false);
if (!string.IsNullOrWhiteSpace(value))
{
claims.Add(new Claim(ClaimTypes.Dns, value, ClaimValueTypes.String, Options.ClaimsIssuer));
}
value = certificate.GetNameInfo(X509NameType.SimpleName, false);
if (!string.IsNullOrWhiteSpace(value))
{
claims.Add(new Claim(ClaimTypes.Name, value, ClaimValueTypes.String, Options.ClaimsIssuer));
}
value = certificate.GetNameInfo(X509NameType.EmailName, false);
if (!string.IsNullOrWhiteSpace(value))
{
claims.Add(new Claim(ClaimTypes.Email, value, ClaimValueTypes.String, Options.ClaimsIssuer));
}
value = certificate.GetNameInfo(X509NameType.UpnName, false);
if (!string.IsNullOrWhiteSpace(value))
{
claims.Add(new Claim(ClaimTypes.Upn, value, ClaimValueTypes.String, Options.ClaimsIssuer));
}
value = certificate.GetNameInfo(X509NameType.UrlName, false);
if (!string.IsNullOrWhiteSpace(value))
{
claims.Add(new Claim(ClaimTypes.Uri, value, ClaimValueTypes.String, Options.ClaimsIssuer));
}
var identity = new ClaimsIdentity(claims, CertificateAuthenticationDefaults.AuthenticationScheme);
return new ClaimsPrincipal(identity);
}
}
}

View File

@ -0,0 +1,53 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Security.Cryptography.X509Certificates;
namespace Microsoft.AspNetCore.Authentication.Certificate
{
/// <summary>
/// Options used to configure certificate authentication.
/// </summary>
public class CertificateAuthenticationOptions : AuthenticationSchemeOptions
{
/// <summary>
/// Value indicating the types of certificates accepted by the authentication middleware.
/// </summary>
public CertificateTypes AllowedCertificateTypes { get; set; } = CertificateTypes.Chained;
/// <summary>
/// Flag indicating whether the client certificate must be suitable for client
/// authentication, either via the Client Authentication EKU, or having no EKUs
/// at all. If the certificate chains to a root CA all certificates in the chain must be validate
/// for the client authentication EKU.
/// </summary>
public bool ValidateCertificateUse { get; set; } = true;
/// <summary>
/// Flag indicating whether the client certificate validity period should be checked.
/// </summary>
public bool ValidateValidityPeriod { get; set; } = true;
/// <summary>
/// Specifies which X509 certificates in the chain should be checked for revocation.
/// </summary>
public X509RevocationFlag RevocationFlag { get; set; } = X509RevocationFlag.ExcludeRoot;
/// <summary>
/// Specifies conditions under which verification of certificates in the X509 chain should be conducted.
/// </summary>
public X509RevocationMode RevocationMode { get; set; } = X509RevocationMode.Online;
/// <summary>
/// The object provided by the application to process events raised by the certificate authentication middleware.
/// The application may implement the interface fully, or it may create an instance of CertificateAuthenticationEvents
/// and assign delegates only to the events it wants to process.
/// </summary>
public new CertificateAuthenticationEvents Events
{
get { return (CertificateAuthenticationEvents)base.Events; }
set { base.Events = value; }
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Authentication.Certificate
{
/// <summary>
/// Enum representing certificate types.
/// </summary>
[Flags]
public enum CertificateTypes
{
/// <summary>
/// Chained certificates.
/// </summary>
Chained = 1,
/// <summary>
/// SelfSigned certificates.
/// </summary>
SelfSigned = 2,
/// <summary>
/// All certificates.
/// </summary>
All = Chained | SelfSigned
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Authentication.Certificate
{
/// <summary>
/// This default implementation of the IBasicAuthenticationEvents may be used if the
/// application only needs to override a few of the interface methods.
/// This may be used as a base class or may be instantiated directly.
/// </summary>
public class CertificateAuthenticationEvents
{
/// <summary>
/// A delegate assigned to this property will be invoked when the authentication fails.
/// </summary>
public Func<CertificateAuthenticationFailedContext, Task> OnAuthenticationFailed { get; set; } = context => Task.CompletedTask;
/// <summary>
/// A delegate assigned to this property will be invoked when a certificate has passed basic validation, but where custom validation may be needed.
/// </summary>
/// <remarks>
/// You must provide a delegate for this property for authentication to occur.
/// In your delegate you should construct an authentication principal from the user details,
/// attach it to the context.Principal property and finally call context.Success();
/// </remarks>
public Func<CertificateValidatedContext, Task> OnCertificateValidated { get; set; } = context => Task.CompletedTask;
/// <summary>
/// Invoked when a certificate fails authentication.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public virtual Task AuthenticationFailed(CertificateAuthenticationFailedContext context) => OnAuthenticationFailed(context);
/// <summary>
/// Invoked after a certificate has been validated
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public virtual Task CertificateValidated(CertificateValidatedContext context) => OnCertificateValidated(context);
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Authentication.Certificate
{
/// <summary>
/// Context used when a failure occurs.
/// </summary>
public class CertificateAuthenticationFailedContext : ResultContext<CertificateAuthenticationOptions>
{
/// <summary>
/// Constructor.
/// </summary>
/// <param name="context"></param>
/// <param name="scheme"></param>
/// <param name="options"></param>
public CertificateAuthenticationFailedContext(
HttpContext context,
AuthenticationScheme scheme,
CertificateAuthenticationOptions options)
: base(context, scheme, options)
{
}
/// <summary>
/// The exception.
/// </summary>
public Exception Exception { get; set; }
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Authentication.Certificate
{
/// <summary>
/// Context used when certificates are being validated.
/// </summary>
public class CertificateValidatedContext : ResultContext<CertificateAuthenticationOptions>
{
/// <summary>
/// Creates a new instance of <see cref="CertificateValidatedContext"/>.
/// </summary>
/// <param name="context">The HttpContext the validate context applies too.</param>
/// <param name="scheme">The scheme used when the Certificate Authentication handler was registered.</param>
/// <param name="options">The <see cref="CertificateAuthenticationOptions"/>.</param>
public CertificateValidatedContext(
HttpContext context,
AuthenticationScheme scheme,
CertificateAuthenticationOptions options)
: base(context, scheme, options)
{
}
/// <summary>
/// The certificate to validate.
/// </summary>
public X509Certificate2 ClientCertificate { get; set; }
}
}

View File

@ -0,0 +1,48 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.Extensions.Logging
{
internal static class LoggingExtensions
{
private static Action<ILogger, Exception> _noCertificate;
private static Action<ILogger, string, string, Exception> _certRejected;
private static Action<ILogger, string, string, Exception> _certFailedValidation;
static LoggingExtensions()
{
_noCertificate = LoggerMessage.Define(
eventId: new EventId(0, "NoCertificate"),
logLevel: LogLevel.Debug,
formatString: "No client certificate found.");
_certRejected = LoggerMessage.Define<string, string>(
eventId: new EventId(1, "CertificateRejected"),
logLevel: LogLevel.Warning,
formatString: "{CertificateType} certificate rejected, subject was {Subject}.");
_certFailedValidation = LoggerMessage.Define<string, string>(
eventId: new EventId(2, "CertificateFailedValidation"),
logLevel: LogLevel.Warning,
formatString: "Certificate validation failed, subject was {Subject}." + Environment.NewLine + "{ChainErrors}");
}
public static void NoCertificate(this ILogger logger)
{
_noCertificate(logger, null);
}
public static void CertificateRejected(this ILogger logger, string certificateType, string subject)
{
_certRejected(logger, certificateType, subject, null);
}
public static void CertificateFailedValidation(this ILogger logger, string subject, IEnumerable<string> chainedErrors)
{
_certFailedValidation(logger, subject, String.Join(Environment.NewLine, chainedErrors), null);
}
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core middleware that enables an application to support certificate authentication.</Description>
<TargetFramework>netcoreapp3.0</TargetFramework>
<DefineConstants>$(DefineConstants);SECURITY</DefineConstants>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;authentication;security;x509;certificate</PackageTags>
<IsShippingPackage>true</IsShippingPackage>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Authentication" />
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -0,0 +1,234 @@
# Microsoft.AspNetCore.Authentication.Certificate
This project sort of contains an implementation of [Certificate Authentication](https://tools.ietf.org/html/rfc5246#section-7.4.4) for ASP.NET Core.
Certificate authentication happens at the TLS level, long before it ever gets to ASP.NET Core, so, more accurately this is an authentication handler
that validates the certificate and then gives you an event where you can resolve that certificate to a ClaimsPrincipal.
You **must** [configure your host](#hostConfiguration) for certificate authentication, be it IIS, Kestrel, Azure Web Applications or whatever else you're using.
## Getting started
First acquire an HTTPS certificate, apply it and then [configure your host](#hostConfiguration) to require certificates.
In your web application add a reference to the package, then in the `ConfigureServices` method in `startup.cs` call
`app.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).UseCertificateAuthentication(...);` with your options,
providing a delegate for `OnValidateCertificate` to validate the client certificate sent with requests and turn that information
into an `ClaimsPrincipal`, set it on the `context.Principal` property and call `context.Success()`.
If you change your scheme name in the options for the authentication handler you need to change the scheme name in
`AddAuthentication()` to ensure it's used on every request which ends in an endpoint that requires authorization.
If authentication fails this handler will return a `403 (Forbidden)` response rather a `401 (Unauthorized)` as you
might expect - this is because the authentication should happen during the initial TLS connection - by the time it
reaches the handler it's too late, and there's no way to actually upgrade the connection from an anonymous connection
to one with a certificate.
You must also add `app.UseAuthentication();` in the `Configure` method, otherwise nothing will ever get called.
For example;
```c#
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate();
// All the other service configuration.
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
// All the other app configuration.
}
```
In the sample above you can see the default way to add certificate authentication. The handler will construct a user principal using the common certificate properties for you.
## Configuring Certificate Validation
The `CertificateAuthenticationOptions` handler has some built in validations that are the minimium validations you should perform on
a certificate. Each of these settings are turned on by default.
### ValidateCertificateChain
This check validates that the issuer for the certificate is trusted by the application host OS. If
you are going to accept self-signed certificates you must disable this check.
### ValidateCertificateUse
This check validates that the certificate presented by the client has the Client Authentication
extended key use, or no EKUs at all (as the specifications say if no EKU is specified then all EKUs
are valid).
### ValidateValidityPeriod
This check validates that the certificate is within its validity period. As the handler runs on every
request this ensures that a certificate that was valid when it was presented has not expired during
its current session.
### RevocationFlag
A flag which specifies which certificates in the chain are checked for revocation.
Revocation checks are only performed when the certificate is chained to a root certificate.
### RevocationMode
A flag which specifies how revocation checks are performed.
Specifying an on-line check can result in a long delay while the certificate authority is contacted.
Revocation checks are only performed when the certificate is chained to a root certificate.
### Can I configure my application to require a certificate only on certain paths?
Not possible, remember the certificate exchange is done that the start of the HTTPS conversation,
it's done by the host, not the application. Kestrel, IIS, Azure Web Apps don't have any configuration for
this sort of thing.
# Handler events
The handler has two events, `OnAuthenticationFailed()`, which is called if an exception happens during authentication and allows you to react, and `OnValidateCertificate()` which is
called after certificate has been validated, passed validation, abut before the default principal has been created. This allows you to perform your own validation, for example
checking if the certificate is one your services knows about, and to construct your own principal. For example,
```c#
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.Events = new CertificateAuthenticationEvents
{
OnValidateCertificate = context =>
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
return Task.CompletedTask;
}
};
});
```
If you find the inbound certificate doesn't meet your extra validation call `context.Fail("failure Reason")` with a failure reason.
For real functionality you will probably want to call a service registered in DI which talks to a database or other type of
user store. You can grab your service by using the context passed into your delegates, like so
```c#
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService =
context.HttpContext.RequestServices.GetService<ICertificateValidationService>();
if (validationService.ValidateCertificate(context.ClientCertificate))
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
return Task.CompletedTask;
}
};
});
```
Note that conceptually the validation of the certification is an authorization concern, and putting a check on, for example, an issuer or thumbprint in an authorization policy rather
than inside OnCertificateValidated() is perfectly acceptable.
## <a name="hostConfiguration"></a>Configuring your host to require certificates
### Kestrel
In program.cs configure `UseKestrel()` as follows.
```c#
public static IWebHost BuildWebHost(string[] args)
=> WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(opt =>
{
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
})
.Build();
```
You must set the `ClientCertificateValidation` delegate to `CertificateValidator.DisableChannelValidation` in order to stop Kestrel using the default OS certificate validation routine and,
instead, letting the authentication handler perform the validation.
### IIS
In the IIS Manager
1. Select your Site in the Connections tab.
2. Double click the SSL Settings in the Features View window.
3. Check the `Require SSL` Check Box and select the `Require` radio button under Client Certificates.
![Client Certificate Settings in IIS](README-IISConfig.png "Client Certificate Settings in IIS")
### Azure
See the [Azure documentation](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth)
to configure Azure Web Apps then add the following to your application startup method, `Configure(IApplicationBuilder app)` add the
following line before the call to `app.UseAuthentication();`
```c#
app.UseCertificateHeaderForwarding();
```
### Random custom web proxies
If you're using a proxy which isn't IIS or Azure's Web Apps Application Request Routing you will need to configure your proxy
to forward the certificate it received in an HTTP header.
In your application startup method, `Configure(IApplicationBuilder app)`, add the
following line before the call to `app.UseAuthentication();`
```c#
app.UseCertificateForwarding();
```
You will also need to configure the Certificate Forwarding middleware to specify the header name.
In your service configuration method, `ConfigureServices(IServiceCollection services)` add
the following code to configure the header the forwarding middleware will build a certificate from;
```c#
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "YOUR_CUSTOM_HEADER_NAME";
});
```
Finally, if your proxy is doing something weird to pass the header on, rather than base 64 encoding it
(looking at you nginx (╯°□°)╯︵ ┻━┻) you can override the converter option to be a func that will
perform the optional conversion, for example
```c#
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "YOUR_CUSTOM_HEADER_NAME";
options.HeaderConverter = (headerValue) =>
{
var clientCertificate =
/* some weird conversion logic to create an X509Certificate2 */
return clientCertificate;
}
});
```

View File

@ -0,0 +1,27 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
namespace Microsoft.AspNetCore.Authentication.Certificate
{
/// <summary>
/// Extension methods for <see cref="X509Certificate2"/>.
/// </summary>
public static class X509Certificate2Extensions
{
/// <summary>
/// Determines if the certificate is self signed.
/// </summary>
/// <param name="certificate">The <see cref="X509Certificate2"/>.</param>
/// <returns>True if the certificate is self signed.</returns>
public static bool IsSelfSigned(this X509Certificate2 certificate)
{
Span<byte> subject = certificate.SubjectName.RawData;
Span<byte> issuer = certificate.IssuerName.RawData;
return subject.SequenceEqual(issuer);
}
}
}

View File

@ -0,0 +1,628 @@
// Copyright (c) Barry Dorrans. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Authentication.Certificate.Test
{
public class ClientCertificateAuthenticationTests
{
[Fact]
public async Task VerifySchemeDefaults()
{
var services = new ServiceCollection();
services.AddAuthentication().AddCertificate();
var sp = services.BuildServiceProvider();
var schemeProvider = sp.GetRequiredService<IAuthenticationSchemeProvider>();
var scheme = await schemeProvider.GetSchemeAsync(CertificateAuthenticationDefaults.AuthenticationScheme);
Assert.NotNull(scheme);
Assert.Equal("CertificateAuthenticationHandler", scheme.HandlerType.Name);
Assert.Null(scheme.DisplayName);
}
[Fact]
public void VerifyIsSelfSignedExtensionMethod()
{
Assert.True(Certificates.SelfSignedValidWithNoEku.IsSelfSigned());
}
[Fact]
public async Task VerifyValidSelfSignedWithClientEkuAuthenticates()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.SelfSigned,
Events = sucessfulValidationEvents
},
Certificates.SelfSignedValidWithClientEku);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task VerifyValidSelfSignedWithNoEkuAuthenticates()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.SelfSigned,
Events = sucessfulValidationEvents
},
Certificates.SelfSignedValidWithNoEku);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task VerifyValidSelfSignedWithClientEkuFailsWhenSelfSignedCertsNotAllowed()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.Chained
},
Certificates.SelfSignedValidWithClientEku);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
public async Task VerifyValidSelfSignedWithNoEkuFailsWhenSelfSignedCertsNotAllowed()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.Chained,
Events = sucessfulValidationEvents
},
Certificates.SelfSignedValidWithNoEku);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
public async Task VerifyValidSelfSignedWithServerFailsEvenIfSelfSignedCertsAreAllowed()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.SelfSigned,
Events = sucessfulValidationEvents
},
Certificates.SelfSignedValidWithServerEku);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
public async Task VerifyValidSelfSignedWithServerPassesWhenSelfSignedCertsAreAllowedAndPurposeValidationIsOff()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.SelfSigned,
ValidateCertificateUse = false,
Events = sucessfulValidationEvents
},
Certificates.SelfSignedValidWithServerEku);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task VerifyValidSelfSignedWithServerFailsPurposeValidationIsOffButSelfSignedCertsAreNotAllowed()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.Chained,
ValidateCertificateUse = false,
Events = sucessfulValidationEvents
},
Certificates.SelfSignedValidWithServerEku);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
public async Task VerifyExpiredSelfSignedFails()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.SelfSigned,
ValidateCertificateUse = false,
Events = sucessfulValidationEvents
},
Certificates.SelfSignedExpired);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
public async Task VerifyExpiredSelfSignedPassesIfDateRangeValidationIsDisabled()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.SelfSigned,
ValidateValidityPeriod = false,
Events = sucessfulValidationEvents
},
Certificates.SelfSignedExpired);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task VerifyNotYetValidSelfSignedFails()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.SelfSigned,
ValidateCertificateUse = false,
Events = sucessfulValidationEvents
},
Certificates.SelfSignedNotYetValid);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
public async Task VerifyNotYetValidSelfSignedPassesIfDateRangeValidationIsDisabled()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.SelfSigned,
ValidateValidityPeriod = false,
Events = sucessfulValidationEvents
},
Certificates.SelfSignedNotYetValid);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task VerifyFailingInTheValidationEventReturnsForbidden()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
ValidateCertificateUse = false,
Events = failedValidationEvents
},
Certificates.SelfSignedValidWithServerEku);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
public async Task DoingNothingInTheValidationEventReturnsOK()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.SelfSigned,
ValidateCertificateUse = false,
Events = unprocessedValidationEvents
},
Certificates.SelfSignedValidWithServerEku);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task VerifyNotSendingACertificateEndsUpInForbidden()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
Events = sucessfulValidationEvents
});
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
public async Task VerifyHeaderIsUsedIfCertIsNotPresent()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.SelfSigned,
Events = sucessfulValidationEvents
},
wireUpHeaderMiddleware : true);
var client = server.CreateClient();
client.DefaultRequestHeaders.Add("X-Client-Cert", Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData));
var response = await client.GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task VerifyHeaderEncodedCertFailsOnBadEncoding()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
Events = sucessfulValidationEvents
},
wireUpHeaderMiddleware: true);
var client = server.CreateClient();
client.DefaultRequestHeaders.Add("X-Client-Cert", "OOPS" + Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData));
var response = await client.GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
public async Task VerifySettingTheAzureHeaderOnTheForwarderOptionsWorks()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.SelfSigned,
Events = sucessfulValidationEvents
},
wireUpHeaderMiddleware: true,
headerName: "X-ARR-ClientCert");
var client = server.CreateClient();
client.DefaultRequestHeaders.Add("X-ARR-ClientCert", Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData));
var response = await client.GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task VerifyACustomHeaderFailsIfTheHeaderIsNotPresent()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
Events = sucessfulValidationEvents
},
wireUpHeaderMiddleware: true,
headerName: "X-ARR-ClientCert");
var client = server.CreateClient();
client.DefaultRequestHeaders.Add("random-Weird-header", Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData));
var response = await client.GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
public async Task VerifyNoEventWireupWithAValidCertificateCreatesADefaultUser()
{
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.SelfSigned
},
Certificates.SelfSignedValidWithNoEku);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
XElement responseAsXml = null;
if (response.Content != null &&
response.Content.Headers.ContentType != null &&
response.Content.Headers.ContentType.MediaType == "text/xml")
{
var responseContent = await response.Content.ReadAsStringAsync();
responseAsXml = XElement.Parse(responseContent);
}
Assert.NotNull(responseAsXml);
// There should always be an Issuer and a Thumbprint.
var actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == "issuer");
Assert.Single(actual);
Assert.Equal(Certificates.SelfSignedValidWithNoEku.Issuer, actual.First().Value);
actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Thumbprint);
Assert.Single(actual);
Assert.Equal(Certificates.SelfSignedValidWithNoEku.Thumbprint, actual.First().Value);
// Now the optional ones
if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.SubjectName.Name))
{
actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.X500DistinguishedName);
if (actual.Count() > 0)
{
Assert.Single(actual);
Assert.Equal(Certificates.SelfSignedValidWithNoEku.SubjectName.Name, actual.First().Value);
}
}
if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.SerialNumber))
{
actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.SerialNumber);
if (actual.Count() > 0)
{
Assert.Single(actual);
Assert.Equal(Certificates.SelfSignedValidWithNoEku.SerialNumber, actual.First().Value);
}
}
if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.DnsName, false)))
{
actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Dns);
if (actual.Count() > 0)
{
Assert.Single(actual);
Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.DnsName, false), actual.First().Value);
}
}
if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.EmailName, false)))
{
actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Email);
if (actual.Count() > 0)
{
Assert.Single(actual);
Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.EmailName, false), actual.First().Value);
}
}
if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.SimpleName, false)))
{
actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Name);
if (actual.Count() > 0)
{
Assert.Single(actual);
Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.SimpleName, false), actual.First().Value);
}
}
if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UpnName, false)))
{
actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Upn);
if (actual.Count() > 0)
{
Assert.Single(actual);
Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UpnName, false), actual.First().Value);
}
}
if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UrlName, false)))
{
actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Uri);
if (actual.Count() > 0)
{
Assert.Single(actual);
Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UrlName, false), actual.First().Value);
}
}
}
[Fact]
public async Task VerifyValidationEventPrincipalIsPropogated()
{
const string Expected = "John Doe";
var server = CreateServer(
new CertificateAuthenticationOptions
{
AllowedCertificateTypes = CertificateTypes.SelfSigned,
Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
// Make sure we get the validated principal
Assert.NotNull(context.Principal);
var claims = new[]
{
new Claim(ClaimTypes.Name, Expected, ClaimValueTypes.String, context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
return Task.CompletedTask;
}
}
},
Certificates.SelfSignedValidWithNoEku);
var response = await server.CreateClient().GetAsync("https://example.com/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
XElement responseAsXml = null;
if (response.Content != null &&
response.Content.Headers.ContentType != null &&
response.Content.Headers.ContentType.MediaType == "text/xml")
{
var responseContent = await response.Content.ReadAsStringAsync();
responseAsXml = XElement.Parse(responseContent);
}
Assert.NotNull(responseAsXml);
var actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Name);
Assert.Single(actual);
Assert.Equal(Expected, actual.First().Value);
Assert.Single(responseAsXml.Elements("claim"));
}
private static TestServer CreateServer(
CertificateAuthenticationOptions configureOptions,
X509Certificate2 clientCertificate = null,
Func<HttpContext, bool> handler = null,
Uri baseAddress = null,
bool wireUpHeaderMiddleware = false,
string headerName = "")
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.Use((context, next) =>
{
if (clientCertificate != null)
{
context.Connection.ClientCertificate = clientCertificate;
}
return next();
});
if (wireUpHeaderMiddleware)
{
app.UseCertificateForwarding();
}
app.UseAuthentication();
app.Use(async (context, next) =>
{
var request = context.Request;
var response = context.Response;
var authenticationResult = await context.AuthenticateAsync();
if (authenticationResult.Succeeded)
{
response.StatusCode = (int)HttpStatusCode.OK;
response.ContentType = "text/xml";
await response.WriteAsync("<claims>");
foreach (Claim claim in context.User.Claims)
{
await response.WriteAsync($"<claim Type=\"{claim.Type}\" Issuer=\"{claim.Issuer}\">{claim.Value}</claim>");
}
await response.WriteAsync("</claims>");
}
else
{
await context.ChallengeAsync();
}
});
})
.ConfigureServices(services =>
{
if (configureOptions != null)
{
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(options =>
{
options.AllowedCertificateTypes = configureOptions.AllowedCertificateTypes;
options.Events = configureOptions.Events;
options.ValidateCertificateUse = configureOptions.ValidateCertificateUse;
options.RevocationFlag = options.RevocationFlag;
options.RevocationMode = options.RevocationMode;
options.ValidateValidityPeriod = configureOptions.ValidateValidityPeriod;
});
}
else
{
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate();
}
if (wireUpHeaderMiddleware && !string.IsNullOrEmpty(headerName))
{
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = headerName;
});
}
});
var server = new TestServer(builder)
{
BaseAddress = baseAddress
};
return server;
}
private CertificateAuthenticationEvents sucessfulValidationEvents = new CertificateAuthenticationEvents()
{
OnCertificateValidated = context =>
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
return Task.CompletedTask;
}
};
private CertificateAuthenticationEvents failedValidationEvents = new CertificateAuthenticationEvents()
{
OnCertificateValidated = context =>
{
context.Fail("Not validated");
return Task.CompletedTask;
}
};
private CertificateAuthenticationEvents unprocessedValidationEvents = new CertificateAuthenticationEvents()
{
OnCertificateValidated = context =>
{
return Task.CompletedTask;
}
};
private static class Certificates
{
public static X509Certificate2 SelfSignedValidWithClientEku { get; private set; } =
new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedClientEkuCertificate.cer"));
public static X509Certificate2 SelfSignedValidWithNoEku { get; private set; } =
new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedNoEkuCertificate.cer"));
public static X509Certificate2 SelfSignedValidWithServerEku { get; private set; } =
new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedServerEkuCertificate.cer"));
public static X509Certificate2 SelfSignedNotYetValid { get; private set; } =
new X509Certificate2(GetFullyQualifiedFilePath("selfSignedNoEkuCertificateNotValidYet.cer"));
public static X509Certificate2 SelfSignedExpired { get; private set; } =
new X509Certificate2(GetFullyQualifiedFilePath("selfSignedNoEkuCertificateExpired.cer"));
private static string GetFullyQualifiedFilePath(string filename)
{
var filePath = Path.Combine(AppContext.BaseDirectory, filename);
if (!File.Exists(filePath))
{
throw new FileNotFoundException(filePath);
}
return filePath;
}
}
}
}

View File

@ -22,6 +22,10 @@
<Content Include="WsFederation\ValidToken.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="$(SharedSourceRoot)test\Certificates\*.cer">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
@ -34,6 +38,7 @@
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Authentication.Certificate" />
<Reference Include="Microsoft.AspNetCore.Authentication.Cookies" />
<Reference Include="Microsoft.AspNetCore.Authentication.Facebook" />
<Reference Include="Microsoft.AspNetCore.Authentication.Google" />
@ -42,6 +47,7 @@
<Reference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
<Reference Include="Microsoft.AspNetCore.Authentication.Twitter" />
<Reference Include="Microsoft.AspNetCore.Authentication.WsFederation" />
<Reference Include="Microsoft.AspNetCore.HttpOverrides" />
<Reference Include="Microsoft.AspNetCore.TestHost" />
</ItemGroup>

View File

@ -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}