Add client certificate to .NET Core HttpClient
Asked Answered
H

9

95

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.

Horvath answered 13/10, 2016 at 6:44 Comment(0)
R
2

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"];
    }
}
Rexer answered 28/11, 2019 at 15:54 Comment(0)
U
79

I ran a fresh install for my platform (Linux Mint 17.3) following these steps: .NET Tutorial - Hello World in 5 minutes. I created a new console application targeting the netcoreapp1.0 framework, was able to submit a client certificate; however, I did receive "SSL connect error" (CURLE_SSL_CONNECT_ERROR 35) while testing, even though I used a valid certificate. My error could be specific to my libcurl.

I ran the exact same thing on Windows 7 and it worked exactly as needed.

// using System.Net.Http;
// using System.Security.Authentication;
// using System.Security.Cryptography.X509Certificates;

var handler = new HttpClientHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.SslProtocols = SslProtocols.Tls12;
handler.ClientCertificates.Add(new X509Certificate2("cert.crt"));
var client = new HttpClient(handler);
var result = client.GetAsync("https://apitest.startssl.com").GetAwaiter().GetResult();
Unemployed answered 21/10, 2016 at 4:13 Comment(11)
woha it worked mate, Thank you so much taking time to help me. All these days I was off for vacation.Horvath
I'm curious. Did you get this to work on a linux machine or windows?Bulletproof
I got it working on both Windows (10) and Linux (Mint 17.3, 18, and 18.1). I did get a "SSL connect error" (CURLE_SSL_CONNECT_ERROR 35) when I tried using a certificate without a private key.Unemployed
@yfisaqt can you elaborate on how you saw the CURL error? I am finding I can sign with a cert on windows, but the same code on Ubuntu 17.04 is causing errors.Ginter
@Ginter if my cert file was just the public key, I would get error 35, but only on Linux. If cert has the private key, the error would go away. In the Windows world, this could be a PFX file.Unemployed
What does your cert.crt file look like?Contrary
@Contrary To be technical, it depends on the file format. Keep in mind this was answered before I upgraded to .NET Core SDK 2, so this may behave a little differently in 2.0. At the time I answered this, Windows required the file to be a PFX but on Linux I was able to use a regular certificate (no private key).Unemployed
I'm using .NET Core SDK 2 as well and having trouble creating a certificate to access an api that isn't SSL secure. Was hoping someone here could help. I need this to be able to run on Windows or Linux.Contrary
@Contrary I would recommend asking a new question, explain what you've tried, what works, what doesn't, and what you're trying to do.Unemployed
This code is still working in 2022 :) Only difference for me was that I had to reference a .pfx file instead of .cerTuatara
Unlikely. Too bad to use var client = new HttpClient() in 2022. Use IHttpClientFactory instead.Comeback
A
35

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);
}
Arthropod answered 13/1, 2018 at 20:18 Comment(2)
How do you confirm, that the client request is made with HTTP/1.1? There is no client certificate authentication in HTTP/2.Twelvetone
I know this is old, but to answer your comment: handler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;Submissive
L
7

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") }
});
Lipson answered 10/10, 2021 at 13:40 Comment(2)
It is not working at linux machine.Markettamarkey
@Markettamarkey Have you figured out how to make it work on Linux?Robin
F
4

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.

Ferrand answered 14/10, 2016 at 8:46 Comment(1)
thanks for taking time answer, but I was using this for asp.net core. Above code worked for me.Horvath
F
2

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://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler?view=netframework-4.7.1

Ferrotype answered 25/3, 2019 at 16:8 Comment(3)
handler.ClientCertificates.Add(new X509Certificate2("cert.crt")) works only from Framework 4.8 (learn.microsoft.com/ru-ru/dotnet/api/…)Appendant
@Appendant 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.Ferrotype
Version specification seems to be reversed, I think it should read: Can be used for both .NET Core >2.0 and .NET Framework >4.7.1: Note inversion of sign(s).Upu
R
2

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"];
    }
}
Rexer answered 28/11, 2019 at 15:54 Comment(0)
O
1

I thought the best answer for this was provided here.

By utilizing the X-ARR-ClientCert header you can provide the certificate information.

An adapted solution is here:

X509Certificate2 certificate;
var handler = new HttpClientHandler {
    ClientCertificateOptions = ClientCertificateOption.Manual,
    SslProtocols = SslProtocols.Tls12
};
handler.ClientCertificates.Add(certificate);
handler.CheckCertificateRevocationList = false;
// this is required to get around self-signed certs
handler.ServerCertificateCustomValidationCallback =
    (httpRequestMessage, cert, cetChain, policyErrors) => {
        return true;
    };
var client = new HttpClient(handler);
requestMessage.Headers.Add("X-ARR-ClientCert", certificate.GetRawCertDataString());
requestMessage.Content = new StringContent(JsonConvert.SerializeObject(requestData), Encoding.UTF8, "application/json");
var response = await client.SendAsync(requestMessage);

if (response.IsSuccessStatusCode)
{
    var responseContent = await response.Content.ReadAsStringAsync();
    var keyResponse = JsonConvert.DeserializeObject<KeyResponse>(responseContent);

    return keyResponse;
}

And in your .net core server's Startup routine:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddCertificateForwarding(options => {
        options.CertificateHeader = "X-ARR-ClientCert";
        options.HeaderConverter = (headerValue) => {
            X509Certificate2 clientCertificate = null;
            try
            {
                if (!string.IsNullOrWhiteSpace(headerValue))
                {
                    var bytes = ConvertHexToBytes(headerValue);
                    clientCertificate = new X509Certificate2(bytes);
                }
            }
            catch (Exception)
            {
                // invalid certificate
            }

            return clientCertificate;
        };
    });
}
Ogpu answered 4/10, 2019 at 0:56 Comment(2)
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.Foreigner
This is relevant when proxy/load balancer does resend client certificate in X-Client-Cert header and ASP.NET Core app needs to use this certificate to create user ClaimsPrincipal from that certificate. Actually the default options are enough in that case and only this line is needed for setup: app.UseCertificateForwarding();Submarginal
M
0

If you look at the .NET Standard reference for the HttpClientHandler class, you can see that the ClientCertificates property exists, but is hidden due to the use of EditorBrowsableState.Never. This prevents IntelliSense from showing it, but will still work in code that uses it.

[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
public System.Security.Cryptography.X509Certificates.X509CertificateCollection ClientCertificates { get; }
Mikaela answered 7/8, 2018 at 14:42 Comment(0)
P
0

I deleted Local State file from AppData/Local/BROWSERNAME/UserData here.

By the way this deletion can destroy your bookmarks or passwords etc.

Thanks.

Pule answered 15/9, 2023 at 12:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.