We’re currently using an Application Gateway that sits in front of several of our app services. Some of these app services use Azure Active Directory (AAD) authentication and need to be able to work, encrypted, without the use of custom domains, or TLS certificates installed on the app services. We also want to be able to modify HTTP headers in our .NET code (Core and Framework), so that the Azure website url is not exposed to the end user. Here is how we accomplished that (certain setting names blanked out for privacy):

 Layout

Our application gateway is configured to have:

  • 1 backend pool per app
  • 1 backend setting per app
  • 1 health probe per app
  • 2 listeners per domain on the gateway (1 HTTP, 1 HTTPS)
  • 2 rules per domain on the gateway (1 HTTP, 1 HTTPS)
  • 1 AAD Auth Rewrite

Backend Pool

The backend pool simply uses a target type of App Services, targeting our app service location. It also has its own associated rule for the domain. Nothing much to point out here.

Backend Settings

The backend settings are where things start to get interesting. Things to note: We are using a trusted root certificate. This is a certificate for the domain that is stored in a key vault, and referenced by our application gateway. This is the only place we really need to reference the certificate. It is not needed at the app service level. Other things to note, we are picking the hostname from the backend target (this tells the gateway to use the Azure website URL, and Azure-provided certificate for encryption). We are also using a custom health probe. You will only need to override the backend path if your app is located in a subdirectory of your app service like ours is.

Health Probe

Nothing too interesting to point out on the probe, other than “Pick hostname from backend settings” is enabled. We also specify a path (optional, if your app is hosted in a subdirectory of the URL).

 

Listeners

Our HTTPS listener will reference our imported key vault certificate. We also specify the custom domain hostname. This allows traffic from the client to the app gateway, to be encrypted with our hostname certificate. Traffic from the app gateway to the app service will be encrypted using the Azure website-provided certificate.

Rules

Nothing much to point out about the rule. This is where you can set up various paths off of your hostname. If you want to host multiple app services under one hostname, you will need rules setup, so the app gateway can forward traffic to the correct app service.

Rewrite Rule Set

We will create one rule set for AAD rewrite. The set has 2 individual rules for AAD-required redirects. I want to give credit to https://medium.com/objectsharp/azure-application-gateway-http-headers-rewrite-rules-for-app-service-with-aad-authentication-b1092a58b60 for helping me find the required AAD rewrite rules. Complete life saver!

I will paste 2 screenshots of the rules below, as well as the accompanying ARM template for the rules.

These rules allow the AAD-required redirects to correctly redirect back to a custom hostname/URL while using the backend pool settings of the Azure website URL. This allows the appropriate AAD redirecting to occur while hiding the Azure website URL the whole time.

ARM Template

If you’re configuring your Application Gateway via ARM, here are the rewrite rules for AAD authentication.


{
  "rewriteRuleSets": [
    {
      "name": "AADAuth",
      "id": "[concat(resourceId('Microsoft.Network/applicationGateways', parameters('applicationGateways_cstmapps_gateway_netapp_d_cus_00_name')), '/rewriteRuleSets/AADAuth')]",
      "properties": {
        "rewriteRules": [
          {
            "ruleSequence": 100,
            "conditions": [
              {
                "variable": "http_resp_Location",
                "pattern": "(.*)(redirect_uri=https%3A%2F%2F).*\\.azurewebsites\\.net(.*)$",
                "ignoreCase": true,
                "negate": false
              }
            ],
            "name": "AAD Login Redirect",
            "actionSet": {
              "requestHeaderConfigurations": [],
              "responseHeaderConfigurations": [
                {
                  "headerName": "Location",
                  "headerValue": "{http_resp_Location_1}{http_resp_Location_2}{var_host}{http_resp_Location_3}"
                }
              ]
            }
          },
          {
            "ruleSequence": 101,
            "conditions": [
              {
                "variable": "http_resp_Location",
                "pattern": "(https:\\/\\/).*\\.azurewebsites\\.net(.*)$",
                "ignoreCase": true,
                "negate": false
              }
            ],
            "name": "AAD Callback",
            "actionSet": {
              "requestHeaderConfigurations": [],
              "responseHeaderConfigurations": [
                {
                  "headerName": "Location",
                  "headerValue": "https://{var_host}{http_resp_Location_2}"
                }
              ]
            }
          }
        ]
      }
    }
  ]
}

HTTP Header Forwarding in .NET Code

You may find that in certain instances of your code, you’re relying on HTTP headers to set certain information in your app, such as hostname. If you find that you’re seeing the azurewebsites.net URL coming through to your app, and you want the custom domain that the app was setup with, you’ll need to configure HTTP Header Forwarding.

