Cookie based authentication against API isn’t common scenario but you would find it here and there, mostly in legacy applications. HttpClient
supports cookies out of the box, but it doesn’t work always as expected. In my scenario the cookie is always invalided by API after each call which results always to the following behavior: the first request is successful, every other is rejected. Let see how can be avoid it.
I setup two HttpClient
services in DI container, each of them for a different purpose. One is responsible for the authentication and is used inside of CckmAuthenticationHandler
and the second one is used inside of infrastructure layer to execute RESTful operations against API.
//HttpClient used to authenticate
services.AddHttpClient<CckmAuthenticationHandler>((httpClient) => httpClient.BaseAddress = new Uri(integrationTestsConfiguration.CckmBaseUrl))
.ConfigurePrimaryHttpMessageHandler((serviceProvider) =>
new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true,
UseCookies = false
});
//HttpClient used to run RESTful operations (for example GET/POST/DELETE against API)
services.AddHttpClient(HttpClientName.CckmApiHttpClient, (httpClient) => httpClient.BaseAddress = new Uri(integrationTestsConfiguration.CckmBaseUrl))
.AddHttpMessageHandler<CckmAuthenticationHandler>() //here is used the authentication handler which utilize HttpClient registered at the beginning
.ConfigurePrimaryHttpMessageHandler((serviceProvider) =>
new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true,
UseCookies = false
});
It is fair to ask if is this is really necessary. But I like the fact that I can use one client to run RESTful operations again and again and don’t need to care about authentication. When in the future I would need another HttpClient
service to use same authentication, everything what I need to do is just call AddHttpMessageHandler
.
public class CckmAuthenticationHandler : DelegatingHandler
{
private readonly IOptionsMonitor<CryptMasterConfiguration> _configuration;
private readonly ILogger<CckmAuthenticationHandler> _logger;
private readonly HttpClient _httpClient;
public CckmAuthenticationHandler(IOptionsMonitor<CryptMasterConfiguration> configuration, ILogger<CckmAuthenticationHandler> logger, HttpClient httpClient)
{
_configuration = configuration;
_logger = logger;
_httpClient = httpClient;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
const string xXsrfTokenHeaderIdentifier = "X-XSRF-TOKEN";
const string setCookieHeaderIdentifier = "SET-COOKIE";
const string cookieIdentifier = "Cookie";
_logger.LogInformation($"Getting {xXsrfTokenHeaderIdentifier} and {setCookieHeaderIdentifier} from CCKM.");
// API provide X-XSRF-TOKEN and SET-COOKIE values which need to be provided be each request
var tokenResult = await _httpClient.GetAsync(_configuration.CurrentValue.CckmBaseUrl);
tokenResult.EnsureSuccessStatusCode();
var xSrfToken = tokenResult.Headers.GetValues(xXsrfTokenHeaderIdentifier).Single();
var sessionId = tokenResult.Headers.GetValues(setCookieHeaderIdentifier).Single();
var authenticationUrl = $"{_configuration.CurrentValue.CckmBaseUrl}/auth2/azure";
var authenticationContent = new FormUrlEncodedContent(
new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("tenant", _configuration.CurrentValue.TenantName),
new KeyValuePair<string, string>("password", _configuration.CurrentValue.CckmTenantPassword)
});
var authenticationRequest = new HttpRequestMessage()
{
RequestUri = new Uri(authenticationUrl),
Method = HttpMethod.Post,
Content = authenticationContent
};
// Set X-XSRF-TOKEN and COOKIE values to be able to authenticate
authenticationRequest.Headers.Add(xXsrfTokenHeaderIdentifier, xSrfToken);
authenticationRequest.Headers.Add(cookieIdentifier, sessionId);
_logger.LogInformation("Authenticate agains CCKM.");
var authenticationResponse = await _httpClient.SendAsync(authenticationRequest);
authenticationResponse.EnsureSuccessStatusCode();
// When authentication succeeded add X-XSRF-TOKEN and COOKIE to the initial request headers
request.Headers.Add(xXsrfTokenHeaderIdentifier, xSrfToken);
request.Headers.Add(cookieIdentifier, sessionId);
return await base.SendAsync(request, cancellationToken);
}
}
CckmAuthenticationHandler
is an implementation of DelegatingHandler
. As you can see all operation regarding of authentication are executed through the subsequent request and the result is that the initial request can continue to run enriched by X-XSRF-TOKEN
and SET-COOKIE
values. This is a separation of concerns by par excellence. The initial infrastructure request doesn’t know anything about authentication, well, because he doesn’t need to.