I have this scenario: I have an Azure Function that calls some REST apis from a different service. This other service requires a certificate.
If I try to add the certificate during my dependency injection in Startup.cs, the certificate is not found. Seems Startup.Configure is run before all certificates are loaded(?).
So, I need to be able to load the certificate to the httpclient within the azure function itself. But, IHttpClientFactory does not seem to have any mechanisms to alter the client created by CreateClient, other than some headers. How can I add a certificate (which would have been done via HttpClientHandler.ClientCertificates.Add()) at some point later in the call stack?
// Startup.cs::Configure
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddLogging();
// My cert is stored in keyvault, so I go get it from there for the thumbprint
string certThumbprint = CertificateUtils.GetCertificateThumbprintFromKeyVault();
// This call tries to find the certificate with that thumbprint in CertStore.My
X509Certificate2 cert = CertificateUtils.GetCertificate(certThumbprint);
if (cert == null)
{
throw new MyException("Unable to retrieve your certificate from key vault");
}
using HttpClientHandler myApiHandler = new HttpClientHandler();
MyApiHandler.ClientCertificates.Add(cert);
builder.Services.AddHttpClient<IMyAPI, MyAPI>("MyApi", client => { client.BaseAddress = new Uri("<baseurl>"); })
.ConfigurePrimaryHttpMessageHandler(() => myApiHandler);
}
// CertificateUtils:
public static X509Certificate2 GetCertificate(
string certId,
StoreLocation storeLocation = StoreLocation.LocalMachine,
StoreName storeName = StoreName.My,
X509FindType findType = X509FindType.FindByThumbprint)
{
X509Certificate2Collection set = GetCertificates(storeLocation, storeName, findType, certId, false);
if (set.Count != 1 || set[0] == null)
{
string exceptionDetails = set.Count != 1 ? "with certificate count of " + set.Count : "element at position 0 is null";
throw new ConfigException($"Failed to retrieve certificate {certId} from store {storeLocation}\\{storeName}, {exceptionDetails}");
}
return set[0];
}
private static X509Certificate2Collection GetCertificates(
StoreLocation storeLocation,
StoreName storeName,
X509FindType findType,
string certId,
bool validOnly)
{
X509Store certStore = new X509Store(storeName, storeLocation);
certStore.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
try
{
return certStore.Certificates.Find(findType, certId, validOnly);
}
finally
{
certStore.Close();
}
}
}
// MyAPI
public class MyAPI : MyAPI
{
private readonly HttpClient HttpClient;
private IHttpClientFactory HttpClientFactory;
public MyAPI(IHttpClientFactory httpClientFactory)
{
this.HttpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
this.HttpClient = httpClientFactory.CreateClient("MyApi");
}
public Data DoSomething(string name)
{
Uri uri = new Uri($"{this.HttpClient.BaseAddress}/Some/REST/API?name={name}");
HttpResponseMessage response = this.HttpClient.GetAsync(uri).Result;
response.EnsureSuccessStatusCode();
string body = response.Content.ReadAsStringAsync().Result;
return JsonConvert.DeserializeObject<Data>(body);
}
}
public class MyAzureFunc
{
private readonly IMyAPI MyAPI;
private readonly ILogger Log;
public MyAzureFunc(ILogger<MyAzureFunc> log, IMyAPI myApi)
{
this.Log = log;
this.MyAPI = myApi;
}
[FunctionName("MyAzureFunc")]
public async Task<HttpResponseMessage> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = null)]
HttpRequest req)
{
// ... standard boilerplate stuff to read the name=Rusty part from the GET call ...
// Call the 3rd party service that requires the cert to get some data
MyData data = this.MyAPI.DoSomething(name);
// ... then do something with the data
return new HttpResponseMessage(HttpStatusCode.OK);
}
}
On starting my azure function in local debugging, it immediately fails in GetCertificate. I successfully get the certificate thumbprint out of my keyvault, but the set of certificates it found in my CertStore.My is incomplete (it returns 4 certificates, none of which are in my personal store ?!?). This is what leads me to believe that Startup.Configure is happening before certificates are loaded by the azure function.
What I had before trying to use the recommended approach of Microsoft.Extensions.Http + DI + Polly was as below, in the function MyAPI.DoSomething function itself:
string certThumbprint = CertificateUtils.GetCertificateThumbprintFromKeyVault();
X509Certificate2 cert = CertificateUtils.GetCertificate(certThumbprint, storeLocation: StoreLocation.CurrentUser);
using HttpClientHandler handler = new HttpClientHandler();
handler.ClientCertificates.Add(cert);
using HttpClient client = new HttpClient(handler);
response = await client.GetAsync(uri);
response.EnsureSuccessStatusCode();
This worked fine.
So, what I'm thinking, is that since the certificates were apparently not available when Startup.cs was called, that I need to be able to add the certificate at the point where I initialize the HttpClient via
this.HttpClient = httpClientFactory.CreateClient("MyApi");
// Get my certificate here and add it to the client via HttpClientHandler or such
Or, if it's not the case that the certificates aren't loaded after Startup.Configure, why weren't all of my certificates in my local store picked up, and how do I go about making that work properly?