We learn to configure our app and overcome the dreaded AADB2C90006 issue to run our ASP.NET Core application in Google App Engine.
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:
- Google App Engine, so we can take advantage of its built-in geolocation service for web visitors (a HUGE value-add for location-based apps, in our opinion)
- Algolia for resume searches
- Google Firestore, the GCP-specific version that's replacing their existing Datastore product
- Azure Functions which runs a service leveraging Syncfusion's DocIO and PDF libraries to extract resume text from Word and PDF documents for storage in Algolia
- Azure ServiceBus to relay the message to the function that the resume is sitting in storage and waiting for parsing
- Azure Active Directory B2C, which is still our favorite authentication layer for our applications
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");
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.
Looking for help building or integrating your web application with Azure AD B2C? Contact us for a quote - not only is it free, but we can help you find what you're looking for at a better price than most consulting firms!