Search code examples
c#asp.net-coredependency-injectiondotnet-httpclienthttpclientfactory

Re-set the certificate of singleton httpclient (Reconfiguring IHttpclientFactory?)


Using C#, .NET Core 3.1

I add a singleton httpclient via in startup.cs:

services.AddHttpClient<IClientLogic, ClientLogicA>().ConfigurePrimaryHttpMessageHandler(() =>
{
   var handler = new HttpClientHandler();
   
   var cert= GetCertFromX();

   handler.ClientCertificates.Add(cert);

   return handler;
});

But lets say, later in ClientLogicA class, I want to change the certificate, how do I go about doing this and will the change persist for future uses of the httpclient singleton?


Solution

  • So what you want to do is modify the certificate of a HttpClient that is being produced by an IHttpClientFactory. It looks as though Microsoft may be adding this type of functionality in .NET 5, but in the meantime, we need to come up with a way to do it now.

    This solution will work with both Named HttpClient and Typed HttpClient objects.

    So the issue here is to create the Named or Typed HttpClient where the certificate collection that is bound to the HttpClient can be updated at any time. The problem is we can only set the creation parameters for HttpClient once. After that, the IHttpClientFactory reuses those settings over and over.

    So, let's start by looking at how we inject our services:

    Named HttpClient Injection Routine

    services.AddTransient<IMyService, MyService>(); 
    
    services.AddSingleton<ICertificateService, CertificateService>();
    
    services.AddHttpClient("MyCertBasedClient").
        ConfigurePrimaryHttpMessageHandler(sp =>
        new CertBasedHttpClientHandler(
            sp.GetRequiredService<ICertificateService>()));
    

    Typed HttpClient Injection Routine

    services.AddSingleton<ICertificateService, CertificateService>();
    
    services.AddHttpClient<IMyService, MyService>().
        ConfigurePrimaryHttpMessageHandler(sp =>
            new CertBasedHttpClientHandler(
                sp.GetRequiredService<ICertificateService>()));
    

    We inject a ICertificateService as singleton that holds our current certificate and allows other services to change it. IMyService is injected manually when using Named HttpClient, while when using a Typed HttpClient, IMyService will be automatically injected. When it comes time for the IHttpClientFactory to create our HttpClient, it will call the lambda and produce an extended HttpClientHandler which takes our ICertificateService from our service pipeline as a constructor parameter.

    This next part is the source to the ICertificateService. This service maintains the certificate with an "id" (which is just a timestamp of when it was last updated).

    CertificateService.cs

    public interface ICertificateService
    {
        void UpdateCurrentCertificate(X509Certificate cert);
        X509Certificate GetCurrentCertificate(out long certId);
        bool HasCertificateChanged(long certId);
    }
    
    public sealed class CertificateService : ICertificateService
    {
        private readonly object _certLock = new object();
        private X509Certificate _currentCert;
        private long _certId;
        private readonly Stopwatch _stopwatch = new Stopwatch();
    
        public CertificateService()
        {
            _stopwatch.Start();
        }
    
        public bool HasCertificateChanged(long certId)
        {
            lock(_certLock)
            {
                return certId != _certId;
            }
        }
    
        public X509Certificate GetCurrentCertificate(out long certId)
        {
            lock(_certLock)
            {
                certId = _certId;
                return _currentCert;
            }
        }
    
        public void UpdateCurrentCertificate(X509Certificate cert)
        {
            lock(_certLock)
            {
                _currentCert = cert;
                _certId = _stopwatch.ElapsedTicks;
            }
        }
    }
    

    This final part is the class that implements a custom HttpClientHandler. With this we can hook in to all HTTP requests being made by the client. If the certificate has changed, we swap it out before the request is made.

    CertBasedHttpClientHandler.cs

    public sealed class CertBasedHttpClientHandler : HttpClientHandler
    {
        private readonly ICertificateService _certService;
        private long _currentCertId;
    
        public CertBasedHttpClientHandler(ICertificateService certificateService)
        {
            _certService = certificateService;
            var cert = _certService.GetCurrentCertificate(out _currentCertId);
            if(cert != null)
            {
                ClientCertificates.Add(cert);
            }
        }
    
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, 
            CancellationToken cancellationToken)
        {
            if(_certService.HasCertificateChanged(_currentCertId))
            {
                ClientCertificates.Clear();
                var cert = _certService.GetCurrentCertificate(out _currentCertId);
                if(cert != null)
                {
                    ClientCertificates.Add(cert);
                }
            }
            return base.SendAsync(request, cancellationToken);
        }
    }
    

    Now I think the biggest down-side to this is if the HttpClient is in the middle of a request on another thread, we could run in to a race condition. You could alleviate that by guarding the code in the SendAsync with a SemaphoreSlim or any other asynchronous thread synchronizing pattern, but that could cause a bottle-neck so I didn't bother doing that. If you want to see that added, I will update this answer.