Skip to content
Jonathan George By Jonathan George Software Engineer IV
Correctly configuring ASP.NET Core MVC authentication when hosting in Azure Container Apps

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 Program.cs file.

Firstly, adding the necessary services:

builder.Services
    .AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization(
    options =>
    {
        options.FallbackPolicy = options.DefaultPolicy;
    });

IMvcBuilder mvcBuilder = builder.Services
    .AddControllersWithViews()
    .AddMicrosoftIdentityUI();

And then configuring the app middleware:

app.UseAuthentication();

app.UseAuthorization();

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:

  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "<your-domain>",
    "TenantId": "<your-aad-tenant-id>",
    "ClientId": "<id-of-the-app-registration-created-for-auth>",
  },

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 https://<my-app-url>, the redirect_url being sent into the authentication flow was http://<my-app-url>/signin-oidc. Note that it's http not 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. http or https.
  • 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 HttpContext.Request.Host
  • X-Forwarded-For: Used to populate HttpContext.Connection.RemoteIpAddress

You can read more detail on exactly how this works and what controls you have over the process on this page.

Enabling ForwardedHeadersMiddleware

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:

services.Configure<ForwardedHeadersOptions>(options =>
{
    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 options.ForwardedHeaders to ForwardedHeaders.None.

If you don't require any customization, you can enable it via environment variable. Simply set ASPNETCORE_FORWARDEDHEADERS_ENABLED to 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!

Jonathan George

Software Engineer IV

Jonathan George

Jon is an experienced project lead and architect who has spent nearly 20 years delivering industry-leading solutions for clients across multiple industries including oil and gas, retail, financial services and healthcare. At endjin, he helps clients take advantage of the huge opportunities presented by cloud technologies to better understand and grow their businesses.