This article shows how an HttpClient instance could be setup to send a certificate to an API to use for certificate authentication. In an ASP.NET Core application, the IHttpClientFactory can be used to get an instance of the HttpClient.
Code https://github.com/damienbod/Secure_gRpc/tree/master/SecureGrpc.ManagedClient
Posts in this series
- Certificate Authentication in ASP.NET Core 3.0 (Self Signed)
- Using Chained Certificates for Certificate Authentication in ASP.NET Core 3.0
- Using Certificate Authentication with IHttpClientFactory and HttpClient
Using a named HttpClient
In the following example, a client certificate is added to a HttpClientHandler using the ClientCertificates property from the handler. This handler can then be used in a named instance of a HttpClient using the ConfigurePrimaryHttpMessageHandler method. This is setup in the Startup class in the
ConfigureServices method.
var clientCertificate = new X509Certificate2( Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234"); var handler = new HttpClientHandler(); handler.ClientCertificates.Add(clientCertificate); services.AddHttpClient("namedClient", c => { }).ConfigurePrimaryHttpMessageHandler(() => handler);
The IHttpClientFactory can then be used to get the named instance with the handler and the certificate. The CreateClient method with the name of the client defined in the Startup class is used to get the instance. The HTTP request can be sent using the client as required.
private readonly IHttpClientFactory _clientFactory; public ApiService(IHttpClientFactory clientFactory) { _clientFactory = clientFactory; } private async Task<JsonDocument> GetApiDataWithNamedClient() { var client = _clientFactory.CreateClient("namedClient"); var request = new HttpRequestMessage() { RequestUri = new Uri("https://localhost:44379/api/values"), Method = HttpMethod.Get, }; var response = await client.SendAsync(request); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); var data = JsonDocument.Parse(responseContent); return data; } throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}"); }
Using a HttpClientHandler directly
The HttpClientHandler could be added directly in the constructor of the HttpClient class. Care should be taken when creating instances of the HttpClient. The HttpClient will then send the certificate with each request.
private async Task<JsonDocument> GetApiDataUsingHttpClientHandler() { var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234"); var handler = new HttpClientHandler(); handler.ClientCertificates.Add(cert); var client = new HttpClient(handler); var request = new HttpRequestMessage() { RequestUri = new Uri("https://localhost:44379/api/values"), Method = HttpMethod.Get, }; var response = await client.SendAsync(request); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); var data = JsonDocument.Parse(responseContent); return data; } throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}"); }
Sending the certificate in the X-ARR-ClientCert request header
The HttpClient could also send the certificate using the X-ARR-ClientCert request header. If sending the client as a HTTP request header, the server needs to handle this correctly. This can be implemented using the AddCertificateForwarding extension method.
private async Task<JsonDocument> GetApiDataUsingXARRClientCertHeader() { var client = _clientFactory.CreateClient(); var request = new HttpRequestMessage() { RequestUri = new Uri("https://localhost:44379/api/values"), Method = HttpMethod.Get, }; request.Headers.Add("X-ARR-ClientCert", cert.GetRawCertDataString()); var response = await client.SendAsync(request); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); var data = JsonDocument.Parse(responseContent); return data; } throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}"); }
GRPC named HttpClient with access token
A GRPC service could also send a certificate and an access token for authentication. The certificate is added as in the handler like before, and the access token is sent using the Authorization request header.
public async Task<string> GetGrpcApiDataAsync() { var client = _clientFactory.CreateClient("grpc"); var access_token = await _apiTokenInMemoryClient.GetApiToken( "ProtectedGrpc", "grpc_protected_scope", "grpc_protected_secret" ); var tokenValue = "Bearer " + access_token; var metadata = new Metadata { { "Authorization", tokenValue } }; CallOptions callOptions = new CallOptions(metadata); var channel = GrpcChannel.ForAddress(_authConfigurations.Value.ProtectedApiUrl); var clientGrpc = new Greeter.GreeterClient(channel); var response = await clientGrpc.SayHelloAsync( new HelloRequest { Name = "GreeterClient managed" }, callOptions); return response.Message; }
The following ApiTokenInMemoryClient can be used to get an access token from a secure token server.
using IdentityModel.Client; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; namespace SecureGrpc.ManagedClient { public class ApiTokenInMemoryClient { private readonly ILogger<ApiTokenInMemoryClient> _logger; private readonly HttpClient _httpClient; private readonly IOptions<AuthConfigurations> _authConfigurations; private class AccessTokenItem { public string AccessToken { get; set; } = string.Empty; public DateTime ExpiresIn { get; set; } } private ConcurrentDictionary<string, AccessTokenItem> _accessTokens = new ConcurrentDictionary<string, AccessTokenItem>(); public ApiTokenInMemoryClient( IOptions<AuthConfigurations> authConfigurations, IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory) { _authConfigurations = authConfigurations; _httpClient = httpClientFactory.CreateClient(); _logger = loggerFactory.CreateLogger<ApiTokenInMemoryClient>(); } public async Task<string> GetApiToken(string api_name, string api_scope, string secret) { if (_accessTokens.ContainsKey(api_name)) { var accessToken = _accessTokens.GetValueOrDefault(api_name); if (accessToken.ExpiresIn > DateTime.UtcNow) { return accessToken.AccessToken; } else { // remove _accessTokens.TryRemove(api_name, out AccessTokenItem accessTokenItem); } } _logger.LogDebug($"GetApiToken new from STS for {api_name}"); // add var newAccessToken = await getApiToken( api_name, api_scope, secret); _accessTokens.TryAdd(api_name, newAccessToken); return newAccessToken.AccessToken; } private async Task<AccessTokenItem> getApiToken(string api_name, string api_scope, string secret) { try { var disco = await HttpClientDiscoveryExtensions.GetDiscoveryDocumentAsync( _httpClient, _authConfigurations.Value.StsServer); if (disco.IsError) { _logger.LogError($"disco error Status code: {disco.IsError}, Error: {disco.Error}"); throw new ApplicationException($"Status code: {disco.IsError}, Error: {disco.Error}"); } var tokenResponse = await HttpClientTokenRequestExtensions.RequestClientCredentialsTokenAsync(_httpClient, new ClientCredentialsTokenRequest { Scope = api_scope, ClientSecret = secret, Address = disco.TokenEndpoint, ClientId = api_name }); if (tokenResponse.IsError) { _logger.LogError($"tokenResponse.IsError Status code: {tokenResponse.IsError}, Error: {tokenResponse.Error}"); throw new ApplicationException($"Status code: {tokenResponse.IsError}, Error: {tokenResponse.Error}"); } return new AccessTokenItem { ExpiresIn = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn), AccessToken = tokenResponse.AccessToken }; } catch (Exception e) { _logger.LogError($"Exception {e}"); throw new ApplicationException($"Exception {e}"); } } } }
Links
https://docs.microsoft.com/en-us/aspnet/core/security/authentication/certauth
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests
[…] Using Certificate Authentication with IHttpClientFactory and HttpClient (Damien Bowden) […]
I believe this code will fail after a while when the handler gets recycled. I have had some similar code (with a typed client), and if the process lives long enough it ends with an `Cannot access a disposed object. Object name: ‘System.Net.Http.WinHttpHandler’.`
“`
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(clientCertificate);
services.AddHttpClient(“namedClient”, c =>
{
}).ConfigurePrimaryHttpMessageHandler(() => handler)
“`
the () => handler part should instead be replaced with something that new up the handler,
“`
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(clientCertificate);
services.AddHttpClient(“namedClient”, c =>
{
}).ConfigurePrimaryHttpMessageHandler(() => CreateNewHandler(clientCertificate))
“`
“`
private static HttpClientHandler CreateNewHandler(X509Certificate cert)
{
var handler= new HttpClientHandler();
handler.ClientCertificates.Add(cert);
return handler;
}
“`
thanks for the feedback, will look into this, I have not experienced this problem yet, but have not tested it or used it yet under load
Greetings Damien
I also experienced the same issue as Terje.
I fixed it by creating my own HttpClientHandler
public class MyHttpClientHandler: HttpClientHandler
{
public MyHttpClientHandler()
{
ClientCertificateOptions = ClientCertificateOption.Manual;
X509Certificate2 clientCertificate = GetClientCertificate();
if (clientCertificate != null) {
ClientCertificates.Add(clientCertificate);
}
}
And then in my statup class:
services.AddTransient();
services.AddHttpClient(client =>
{
client.BaseAddress = new Uri(configuration[“RestrictionsService:BaseUrl”]);
}).ConfigurePrimaryHttpMessageHandler();
Thanks Maxime!
@damienbod : Great article, thanks!
@Maxime Matter : Thanks for the fix! Could you please add also the code for the GetClientCertificate() method?
Hi there
I am writing a ASP.NET Console application to connect to a WebAPI.
I have a certificate file that are signed by the API owner with their CA.
Here my code:
X509Certificate2 x509Cer2 = X509Certificate2.CreateFromPemFile(“cerfile”,”keyfile”);
HttpClientHandler handler = new HttpClientHandler();
handler.ClientCertificates.Add(x509Cer2);
var client = new HttpClient(handler);
var request = new HttpRequestMessage()
{
RequestUri = new Uri(“https://myAPI.URL”),
Method = HttpMethod.Get,
};
var result = client.SendAsync(request).Result;
The SendAsync() will result an exception of :
“No credentials are available in the security package”
Any idea how to fix this? Do I need to add the CA certificate to the handler as well?
if so, how?
Thank you very much!