When using header forwarding, I recommend checking the IP address of the request against the known/expected IP addresses that are in use by your application gateway. If the request is coming from one of those IPs, then we can enable forwarding on it.

You can see the basics of doing this forwarding in the .NET Framework, below, and with .NET Core onwards (much easier).

.NET Framework

OWN Startup Work

using System.Collections.Generic;
using System.Linq;
using System.Net;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(OwinStartup))]

namespace Web
{
    public class OwinStartup
    {
        private const string SecurityAppGatewayIpKey = "Security:AppGatewayIP";

        private bool IsApplicationConfiguredForApplicationGateway()
        {
            return System.Configuration.ConfigurationManager.AppSettings[SecurityAppGatewayIpKey] != null;
        }

        private List<IPAddress> GetListOfApplicationGatewayIPs()
        {
            var ipsFromAppSettings = System.Configuration.ConfigurationManager.AppSettings[SecurityAppGatewayIpKey];
            return ipsFromAppSettings.Split(';').Select(ip => IPAddress.Parse(ip)).ToList();
        }

        public void Configuration(IAppBuilder app)
        {
            if (IsApplicationConfiguredForApplicationGateway())
            {
                app.UseForwardedHeaders(GetListOfApplicationGatewayIPs());
            }
        }
    }
}

Custom Application Gateway Extension Code

using System;
using System.Collections.Generic;
using System.Net;
using System.Web;
using Owin;

namespace Promega.FindMyGene.Web
{
    public static class ApplicationGatewayExtensions
    {
        private const string ForwardedHeadersAdded = "ForwardedHeadersAdded";
        private const string ProxyAddressesAdded = "ProxyAddressesAdded";

        /// <summary>
        /// https://stackoverflow.com/questions/66382772/net-framework-equivalent-of-iapplicationbuilder-useforwardedheaders/66440470#66440470
        /// Checks for the presence of <c>X-Forwarded-For</c> and <c>X-Forwarded-Proto</c> headers, and if present updates the properties of the request with those headers' details.
        /// </summary>
        /// <remarks>
        /// This extension method is needed for operating our website on an HTTP connection behind a proxy which handles SSL hand-off. Such a proxy adds the <c>X-Forwarded-For</c>
        /// and <c>X-Forwarded-Proto</c> headers to indicate the nature of the client's connection.
        /// </remarks>
        public static IAppBuilder UseForwardedHeaders(this IAppBuilder app, List<IPAddress> ipAddresses)
        {
            if (app == null)
            {
                throw new ArgumentNullException(nameof(app));
            }

            if (!app.Properties.ContainsKey(ProxyAddressesAdded))
            {
                app.Properties[ProxyAddressesAdded] = ipAddresses;
            }

            // No need to add more than one instance of this middleware to the pipeline.
            if (!app.Properties.ContainsKey(ForwardedHeadersAdded))
            {
                app.Properties[ForwardedHeadersAdded] = true;

                app.Use(async (context, next) =>
                {
                    var request = context.Request;

                    if (request.Scheme != Uri.UriSchemeHttps && String.Equals(request.Headers["X-Forwarded-Proto"], Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
                    {
                        List<IPAddress> proxyIpAddresses = (List<IPAddress>)app.Properties[ProxyAddressesAdded];
                        if (proxyIpAddresses.Contains(IPAddress.Parse(request.RemoteIpAddress)))
                        {
                            var httpContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
                            var serverVars = httpContext.Request.ServerVariables;
                            serverVars["HTTPS"] = "on";
                            serverVars["SERVER_PORT_SECURE"] = "1";
                            serverVars["SERVER_PORT"] = "443";
                            serverVars["HTTP_HOST"] = request.Headers.ContainsKey("X-Forwarded-Host")
                                ? request.Headers["X-Forwarded-Host"]
                                : serverVars["HTTP_HOST"];
                        }
                    }

                    await next.Invoke().ConfigureAwait(false);
                });
            }

            return app;
        }
    }
}

.NET Core Onward

Courtesy of https://www.domstamand.com/controlling-the-hostname-with-a-webapp-when-fronted-by-application-gateway/, in app startup.

services.Configure<ForwardedHeadersOptions>(options =>
{
	options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto |
				   ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedHost;

	options.ForwardedHostHeaderName = "X-Original-Host";

	options.KnownNetworks.Clear();
	options.KnownProxies.Clear();
});

This blog post has gone through, how to configure your application gateway to serve mulptiple applications on multiple custom domains, and AAD authentication setup, all without the need of having custom domains and TLS certificates set on your app services. Thanks for reading!