Add client certificate to .NET Core HttpClient

129,251

Solution 1

Make all configuration in Main() like this:

public static void Main(string[] args)
{
    var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
    var logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();
    string env="", sbj="", crtf = "";

    try
    {
        var whb = WebHost.CreateDefaultBuilder(args).UseContentRoot(Directory.GetCurrentDirectory());

        var environment = env = whb.GetSetting("environment");
        var subjectName = sbj = CertificateHelper.GetCertificateSubjectNameBasedOnEnvironment(environment);
        var certificate = CertificateHelper.GetServiceCertificate(subjectName);

        crtf = certificate != null ? certificate.Subject : "It will after the certification";

        if (certificate == null) // present apies even without server certificate but dont give permission on authorization
        {
            var host = whb
                .ConfigureKestrel(_ => { })
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup<Startup>()
                .UseConfiguration(configuration)
                .UseSerilog((context, config) =>
                {
                    config.ReadFrom.Configuration(context.Configuration);
                })
                .Build();
            host.Run();
        }
        else
        {
            var host = whb
                .ConfigureKestrel(options =>
                {
                    options.Listen(new IPEndPoint(IPAddress.Loopback, 443), listenOptions =>
                    {
                        var httpsConnectionAdapterOptions = new HttpsConnectionAdapterOptions()
                        {
                            ClientCertificateMode = ClientCertificateMode.AllowCertificate,
                            SslProtocols = System.Security.Authentication.SslProtocols.Tls12,
                            ServerCertificate = certificate
                        };
                        listenOptions.UseHttps(httpsConnectionAdapterOptions);
                    });
                })
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseUrls("https://*:443")
                .UseStartup<Startup>()
                .UseConfiguration(configuration)
                .UseSerilog((context, config) =>
                {
                    config.ReadFrom.Configuration(context.Configuration);
                })
                .Build();
            host.Run();
        }

        Log.Logger.Information("Information: Environment = " + env +
            " Subject = " + sbj +
            " Certificate Subject = " + crtf);
    }
    catch(Exception ex)
    {
        Log.Logger.Error("Main handled an exception: Environment = " + env +
            " Subject = " + sbj +
            " Certificate Subject = " + crtf +
            " Exception Detail = " + ex.Message);
    }
}

Configure file startup.cs like this:

#region 2way SSL settings
services.AddMvc();
services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = CertificateAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = CertificateAuthenticationDefaults.AuthenticationScheme;
})
.AddCertificateAuthentication(certOptions =>
{
    var certificateAndRoles = new List<CertficateAuthenticationOptions.CertificateAndRoles>();
    Configuration.GetSection("AuthorizedCertficatesAndRoles:CertificateAndRoles").Bind(certificateAndRoles);
    certOptions.CertificatesAndRoles = certificateAndRoles.ToArray();
});

services.AddAuthorization(options =>
{
    options.AddPolicy("CanAccessAdminMethods", policy => policy.RequireRole("Admin"));
    options.AddPolicy("CanAccessUserMethods", policy => policy.RequireRole("User"));
});
#endregion

The certificate helper

public class CertificateHelper
{
    protected internal static X509Certificate2 GetServiceCertificate(string subjectName)
    {
        using (var certStore = new X509Store(StoreName.Root, StoreLocation.LocalMachine))
        {
            certStore.Open(OpenFlags.ReadOnly);
            var certCollection = certStore.Certificates.Find(
                                       X509FindType.FindBySubjectDistinguishedName, subjectName, true);
            X509Certificate2 certificate = null;
            if (certCollection.Count > 0)
            {
                certificate = certCollection[0];
            }
            return certificate;
        }
    }

    protected internal static string GetCertificateSubjectNameBasedOnEnvironment(string environment)
    {
        var builder = new ConfigurationBuilder()
         .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile($"appsettings.{environment}.json", optional: false);

        var configuration = builder.Build();
        return configuration["ServerCertificateSubject"];
    }
}

Solution 2

I have a similar project where I communicate between services as well as between mobile and desktop with a service.

We use the Authenticode certificate from the EXE file to ensure that it's our binaries that are doing the requests.

On the requesting side (over simplified for the post).

Module m = Assembly.GetEntryAssembly().GetModules()[0];
using (var cert = m.GetSignerCertificate())
using (var cert2 = new X509Certificate2(cert))
{
   var _clientHandler = new HttpClientHandler();
   _clientHandler.ClientCertificates.Add(cert2);
   _clientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
   var myModel = new Dictionary<string, string>
   {
       { "property1","value" },
       { "property2","value" },
   };
   using (var content = new FormUrlEncodedContent(myModel))
   using (var _client = new HttpClient(_clientHandler))
   using (HttpResponseMessage response = _client.PostAsync($"{url}/{controler}/{action}", content).Result)
   {
       response.EnsureSuccessStatusCode();
       string jsonString = response.Content.ReadAsStringAsync().Result;
       var myClass = JsonConvert.DeserializeObject<MyClass>(jsonString);
    }
}

I then use the following code on the action that gets the request:

X509Certificate2 clientCertInRequest = Request.HttpContext.Connection.ClientCertificate;
if (!clientCertInRequest.Verify() || !AllowedCerialNumbers(clientCertInRequest.SerialNumber))
{
    Response.StatusCode = 404;
    return null;
}

We rather provide a 404 than a 500 as we like those that are trying URLs to get a bad request rather then let them know that they are "on the right track"

In .NET Core, the way to get the certificate is no longer by going over Module. The modern way that might work for you is:

private static X509Certificate2? Signer()
{
    using var cert = X509Certificate2.CreateFromSignedFile(Assembly.GetExecutingAssembly().Location);
    if (cert is null)
        return null;

    return new X509Certificate2(cert);
}

Solution 3

I'm not using .NET for my client, but server side it can be configured simply via IIS by deploying my ASP.NET Core website behind IIS, configuring IIS for HTTPS + client certificates:

IIS client certificate setting:

Then you can get it simply in the code:

        var clientCertificate = await HttpContext.Connection.GetClientCertificateAsync();

        if(clientCertificate!=null)
            return new ContentResult() { Content = clientCertificate.Subject };

It's working fine for me, but I'm using curl or chrome as clients, not the .NET ones. During the HTTPS handshake, the client gets a request from the server to provide a certificate and send it to the server.

If you are using a .NET Core client, it can't have platform-specific code and it would make sense if it couldn't connect itself to any OS specific certificates store, to extract it and send it to the server. If you were compiling against .NET 4.5.x then it seems easy:

Using HttpClient with SSL/TLS-based client side authentication

It's like when you compile curl. If you want to be able to connect it to the Windows certificates store, you have to compile it against some specific Windows library.

Solution 4

After a lot of testing with this issue I ended up with this.

  1. Using SSL, I created a pfx file from the certificate and key.
  2. Create a HttpClient as follows:
_httpClient = new(new HttpClientHandler
{
    ClientCertificateOptions = ClientCertificateOption.Manual,
    SslProtocols = SslProtocols.Tls12,
    ClientCertificates = { new X509Certificate2(@"C:\kambiDev.pfx") }
});

Solution 5

Can be used for both .NET Core 2.0< and .NET Framework 4.7.1<:

var handler = new HttpClientHandler();
handler.ClientCertificates.Add(new X509Certificate2("cert.crt"));
var client = new HttpClient(handler);

https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler?view=netframework-4.7.1

Share:
129,251
Krishna.N
Author by

Krishna.N

Updated on July 05, 2022

Comments

  • Krishna.N
    Krishna.N almost 2 years

    I was playing around with .NET Core and building an API that utilizes payment APIs. There's a client certificate that needs to be added to the request for two-way SSL authentication. How can I achieve this in .NET Core using HttpClient?

    I have looked at various articles and found that HttpClientHandler doesn't provide any option to add client certificates.

  • Krishna.N
    Krishna.N about 7 years
    thanks for taking time answer, but I was using this for asp.net core. Above code worked for me.
  • Jari Turkia
    Jari Turkia about 5 years
    How do you confirm, that the client request is made with HTTP/1.1? There is no client certificate authentication in HTTP/2.
  • Kate
    Kate over 4 years
    handler.ClientCertificates.Add(new X509Certificate2("cert.crt")) works only from Framework 4.8 (docs.microsoft.com/ru-ru/dotnet/api/…)
  • Ogglas
    Ogglas over 4 years
    @Kate Miss from me that it did not work with .Net Framework 4.5< but the code works with .Net Framework 4.7.1. Tested it locally and documentation says so as well. Updated the answer.
  • Michael
    Michael almost 4 years
    I know this is old, but to answer your comment: handler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
  • Austin
    Austin about 3 years
    X-ARR-ClientCert is an Azure-specific thing and this code does not actually authenticate that the client has the private key for the cert. It just attaches the public key the request. Note that the linked article has been corrected to actually use the client cert correctly.