Running ASP.NET Core Apps in Google App Engine with Azure AD B2C

Sunday, January 27, 2019

Running ASP.NET Core Apps in Google App Engine with Azure AD B2C

TL;DR: We found a simple fix to error ADDB2C90006 when using Azure B2C where our redirect_uri was coming back http instead of https. You can read details about the issue - and the fix - on StackOverflow.

We're getting into the later stages of a jobs website we're building in ASP.NET Core. We're pretty jazzed about the technology stack overall; we're using:

Now all of these products play together very nicely, mostly due to the fact that they consist almost entirely of client libraries and API calls, so mashups are very easy. We're probably going to make some tweaks to this down the road, like moving the Azure function to a service that runs with the app in App Engine because it wasn't clear to us initially that we could do that. We also wanted to feel out Azure Functions and its ability to play nicely with the Google security and API layers, something that seemed to be a bit of an issue in their first iteration. Happy to report full integration there.

ASP.NET Core on App Engine

If you're not familiar with App Engine, it's Google's Azure equivalent of App Services, only it's been around since 2008, back when Microsoft Azure was a collection of APIs under the Windows Live platform. Google recently came out with a feature called the App Engine Flexible Environment, which allows for a much wider variety of runtimes and versions than the Standard Environment which until only recently had only supported very old runtime versions of Java, Python and PHP. Anyways, .NET (Core, of course) is now a supported runtime for App Engine (flex), so your ASP.NET Core apps can run as first-class citizens and enjoy the benefits of the App Engine runtime (did I mention the thing about the geo-location services? That's YUGE).

When you deploy into App Engine, you have two things going on:

  • Your application is running using the Kestrel web engine
  • App Engine is running Nginx as a reverse proxy in front of your Kestrel server

These things are not as tricky as they seem. In fact, it only appears to be tricky because there is no documentation that really specifies how the systems interact with each other. There is, however, this document on how to host ASP.NET Core on Linux with Nginx, so that's a great start - but we need to make some modifications.

Kestrel Setup

My Program.cs has a very simple method for building the IWebHostBuilder:

public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
    return new WebHostBuilder()
        .UseKestrel()
        .UseContentRoot(Directory.GetCurrentDirectory())
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            var env = hostingContext.HostingEnvironment;
            config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json",
                    optional: true, reloadOnChange: true);
            config.AddEnvironmentVariables();                    
        })
        .ConfigureLogging((hostingContext, logging) =>
        {
            if (!hostingContext.HostingEnvironment.IsDevelopment()) return;
            logging.AddConsole();
            logging.AddDebug();
            logging.AddEventSourceLogger();
        })
        .UseStartup<Startup>();
}

You'll note that I don't explicitly set up Kestrel with SSL; this is because App Engine (Nginx) terminates all https sessions before forwarding, which becomes a tricky point later with Azure AD B2C.

In my Startup.cs I have this:

public void Configure(IApplicationBuilder app, IHostingEnvironment environment, ILoggerFactory loggerFactory)
{
	//code for setting up Stackdriver exception logging and tracing

    app.UseHttpsRedirection();
    app.Use(async (context, next) =>
    {
        if (context.Request.IsHttps)
        {
            await next();
        }
        else
        {
            var log = loggerFactory.CreateLogger("HTTPS Middleware");
            log.LogInformation($"Request URL: {Microsoft.AspNetCore.Http.Extensions.UriHelper.GetDisplayUrl(context.Request)}");
            string queryString = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : string.Empty;
            var https = "https://" + context.Request.Host + context.Request.Path + queryString;
            context.Response.Redirect(https);
        }
    });
    
    if (environment.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();                
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");

        //get ALL headers!
        app.UseForwardedHeaders(new ForwardedHeadersOptions
        {
            ForwardedHeaders = ForwardedHeaders.All
        });
    }

    //static file, authentication and routing calls

}

SSL

One thing that should stand out is the fact that we don't explicitly set up Kestrel to use HTTPS. The reasoning for this is simple: we have it set up with App Engine (and therefore Nginx), and if we attempt to set it up in Kestrel, the Kestrel changes override the Nginx changes. It could be simply because we're doing something wrong; indeed, we're getting the "failed to determine the https port for redirect" mentioned in the Enforce HTTPS article in the ASP.NET Core docs, but here's what we're running into:

  • if we set up a port (say, 5050 for example) in Kestrel, then our URLs show up with port 5050 in them, which App Engine/Nginx doesn't use, therefore breaking everything
  • If we simply use HTTPS - and make sure Nginx gives us https URL paths (important!), then everything works as intended.

Ideally we want to see that 'failed to determine the https port for redirect' message go away, but it's not breaking anything yet, so we're deliberately going to ignore it.

Google App Engine and Azure AD B2C

Azure AD B2C is a great product for setting up claims-based authentication using your own custom Active Directory instance for your customers. Frankly we're surprised it hasn't gained more traction, especially in light of the egregious number of security breaches from developer hubris when it comes to authentication and security policies. Setup was incredibly easy, and we were able to get B2C running in less than a day in our development environment. Having the external sources aligned with your Active Directory is also a nice way of keeping a large distance between your user security and your application.

It does have its weaknesses; for starters, you have to use the Azure AD Graph API to interact with the Active Directory environment, so if you wanted to see if your user belonged to a particular group (or add them to one), you have to set up a separate service to use Azure AD Graph API to interact with it. You CAN, however, read the sign-in policy name in the list of claims, so you can use that to identify a type of user. This isn't ideal for most apps, however, so the limitations can get frustrating.

When we first deployed to App Engine, we were immediately hit with a challenge: Whenever we went to authenticate our test user, we would get an AADB2C90006 error because our Reply URL would come back with an http scheme instead of https. This was incredibly baffling because you're not even allowed to specify http anywhere anymore, let alone try to use it as a callback to your application with security credentials. And to be honest, we're really unclear as to why this is happening - but we do have a fix for it.

Preventing AADB2C90006 in Azure AD B2C

We used the Azure AD B2C Sample on GitHub to poach some code in order to streamline our development; mainly, we took the OpenIdConnectOptionsSetup module. In this module is a class named OpenIdConnectOptionsSetup that you can use for configuring a number of different OpenID options. One of these options is the OnRedirectToIdentityProvider delegate in the OpenIdConnectEvents property of the OpenIdConnectOptions options.

Here's our modified OnRedirectToIdentityProvider delegate:

public Task OnRedirectToIdentityProviderAsync(RedirectContext context)
{
	//it's literally the first thing we check for
    if (context.ProtocolMessage.RedirectUri.Contains("http:"))
    {                    
        Logger.LogInformation("http: found in RedirectUri, replacing with https");
        context.ProtocolMessage.RedirectUri = context.ProtocolMessage.RedirectUri.Replace("http:", "https:");
    }
    //the rest of it is the boilerplate from the sample

}

This simple fix to make sure you're sending https to Azure AD B2C. What you'll inevitably find infurating is that you don't have http defined anywhere but (without the code above) you'll still get this error. We submitted a pull request to the Azure AD B2C sample authors to include this blurb after finding several other people had this problem, but we'll see what happens.

Side Note: /signin-oidc

If you've ever wondered what the /signin-oidc part of your URL is, apparently it's a pre-baked URL in the OpenIdConnect middleware that Azure AD uses. There's a nice little conversation about it on GitHub here.