I recently needed to set up authentication in an ASP.NET Core MVC application. Everything went smoothly until I came to deploy it to Azure Container Apps, where I ran into an issue.
For background, I'm using the Microsoft.Identity.Web library to integrate AAD Authentication into my ASP.NET Core MVC application. This is a helper library that makes it straightforward to hook up ASP.NET Core authentication with the Microsoft Authentication Library for .NET (also known as MSAL). It's configured as follows in my
Firstly, adding the necessary services:
options.FallbackPolicy = options.DefaultPolicy;
IMvcBuilder mvcBuilder = builder.Services
And then configuring the app middleware:
The first interesting part of this is the call to
AddMicrosoftIdentityWebApp, which configures all the necessary services to integrate with the Microsoft Identity platform. The specific details of the AAD tenant, app registration and so on are stored in config. For example, straightforward AAD integration looks like this:
The second interesting part is the call the
AddMicrosoftIdentityUI. This adds an MVC Area to your application, giving the following:
- A new controller,
AccountController, which provides actions for signing in, out, resetting your password and editing your profile. Mostly these redirect to the appropriate locations derived from the selected authentication scheme.
- Some basic views for access denied, error and "signed out" pages.
With this in place, adding an
[Authorize] attribute (or equivalent) to your controllers and then attempting to access them will result in you being redirected through the sign in process.
It works on my machine...
This all worked fine when running locally. However, when deployed to Azure Container Apps, I hit an issue.
When I attempted to access my app, what's supposed to happen is a redirect to the AAD login flow. Various pieces of information are passed as part of that URL, including the
redirect_uri. This is the URL that AAD will
POST back to once the user has completed the authentication flow and (if all goes well) will return the user's token.
Although I was accessing the app on the URL
redirect_url being sent into the authentication flow was
http://<my-app-url>/signin-oidc. Note that it's
https. Unfortunately this isn't correct. AAD doesn't allow you to use a return URL that's not secured and even if it did, it would be a really bad idea.
So why is this happening?
Azure Container Apps networking 101
When you access an containerised application running in Azure Container Apps, you don't talk directly to the container. How your traffic ends up there depends on your ingress settings.
When you configure HTTP ingress, your traffic is routed through an HTTP proxy; in this case, it's Envoy. It does a variety of things but the important one from our perspective is that it provides TLS termination.
This means that while communication with the ingress is via
https on port 443, communication from the ingress to your container is over
http on port 80. As far as your application knows, it's being accessed insecurely.
This is problematic because it means if you construct absolute URLs in code by modifying a request URL (which is what's happening in the
Microsoft.Identity.Web code), you'll end up with the wrong URL. Not necessarily just the wrong protocol, but the domain part of the request might also be different.
So, how do we fix this?
ForwardedHeadersMiddleware to the rescue
Fortunately the fix for this is trivial. This kind of thing is a fairly common issue when using proxies like Envoy, so the norm is that when they modify the incoming request they add headers to inform the downstream recipient what was changed. There are several widely used variants on the names of these headers, and an official standard which you can read here.
Envoy currently uses the following non-standard but widely used header names:
X-Forwarded-Proto- will contain the protocol that the client used to connect to the proxy - i.e.
X-Forwarded-Host- the original host requested by the client.
X-Forwarded-For- contains information about the client that made the request and potentially any intermediate proxy servers.
ASP.NET Core has had support for these headers out of the box since v1, via the
ForwardedHeadersMiddleware. This takes values provided in the three headers listed above and reinstates them in the location you'd expect to find them in if the request had come in directly:
For example, the value from
X-Forwarded-Proto is used to populate
HttpContext.Request.Scheme. This means that anything using this to build fully qualified links - such as our
Microsoft.Identity.Web code - will have the correct scheme.
The other two headers are used as follows:
X-Forwarded-Host: Used to populate
X-Forwarded-For: Used to populate
You can read more detail on exactly how this works and what controls you have over the process on this page.
There are two ways to enable it.
Firstly you can add it in your app startup with
app.UseForwardedHeaders();. This is the best choice if you need to customize the header names or which headers are used via the
ForwardedHeadersOptions you add to the service collection:
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.ForwardedForHeaderName = "custom-name-for-this-header";
Note that if you do enable forwarded headers in code, you must provide a
ForwardedHeadersOptions and explicitly specify the list of headers to forward as the default is to set
If you don't require any customization, you can enable it via environment variable. Simply set
true and this will enable the forwarded headers middleware with all headers being forwarded.
For my use case, there were two options on where to set this environment variable:
- In the Dockerfile being used to build my app container
- As part of the deployment of the container to ACA
The documentation suggests you should only use the environment variable approach when you're positive that your application is running behind a trusted proxy. As a result, it makes sense to set it as part of the ACA deployment rather than in the container; this means that if the container were deployed elsewhere, the
ForwardedHeadersMiddleware wouldn't automatically be enabled and we wouldn't have to worry about inadvertently introducing a security risk.
Once I'd done this for my container app, the authentication redirect worked as originally expected, with the constructed
redirect_uri having the
https scheme and the authentication flow calling back into my application correctly. Success!