ASP.NET WebAPI, load balancers and SSL termination.

I recently encountered an issue in our ASP.NET WebAPI application, which sits behind a load balancer that does SSL termination. The load balancer gets a HTTPS request, decrypts it, unwraps the original HTTP request and forwards that to the application servers, freeing them from the SSL load.

In our application we are generating URLs using the standard UrlHelper.Link method like so:

Url.Link("SomeRouteName", new { id = 42 });  

This creates proper URLs depending for our API routes. However, when used together with our SSL terminated load balancer, it results in our application creating URLs with HTTP as a schema instead of HTTPS. This makes sense since the load balancer is taking care of the actual HTTPS decryption and forwarding the call over to our application using HTTP, but it results on us giving incorrect URLs to the client.

So how do we fix this? How do we tell our application that the original request came through via HTTPS?

Turns out there is a de-facto HTTP header that can help solve this problem, and that header is X-Forwarded-Proto. That header details which protocol the original request was made in. Armed with this information, the solution was to create a simple DelegatingHandler, detect if we get a X-Forwarded-Proto header, and then change the HttpRequestMessage.RequestUri accordingly. Then we change the load balancer to inject that header into the request.

This is how our handler looks like:

namespace Our.Custom.Handlers  
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Globalization;
    using System.Linq;
    using System.Net.Http;
    using System.Threading.Tasks;

    public class XForwardedProtoHeaderHandler : DelegatingHandler
    {
        /// <summary>
        /// A constant for the header name.
        /// </summary>
        private const string XForwardedProtoKey = "X-Forwarded-Proto";

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            IEnumerable<string> protos;
            request.Headers.TryGetValues(XForwardedProtoHeaderHandler.XForwardedProtoKey, out protos);

            // We received a X-Forwarded-Proto
            if (protos != null)
            {
                // We have a X-Forwarded-Proto header
                var enumeratedProtos = protos.ToArray();
                if (enumeratedProtos.Any())
                {
                    var builder = new UriBuilder(request.RequestUri);
                    string scheme = enumeratedProtos[0].ToLowerInvariant();
                    if (builder.Scheme.ToLowerInvariant() != scheme)
                    {
                        builder.Scheme = scheme;
                    }
                }

                // Let's set the default port based on the scheme (http or https).
                switch (builder.Scheme.ToLowerInvariant())
                {
                    case "http":
                        builder.Port = 80;
                        break;
                    case "https":
                        builder.Port = 443;
                        break;
                }

                request.RequestUri = builder.Uri;   
            }

            return await base.SendAsync(request, cancellationToken);
        }
    }
}

Then we register the handler:

public static class WebApiConfig  
{
    public static void Register(HttpConfiguration config)
    {
        config.MessageHandlers.Add(new XProtoForwardedHeaderHandler());
        // Other code not shown...
    }
}

And voila. Our application can now sit behind a SSL terminated load balancer and still generate correct URLs.

Hope this helps :)

Stefán Jökull Sigurðarson

Read more posts by this author.

Iceland