Add CertificateAuthentication (#9756)
This commit is contained in:
parent
4dde8b9461
commit
b75b892eac
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Certificate.Sample.Controllers
|
||||
{
|
||||
public class HomeController : Controller
|
||||
{
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<h1>Hello @User.Identity.Name</h1>
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
### 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;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue