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.

Interaction between Client and API

Leave a 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 )

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: