1

In an event triggered Azure Function using .NET 7, I'm trying to create an HttpClient using HttpClientFactory:

https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests

I need to add client certificates in requests. To do that, I created my own HttpMessageHandlerBuilder which loads the certificates then adds them in the handler it returns. I then add the services and when I later inject the IHttpClientFactory, it uses this handler that has all certificates:

custom handler builder : HttpMessageHandlerBuilder

public override HttpMessageHandler Build()
{
    var handler = new HttpClientHandler();
    var certificates = certificateService
        .GetAllCertificates(); // this fetches certificates in Azure Keyvault
    foreach (var certificate in certificates)
    {
        handler.ClientCertificates.Add(certificate);
    }
    return handler;
}

program.cs (startup.cs)

serviceCollection.AddCustomCertificateServices();
serviceCollection.AddHttpClient();

function.cs

using var client = httpClientFactory.CreateClient();

This works. However, I now need to send only one certificate per request based on some condition. But I cannot manage to access the handler in the client returned by the factory, nor the certificate collection. The only way I managed to do this is to get rid of the factory and create a new instance of the handler and client as follows (summarized):

function.cs

var handler = new HttpClientHandler();
handler.ClientCertificates.Add(x509Certificate);
using var client = new HttpClient(handler);

(also see Add client certificate to .NET Core HttpClient)

Question
According to the MSDN article this can lead to port exhaustion, however this is not a high load function so this will not be an issue in this case. But in case it was, is there a way (and if yes, how) to manage handler/client certificates per request using the factory + DI? In other words, is there a way to tell the factory to provide a customer that has a handler containing only the certificate needed for the current request? Maybe something like this answer?

evilmandarine
  • 4,241
  • 4
  • 17
  • 40

2 Answers2

1

This can be done with named clients, i.e.

// at startup
services.AddHttpClient("Cert1", client =>
{
    client.AddCertificateClient(cert1);
});

// then you can
using var httpClient = _httpClientFactory.CreateClient("Cert1");

and then for other certificates you just create more named clients, i.e.

// at startup
services.AddHttpClient("Cert2", client =>
{
    client.AddCertificateClient(cert2);
});

// then you can
using var httpClient = _httpClientFactory.CreateClient("Cert2");

When in AddHttpClient this is a good place to configure how to do retries, configure timeout or handle transient errors. Each according to the named client

For full reference Make HTTP requests using IHttpClientFactory in ASP.NET Core

dove
  • 20,469
  • 14
  • 82
  • 108
  • I get the idea, but how does this fit in the startup method? I have a `List`. How do I iterate through it to add the clients? I tried `var services = host.Services;` after building the host (so I can access the config, and load the cert list) but then there is no AddHttpClient method here as this is a `IServiceProvider`, not `IServicesCollection`, and the `AddCertificateClient` does not help. I can post a summary of the whole startup method if this helps. – evilmandarine Mar 01 '23 at 14:31
  • You can acces config values before you build the host. It depends a lot on your project setup but there are various ways to access config settings before you build. For instance bind my configuration to my custom settings and have this setup before I build. It depends on how you use the builder.Configuration – dove Mar 01 '23 at 15:17
  • No problem for the config. I can access it and get the certificates. What I don't see how to do is how to add this variable number of HttpClients *after* I build the host (see previous comment about services), or *during* the build process in one of the `.Configure...` methods. – evilmandarine Mar 01 '23 at 15:46
  • Caveat of writing without context of your project, you would only create the HttpClients when and where you need them and then dispose of them right after. The challenge as I think I understand you is how to know which clients were created as perhaps you are not sure how certificates will be there. Perhaps before building you would add the list of certificate names (or whatever you need to identify which named client to use) to your configuration setup so it would be available later on when you come to create your HttpClient. Is that getting closer to the mark? – dove Mar 01 '23 at 16:02
  • Not really. I managed to solve my services issue. [This](https://pastebin.com/jikpF9Qy) is what I am doing now. But it's still not working, because somehow the (named) client that the factory is returning contains all certificates in its handler, not only one, despite the fact I am creating a new handler every time. Side note: I don't think the client in `AddCertificateClient` is an HttpClient, I think it is an Azure Keyvault client that serves a different purpose. – evilmandarine Mar 01 '23 at 16:53
  • 1
    Ok I think I got it working. Will post an answer later but +1 for the hint. – evilmandarine Mar 01 '23 at 17:37
1

Here is what I ended up doing.

Create an extension method:

public static IServiceCollection AddNamedHandlers(
    this IServiceCollection serviceCollection,
    List<Certificate> certificates) // custom Certificate object, adapt to fit your needs
{
    // Create one handler per certificate.
    // Then add one client per certificate using the created handler.
    foreach (var certificate in certificates)
    {
        var httpClientHandler = new HttpClientHandler
        {
            ClientCertificateOptions = ClientCertificateOption.Manual,
            SslProtocols = SslProtocols.Tls12
        };
        httpClientHandler.ClientCertificates.Add(certificate);

        serviceCollection
            .AddHttpClient(certificate.Name) // whatever you want to name it to be able to use it later
            .ConfigurePrimaryHttpMessageHandler(() => httpClientHandler);
    }

    return serviceCollection;
}

Add the handlers and clients during startup:

serviceCollection.AddNamedHandlers(certificates); // pass your certificates here somehow

Usage with HttpClientFactory:

using var client = httpClientFactory.CreateClient(certificateName);
evilmandarine
  • 4,241
  • 4
  • 17
  • 40