Thirdparty certificate authentication in .net core API between client and server API

942 Views Asked by At

I am trying to implement the certificate authentication in .net core API(Server/target) and this API will be invoked in to another API(Client) .Here is the piece of code of client api which makes request to server/target api.But I'm facing an error on the server/target api .I'm running these two services from local and both certificates have already installed enter image description here Client side controller logic

[HttpGet]    
        public async Task<List<WeatherForecast>> Get()
        {
            
            List<WeatherForecast> weatherForecastList = new List<WeatherForecast>();
            X509Certificate2 clientCert = Authentication.GetClientCertificate();
            if (clientCert == null)
            {
                HttpActionContext actionContext = null;
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                {
                    ReasonPhrase = "Client Certificate Required"
                };
            }
            HttpClientHandler requestHandler = new HttpClientHandler();
            requestHandler.ClientCertificates.Add(clientCert);
            requestHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
            HttpClient client = new HttpClient(requestHandler)
            {
                BaseAddress = new Uri("https://localhost:11111/ServerAPI")
            };
            client.DefaultRequestHeaders
                      .Accept
                      .Add(new MediaTypeWithQualityHeaderValue("application/xml"));//ACCEPT head
            
            using (var httpClient = new HttpClient())
            {
                //httpClient.DefaultRequestHeaders.Accept.Clear();
                var request = new HttpRequestMessage()
                {
                    RequestUri = new Uri("https://localhost:44386/ServerAPI"),
                    Method = HttpMethod.Get,
                };
                request.Headers.Add("X-ARR-ClientCert", clientCert.GetRawCertDataString());
                httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));//ACCEPT head
                //using (var response = await httpClient.GetAsync("https://localhost:11111/ServerAPI"))
                using (var response = await httpClient.SendAsync(request))
                {
                    if (response.StatusCode == System.Net.HttpStatusCode.OK)
                    {
                        string apiResposne = await response.Content.ReadAsStringAsync();
                        weatherForecastList = JsonConvert.DeserializeObject<List<WeatherForecast>>(apiResposne);
                    }
                }
            }
            return weatherForecastList;
        }

authentication class

public static X509Certificate2 GetClientCertificate()
        {
            X509Store userCaStore = new X509Store(StoreName.TrustedPeople, StoreLocation.CurrentUser);
            try
            {
                string str_API_Cert_Thumbprint = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";                   

                userCaStore.Open(OpenFlags.ReadOnly);
                X509Certificate2Collection certificatesInStore = userCaStore.Certificates;
                X509Certificate2Collection findResult = certificatesInStore.Find(X509FindType.FindByThumbprint, str_API_Cert_Thumbprint, false);

                X509Certificate2 clientCertificate = null;
                if (findResult.Count == 1)
                {
                    clientCertificate = findResult[0];
                    if(System.DateTime.Today >= System.Convert.ToDateTime(clientCertificate.GetExpirationDateString()))
                    {
                        throw new Exception("Certificate has already been expired.");
                    }
                    else if (System.Convert.ToDateTime(clientCertificate.GetExpirationDateString()).AddDays(-30) <= System.DateTime.Today)
                    {
                        throw new Exception("Certificate is about to expire in 30 days.");
                    }
                }
                else
                {
                    throw new Exception("Unable to locate the correct client certificate.");
                }
                return clientCertificate;
            }
            catch (Exception ex)
            {
                throw;
            }
            finally
            {
                userCaStore.Close();
            }
        }

Server/target api code

[HttpGet]
    public IEnumerable<WeatherForecast> Getcertdata()
    {
        IHeaderDictionary headers = base.Request.Headers;
        X509Certificate2 clientCertificate = null;
        string certHeaderString = headers["X-ARR-ClientCert"];

        if (!string.IsNullOrEmpty(certHeaderString))
        { 
            //byte[] bytes = Encoding.ASCII.GetBytes(certHeaderString);
            //byte[] bytes = Convert.FromBase64String(certHeaderString);
            //clientCertificate = new X509Certificate2(bytes);              
            clientCertificate = new X509Certificate2(WebUtility.UrlDecode(certHeaderString));                
            var serverCertificate = new X509Certificate2(Path.Combine("abc.pfx"), "pwd");
            if (clientCertificate.Thumbprint == serverCertificate.Thumbprint)
            {
                //Valida Cert
            }

        }
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        }).ToArray();

        //return new List<WeatherForecast>();
    }
1

There are 1 best solutions below

0
On

You have much more problems here, the code is significantly flawed and insecure in various ways. Let's explain each issue:

  • HttpClient in using clause in client side controller logic

Although you expect to wrap anything that implements IDisposable in using statement. However, it is not really the case with HttpClient. Connections are not closed immediately. And with every request to client controller action, a new connection is established to remote endpoint, while previous connections sit in TIME_WAIT state. Under certain constant load, your HttpClient will exhaust TCP port pool (which is limited) and any new attempt to create a new connection will throw an exception. Here are more details on this problem: You're using HttpClient wrong and it is destabilizing your software

Microsoft recommendation is to re-use existing connections. One way to do this is to Use IHttpClientFactory to implement resilient HTTP requests. Microsoft article talks a bit about this problem:

Though this class implements IDisposable, declaring and instantiating it within a using statement is not preferred because when the HttpClient object gets disposed of, the underlying socket is not immediately released, which can lead to a socket exhaustion problem.

BTW, you have created a client variable, but do not use it in any way.

  • Ignore certificate validation problems

The line:

requestHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;

make you vulnerable to MITM attack.

  • you are doing client certificate authentication wrong

The line:

request.Headers.Add("X-ARR-ClientCert", clientCert.GetRawCertDataString());

It is not the proper way how to do client cert authentication. What you literally doing is passing certificate's public part to server. That's all. You do not prove private key possession which is required to authenticate you. The proper way to do so is:

requestHandler.ClientCertificates.Add(clientCert);

This will force client and server to perform proper client authentication and check if you possess the private key for certificate you pass (it is done in TLS handshake automatically). If you have ASP.NET on server side, then you read it this way (in controller action):

X509Certificate2 clientCert = Request.HttpContext.Connection.ClientCertificate
if (clientCert == null) {
    return Unauthorized();
}
// perform client cert validation according server-side rules.
  • Non-standard cert store

In authentication class you open StoreName.TrustedPeople store, while normally it should be StoreName.My. TrustedPeople isn't designed to store certs with private key. It isn't a functional problem, but it is bad practice.

  • unnecessary try/catch clause in authentication class

If you purposely throw exceptions in method, do not use try/catch. In your case you simply rethrow exception, thus you are doing a double work. And this:

throw new Exception("Certificate is about to expire in 30 days.");

is behind me. Throwing exception on technically valid certificate? Really?

  • server side code

As said, all this:

IHeaderDictionary headers = base.Request.Headers;
X509Certificate2 clientCertificate = null;
string certHeaderString = headers["X-ARR-ClientCert"];
if (!string.IsNullOrEmpty(certHeaderString))
    { 
        //byte[] bytes = Encoding.ASCII.GetBytes(certHeaderString);
        //byte[] bytes = Convert.FromBase64String(certHeaderString);
        //clientCertificate = new X509Certificate2(bytes);              
        clientCertificate = new X509Certificate2(WebUtility.UrlDecode(certHeaderString));                
        var serverCertificate = new X509Certificate2(Path.Combine("abc.pfx"), "pwd");
        if (clientCertificate.Thumbprint == serverCertificate.Thumbprint)
        {
            //Valida Cert
        }

    }

must be replaced with:

X509Certificate2 clientCert = Request.HttpContext.Connection.ClientCertificate
if (clientCert == null) {
    return Unauthorized();
}
// perform client cert validation according server-side rules.

BTW:

var serverCertificate = new X509Certificate2(Path.Combine("abc.pfx"), "pwd");
if (clientCertificate.Thumbprint == serverCertificate.Thumbprint)
{
    //Valida Cert
}

This is another disaster in your code. You are loading the server certificate from PFX just to compare their thumbprints? So, you suppose that client will have a copy of server certificate? Client and server certificates must not be the same. Next thing is you are generating a lot of copies of server certificate's private key files. More private key files you generate, the slower the process is and you just generate a lot of garbage. More details on this you can find in my blog post: Handling X509KeyStorageFlags in applications