Using Certificate Authentication with IHttpClientFactory and HttpClient

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

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

One comment

  1. […] Using Certificate Authentication with IHttpClientFactory and HttpClient (Damien Bowden) […]

Leave a Reply to Dew Drop – September 9, 2019 (#3026) | Morning Dew Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: