mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-01-11 22:30:41 +01:00
Add client certificate support (#862)
* Add client certificate support * Add missing test certificate file * Review fixes * Review fixes * Review fixes * Review fixes
This commit is contained in:
@@ -96,4 +96,16 @@ public class SettingsModel
|
||||
/// Default value = "All".
|
||||
/// </summary>
|
||||
public QueryParameterMultipleValueSupport? QueryParameterMultipleValueSupport { get; set; }
|
||||
|
||||
#if NETSTANDARD1_3_OR_GREATER || NET461
|
||||
/// <summary>
|
||||
/// Server client certificate mode
|
||||
/// </summary>
|
||||
public ClientCertificateMode ClientCertificateMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to accept any client certificate
|
||||
/// </summary>
|
||||
public bool AcceptAnyClientCertificate { get; set; }
|
||||
#endif
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
#if NETSTANDARD1_3_OR_GREATER || NET461
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
#endif
|
||||
using WireMock.Types;
|
||||
using WireMock.Util;
|
||||
|
||||
@@ -134,4 +137,11 @@ public interface IRequestMessage
|
||||
/// Gets the origin
|
||||
/// </summary>
|
||||
string Origin { get; }
|
||||
|
||||
#if NETSTANDARD1_3_OR_GREATER || NET461
|
||||
/// <summary>
|
||||
/// Gets the connection's client certificate
|
||||
/// </summary>
|
||||
X509Certificate2? ClientCertificate { get; }
|
||||
#endif
|
||||
}
|
||||
31
src/WireMock.Net.Abstractions/Types/ClientCertificateMode.cs
Normal file
31
src/WireMock.Net.Abstractions/Types/ClientCertificateMode.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace WireMock.Types;
|
||||
|
||||
#if NETSTANDARD1_3_OR_GREATER || NET461
|
||||
/// <summary>
|
||||
/// Describes the client certificate requirements for a HTTPS connection.
|
||||
/// This enum is the same as https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.clientcertificatemode
|
||||
/// </summary>
|
||||
public enum ClientCertificateMode
|
||||
{
|
||||
/// <summary>
|
||||
/// A client certificate is not required and will not be requested from clients.
|
||||
/// </summary>
|
||||
NoCertificate,
|
||||
|
||||
/// <summary>
|
||||
/// A client certificate will be requested; however, authentication will not fail if a certificate is not provided by the client.
|
||||
/// </summary>
|
||||
AllowCertificate,
|
||||
|
||||
/// <summary>
|
||||
/// A client certificate will be requested, and the client must provide a valid certificate for authentication to succeed.
|
||||
/// </summary>
|
||||
RequireCertificate,
|
||||
|
||||
/// <summary>
|
||||
/// A client certificate is not required and will not be requested from clients at the start of the connection.
|
||||
/// It may be requested by the application later.
|
||||
/// </summary>
|
||||
DelayCertificate,
|
||||
}
|
||||
#endif
|
||||
@@ -4,7 +4,7 @@
|
||||
<Description>Commonly used models, enumerations and types.</Description>
|
||||
<AssemblyTitle>WireMock.Net.Abstractions</AssemblyTitle>
|
||||
<Authors>Stef Heyenrath</Authors>
|
||||
<TargetFrameworks>net45;net451;netstandard1.0;netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
<TargetFrameworks>net45;net451;net461;netstandard1.0;netstandard1.3;netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591;8603</NoWarn>
|
||||
<AssemblyName>WireMock.Net.Abstractions</AssemblyName>
|
||||
@@ -41,6 +41,16 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="$(TargetFramework.StartsWith('netstandard')) and '$(TargetFramework)' != 'netstandard1.0'">
|
||||
<PackageReference Include="System.Security.Cryptography.X509Certificates" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net461'">
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net46" Version="1.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<!--<ItemGroup Condition="'$(TargetFramework)' == 'net45'">
|
||||
<PackageReference Include="Nullable" Version="1.2.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using WireMock.HttpsCertificate;
|
||||
using CertificateLoader = WireMock.HttpsCertificate.CertificateLoader;
|
||||
|
||||
namespace WireMock.Owin
|
||||
{
|
||||
@@ -26,21 +27,25 @@ namespace WireMock.Owin
|
||||
{
|
||||
kestrelOptions.ListenAnyIP(urlDetail.Port, listenOptions =>
|
||||
{
|
||||
if (wireMockMiddlewareOptions.CustomCertificateDefined)
|
||||
listenOptions.UseHttps(options =>
|
||||
{
|
||||
listenOptions.UseHttps(CertificateLoader.LoadCertificate(
|
||||
wireMockMiddlewareOptions.X509StoreName,
|
||||
wireMockMiddlewareOptions.X509StoreLocation,
|
||||
wireMockMiddlewareOptions.X509ThumbprintOrSubjectName,
|
||||
wireMockMiddlewareOptions.X509CertificateFilePath,
|
||||
wireMockMiddlewareOptions.X509CertificatePassword,
|
||||
urlDetail.Host)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
listenOptions.UseHttps();
|
||||
}
|
||||
if (wireMockMiddlewareOptions.CustomCertificateDefined)
|
||||
{
|
||||
options.ServerCertificate = CertificateLoader.LoadCertificate(
|
||||
wireMockMiddlewareOptions.X509StoreName,
|
||||
wireMockMiddlewareOptions.X509StoreLocation,
|
||||
wireMockMiddlewareOptions.X509ThumbprintOrSubjectName,
|
||||
wireMockMiddlewareOptions.X509CertificateFilePath,
|
||||
wireMockMiddlewareOptions.X509CertificatePassword,
|
||||
urlDetail.Host);
|
||||
}
|
||||
|
||||
options.ClientCertificateMode = (ClientCertificateMode) wireMockMiddlewareOptions.ClientCertificateMode;
|
||||
if (wireMockMiddlewareOptions.AcceptAnyClientCertificate)
|
||||
{
|
||||
options.ClientCertificateValidation = (_, _, _) => true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
else
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Server.Kestrel;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using WireMock.HttpsCertificate;
|
||||
@@ -23,21 +24,22 @@ internal partial class AspNetCoreSelfHost
|
||||
{
|
||||
if (urlDetail.IsHttps)
|
||||
{
|
||||
if (wireMockMiddlewareOptions.CustomCertificateDefined)
|
||||
options.UseHttps(new HttpsConnectionFilterOptions
|
||||
{
|
||||
options.UseHttps(CertificateLoader.LoadCertificate(
|
||||
wireMockMiddlewareOptions.X509StoreName,
|
||||
wireMockMiddlewareOptions.X509StoreLocation,
|
||||
wireMockMiddlewareOptions.X509ThumbprintOrSubjectName,
|
||||
wireMockMiddlewareOptions.X509CertificateFilePath,
|
||||
wireMockMiddlewareOptions.X509CertificatePassword,
|
||||
urlDetail.Host)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
options.UseHttps(PublicCertificateHelper.GetX509Certificate2());
|
||||
}
|
||||
ServerCertificate = wireMockMiddlewareOptions.CustomCertificateDefined
|
||||
? CertificateLoader.LoadCertificate(
|
||||
wireMockMiddlewareOptions.X509StoreName,
|
||||
wireMockMiddlewareOptions.X509StoreLocation,
|
||||
wireMockMiddlewareOptions.X509ThumbprintOrSubjectName,
|
||||
wireMockMiddlewareOptions.X509CertificateFilePath,
|
||||
wireMockMiddlewareOptions.X509CertificatePassword,
|
||||
urlDetail.Host)
|
||||
: PublicCertificateHelper.GetX509Certificate2(),
|
||||
ClientCertificateMode = (ClientCertificateMode) wireMockMiddlewareOptions.ClientCertificateMode,
|
||||
ClientCertificateValidation = wireMockMiddlewareOptions.AcceptAnyClientCertificate
|
||||
? (_, _, _) => true
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ internal interface IWireMockMiddlewareOptions
|
||||
Action<IServiceCollection>? AdditionalServiceRegistration { get; set; }
|
||||
|
||||
CorsPolicyOptions? CorsPolicyOptions { get; set; }
|
||||
|
||||
ClientCertificateMode ClientCertificateMode { get; set; }
|
||||
|
||||
bool AcceptAnyClientCertificate { get; set; }
|
||||
#endif
|
||||
|
||||
IFileSystemHandler? FileSystemHandler { get; set; }
|
||||
|
||||
@@ -68,7 +68,21 @@ namespace WireMock.Owin.Mappers
|
||||
body = await BodyParser.ParseAsync(bodyParserSettings).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return new RequestMessage(options, urlDetails, method, clientIP, body, headers, cookies) { DateTime = DateTime.UtcNow };
|
||||
return new RequestMessage(
|
||||
options,
|
||||
urlDetails,
|
||||
method,
|
||||
clientIP,
|
||||
body,
|
||||
headers,
|
||||
cookies
|
||||
#if USE_ASPNETCORE
|
||||
, await request.HttpContext.Connection.GetClientCertificateAsync()
|
||||
#endif
|
||||
)
|
||||
{
|
||||
DateTime = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private static (UrlDetails UrlDetails, string ClientIP) ParseRequest(IRequest request)
|
||||
|
||||
@@ -42,6 +42,11 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions
|
||||
public Action<IServiceCollection>? AdditionalServiceRegistration { get; set; }
|
||||
|
||||
public CorsPolicyOptions? CorsPolicyOptions { get; set; }
|
||||
|
||||
public ClientCertificateMode ClientCertificateMode { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool AcceptAnyClientCertificate { get; set; }
|
||||
#endif
|
||||
|
||||
/// <inheritdoc cref="IWireMockMiddlewareOptions.FileSystemHandler"/>
|
||||
|
||||
@@ -4,6 +4,9 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
#if USE_ASPNETCORE
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
#endif
|
||||
using Stef.Validation;
|
||||
using WireMock.Models;
|
||||
using WireMock.Owin;
|
||||
@@ -92,6 +95,11 @@ public class RequestMessage : IRequestMessage
|
||||
/// <inheritdoc cref="IRequestMessage.Origin" />
|
||||
public string Origin { get; }
|
||||
|
||||
#if USE_ASPNETCORE
|
||||
/// <inheritdoc cref="IRequestMessage.ClientCertificate" />
|
||||
public X509Certificate2? ClientCertificate { get; }
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Used for Unit Testing
|
||||
/// </summary>
|
||||
@@ -115,13 +123,20 @@ public class RequestMessage : IRequestMessage
|
||||
/// <param name="bodyData">The BodyData.</param>
|
||||
/// <param name="headers">The headers.</param>
|
||||
/// <param name="cookies">The cookies.</param>
|
||||
#if USE_ASPNETCORE
|
||||
/// <param name="clientCertificate">The client certificate</param>
|
||||
#endif
|
||||
internal RequestMessage(
|
||||
IWireMockMiddlewareOptions? options,
|
||||
UrlDetails urlDetails, string method,
|
||||
string clientIP,
|
||||
IBodyData? bodyData = null,
|
||||
IDictionary<string, string[]>? headers = null,
|
||||
IDictionary<string, string>? cookies = null)
|
||||
IDictionary<string, string>? cookies = null
|
||||
#if USE_ASPNETCORE
|
||||
, X509Certificate2? clientCertificate = null
|
||||
#endif
|
||||
)
|
||||
{
|
||||
Guard.NotNull(urlDetails, nameof(urlDetails));
|
||||
Guard.NotNull(method, nameof(method));
|
||||
@@ -156,6 +171,9 @@ public class RequestMessage : IRequestMessage
|
||||
Cookies = cookies;
|
||||
RawQuery = urlDetails.Url.Query;
|
||||
Query = QueryStringParser.Parse(RawQuery, options?.QueryParameterMultipleValueSupport);
|
||||
#if USE_ASPNETCORE
|
||||
ClientCertificate = clientCertificate;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -226,7 +226,9 @@ public partial class WireMockServer
|
||||
QueryParameterMultipleValueSupport = _settings.QueryParameterMultipleValueSupport,
|
||||
|
||||
#if USE_ASPNETCORE
|
||||
CorsPolicyOptions = _settings.CorsPolicyOptions?.ToString()
|
||||
CorsPolicyOptions = _settings.CorsPolicyOptions?.ToString(),
|
||||
ClientCertificateMode = _settings.ClientCertificateMode,
|
||||
AcceptAnyClientCertificate = _settings.AcceptAnyClientCertificate
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -275,6 +277,9 @@ public partial class WireMockServer
|
||||
_settings.CorsPolicyOptions = corsPolicyOptions;
|
||||
_options.CorsPolicyOptions = corsPolicyOptions;
|
||||
}
|
||||
|
||||
_options.ClientCertificateMode = _settings.ClientCertificateMode;
|
||||
_options.AcceptAnyClientCertificate = _settings.AcceptAnyClientCertificate;
|
||||
#endif
|
||||
|
||||
return ResponseMessageBuilder.Create("Settings updated");
|
||||
|
||||
@@ -331,6 +331,8 @@ public partial class WireMockServer : IWireMockServer
|
||||
#if USE_ASPNETCORE
|
||||
_options.AdditionalServiceRegistration = _settings.AdditionalServiceRegistration;
|
||||
_options.CorsPolicyOptions = _settings.CorsPolicyOptions;
|
||||
_options.ClientCertificateMode = _settings.ClientCertificateMode;
|
||||
_options.AcceptAnyClientCertificate = _settings.AcceptAnyClientCertificate;
|
||||
|
||||
_httpServer = new AspNetCoreSelfHost(_options, urlOptions);
|
||||
#else
|
||||
|
||||
@@ -235,6 +235,19 @@ public class WireMockServerSettings
|
||||
[PublicAPI]
|
||||
public bool CustomCertificateDefined => CertificateSettings?.IsDefined == true;
|
||||
|
||||
#if USE_ASPNETCORE
|
||||
/// <summary>
|
||||
/// Client certificate mode for the server
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public ClientCertificateMode ClientCertificateMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to accept any client certificate
|
||||
/// </summary>
|
||||
public bool AcceptAnyClientCertificate { get; set; }
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Defines the global IWebhookSettings to use.
|
||||
/// </summary>
|
||||
|
||||
@@ -64,6 +64,8 @@ public static class WireMockServerSettingsParser
|
||||
|
||||
#if USE_ASPNETCORE
|
||||
settings.CorsPolicyOptions = parser.GetEnumValue(nameof(WireMockServerSettings.CorsPolicyOptions), CorsPolicyOptions.None);
|
||||
settings.ClientCertificateMode = parser.GetEnumValue(nameof(WireMockServerSettings.ClientCertificateMode), ClientCertificateMode.NoCertificate);
|
||||
settings.AcceptAnyClientCertificate = parser.GetBoolValue(nameof(WireMockServerSettings.AcceptAnyClientCertificate));
|
||||
#endif
|
||||
|
||||
var loggerType = parser.GetStringValue("WireMockLogger");
|
||||
|
||||
7
test/WireMock.Net.Tests/README.md
Normal file
7
test/WireMock.Net.Tests/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## Creating a client certificate like client_cert.pfx
|
||||
|
||||
Follow the instructions to [create a root certificate](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-7.0#create-root-ca),
|
||||
then [trust it](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-7.0#install-in-the-trusted-root)
|
||||
and [create a child certificate from it](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-7.0#create-child-certificate-from-root-certificate).
|
||||
|
||||
Since the root certificate of `client_cert.pfx` is obviously not trusted automatically by cloning this repo, the tests in `WireMockServerTests.ClientCertificate.cs` set `WireMockServerSettings.AcceptAnyClientCertificate` to `true` so that tests pass even if the device hasn't trusted the root of `client_cert.pfx`.
|
||||
@@ -96,6 +96,10 @@
|
||||
<None Update="__admin\mappings\subdirectory\*.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="client_cert.pfx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<DependentUpon>WireMockServerTests.ClientCertificate.cs</DependentUpon>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#if !NET451 && !NET452
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using WireMock.RequestBuilders;
|
||||
using WireMock.ResponseBuilders;
|
||||
using WireMock.Server;
|
||||
using WireMock.Settings;
|
||||
using WireMock.Types;
|
||||
using Xunit;
|
||||
|
||||
namespace WireMock.Net.Tests;
|
||||
|
||||
public partial class WireMockServerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task WireMockServer_WithRequiredClientCertificates_Should_Work_Correct()
|
||||
{
|
||||
// Arrange
|
||||
var settings = new WireMockServerSettings
|
||||
{
|
||||
ClientCertificateMode = ClientCertificateMode.RequireCertificate,
|
||||
AcceptAnyClientCertificate = true,
|
||||
UseSSL = true,
|
||||
};
|
||||
|
||||
using var server = WireMockServer.Start(settings);
|
||||
|
||||
server.Given(Request.Create().WithPath("/*"))
|
||||
.RespondWith(Response.Create().WithCallback(message => new ResponseMessage
|
||||
{
|
||||
StatusCode = message.ClientCertificate?.Thumbprint == "2E32E3528C87046A95B8B0BA172A1597C3AF3A9D"
|
||||
? 200
|
||||
: 403
|
||||
}));
|
||||
|
||||
var certificates = new X509Certificate2Collection();
|
||||
certificates.Import("client_cert.pfx", "1234", X509KeyStorageFlags.Exportable);
|
||||
|
||||
var httpMessageHandler = new HttpClientHandler
|
||||
{
|
||||
ServerCertificateCustomValidationCallback = (_, _, _, _) => true,
|
||||
};
|
||||
httpMessageHandler.ClientCertificates.AddRange(certificates);
|
||||
|
||||
// Act
|
||||
var response = await new HttpClient(httpMessageHandler)
|
||||
.GetAsync("https://localhost:" + server.Ports[0] + "/foo")
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
BIN
test/WireMock.Net.Tests/client_cert.pfx
Normal file
BIN
test/WireMock.Net.Tests/client_cert.pfx
Normal file
Binary file not shown.
Reference in New Issue
Block a user