Sign Up
Hero

ASP.NET Core Authentication Behind Proxies

Learn how to overcome ASP.NET Core authentication configuration issues when your application is behind a proxy, load balancer, gateway, container, or similar system.

When your ASP.NET Core application runs behind a proxy, a load balancer, or even within a container, its authentication flow may break. Let's try to understand why and what solutions we can adopt.

The Broken Behavior

You have built your awesome ASP.NET Core application and have integrated it with authentication. Everything works like a charm in your local development environment. It's time to deploy it on your stage environment (or do you prefer to go directly into production? 😏).

Let's assume you don't serve your web application directly through a web server. You may need a slightly more complex infrastructure for several reasons:

  • Load balancing, caching, compression, and other performance features.
  • Request and response processing.
  • Request filtering and other security features.
  • Portability, process isolation, and other containerization features.

Whatever your reason is, there are chances that your application will run within a container or behind a reverse proxy, a load balancer, an application gateway, etc. Regardless of the specific function, we can call this intermediary between a client and your application a proxy. The following diagram describes this generic scenario:

In this scenario, your awesome application that was perfectly working in your local development environment may stop working.

The specific error depends on your particular environment and configuration, but typically, the type of error falls into one of the following categories:

  • The callback URL uses HTTP instead of HTTPS.
  • The callback URL uses an internal domain name instead of the public domain name.
  • An unexpected error occurs.

Let's take a look at each of these categories in the following subsections considering a scenario with Auth0 as the Identity provider.

The callback URL uses HTTP instead of HTTPS

Assume that your application is reachable at the address https://my-awesome-app.com and everything is correctly configured on the Auth0 side. When you try to authenticate, you get an error similar to the following:

Callback URL mismatch.
http://my-awesome-app.com/callback is not in the list of allowed callback URLs.

Actually, you have registered the callback URL on the Auth0 side, but your registered its scheme as https while the error message reports http. Why is your actual callback URL using http?

Usually, this type of problem occurs when your application runs within a container service such as Azure Container Apps, AWS Elastic Container Service, or other similar services.

The callback URL does not use the public domain name

Another possible scenario is when you get an error like the following:

Callback URL mismatch.
https://localhost/callback is not in the list of allowed callback URLs.

You correctly defined the callback URL with your domain β€” my-awesome-app.com β€” on the Auth0 dashboard, but your application builds the callback URL using localhost. In some scenarios, the domain of the callback URL can be an internal domain, such as the subdomain name assigned to your application instance by Azure or AWS (e.g., internal-name.azure.com or internal-name.amazonws.com).

Usually, this issue occurs when an application runs behind services like Azure Gateway, Azure Front Door, AWS Elastic Load Balancing, AWS CloudFront, etc. but also behind reverse proxies like nginx or Caddy.

An unexpected error occurs

Other different errors may occur depending on your specific infrastructure. For example, you may experience error messages like the following:

An unhandled exception occurred while processing the request. Exception: Correlation failed.

This does not directly say what the reason for the problem is, but it may depend on the same scenario we are going to illustrate in a moment.

A few considerations

Let's consider a few things before analyzing the issue that may cause this type of error and how to solve it.

We mentioned Blazor as the framework used for building your awesome application, but consider it just an example. Any ASP.NET Core-based framework is subject to the problem we described: ASP.NET Core MVC, Blazor, and Razor Pages.

Also, we are using Auth0 as an example of an OpenID Connect Identity provider, but this is NOT a problem on the Auth0 side or the side of other identity providers. It's not even an OpenID Connect problem. As you will learn, it's a problem of ASP.NET Core. Specifically it's a problem with the OpenID Connect middleware.

More in general, the problem may affect any web application regardless of its development stack, but we are focusing on ASP.NET Core because of its specific behavior and solution.

Why Does My Application Not Work?

To understand why these problems occur, you need to know how the ASP.NET Core OpenID Connect middleware works internally β€” at least at a high level β€” and how the HTTP traffic flows in an infrastructure where proxies are included.

How OpenID Connect middleware works

To implement user authentication using OpenID Connect, you should implement the details of the protocol. This would be a daunting task, but fortunately, ASP.NET Core provides specific middleware that alleviates some of this burden.

The Auth0 SDK for ASP.NET Core makes things even easier, but it also relies on the OpenID Connect middleware, so you cannot avoid the problems we are dealing with here.

Without going into the details of the protocol, one of the tasks of the middleware is to build the URL of the application to which the user's browser will be redirected after authentication. Through this URL, the application receives the authorization code that enables it to get the ID and access tokens. This URL is the so-called redirect URI or callback URL.

The middleware automatically builds this URL based on internal parameters and uses some properties of the HTTP request to build the base URL.

For example, suppose the middleware handles an HTTP request to https://example.com/login. The callback URL will be built as https://example.com/signin-oidc.

If you are using the Auth0 SDK for ASP.NET Core, the default callback URL will be https://example.com/callback.

As you can see, the base path of the callback URL is the same as the request URL.

HTTP requests and proxies

Consider the diagram shown above:

As said, the proxy between the client and your application can have several tasks. Reverse proxying is one of these tasks. Basically, a reverse proxy accepts requests from a client and forwards them to one or more applications.

In some scenarios, the reverse proxy is also used as TLS termination proxy: it encrypts and decrypts communications outside your hosting infrastructure via TLS tunnels. In other words, a reverse proxy accepts secure HTTPS requests but uses plain HTTP to forward the request internally, as highlighted in the following diagram:

In this case, your application will receive a request to http://example.com and then the middleware will build http://example.com/signin-oidc as the callback URL. This will differ from the allowed callback URL you registered with Auth0, and this is why you will get the first case of error: your callback URL has the HTTP scheme instead of HTTPS.

We mentioned earlier that the HTTP vs. HTTPS issue often affects applications running within a container service. The reason is that these services use a proxy to route the traffic, and this proxy provides TLS termination.

In the example above, we supposed that the internal domain name of your application instance is the same as the public one: example.com. This rarely happens. Usually, your internal application instance is reachable through an IP address or an internal name, such as my-app-instance.example.com, as shown below:

In this case, your application will receive a request to https://my-app-instance.example.com and the middleware will build a callback URL as https://my-app-instance.example.com/signin-oidc. Again, this URL differs from the allowed callback URL you registered with Auth0. By the way, depending on the configuration of your hosting environment, the internal URL of your application may not be publicly reachable.

Different error messages may arise in other scenarios or configurations, but often the problems are related to this difference between the public address and the internal address of your application.

How to Fix the Broken Behavior

Once you understand why these errors occur, it's time to learn how to fix these issues.

As you learned in the previous section, when your ASP.NET Core application runs behind a reverse proxy, load balancer, or similar network component, it may lose access to some information coming with the original HTTP request. You need a way to access this information and provide it to the OpenID Connect middleware to build a correct callback URL.

Meet the X-Forwarded-* headers

Most reverse proxies forward the original HTTP request data through specific HTTP headers. The most common are:

  • X-Forwarded-For. This header contains the IP address of the original client.
  • X-Forwarded-Host. This one has the original value of the Host header included in the original client's request.
  • X-Forwarded-Proto. In this header, you can find the protocol used for the original request β€” HTTP or HTTPS.

Thanks to these headers, you can access the relevant values of the original HTTP request and build your callback URL correctly.

Despite what is commonly thought, X-Forwarded-* headers do not derive from an official standard but are so widely used that they can be considered a de facto standard. Actually a standard exists and it's based on a single header: Forwarded. However, the X-Forwarded-* headers are still more popular.

Make sure your proxy is correctly configured in order to trust the values forwarded via X-Forwarded-* headers. In fact, the client's request may go through a chain of proxies on the Internet, and if your proxy simply forwards the X-Forwarded-* headers it receives, you may be exposed to security risks. Well-configured proxies don’t accept forwarded headers. Instead, they populate these headers with reliable values like the TCP connection and original HTTP headers such as Host. Read this article for an example of security issues to which a misconfigured proxy can lead.

Using Forwarded Headers middleware

Fortunately, you don't need to directly access HTTP requests to extract the values of X-Forwarded-* headers. ASP.NET Core provides you with middleware that simplifies your life: the Forwarded Headers middleware implemented by the ForwardedHeadersMiddleware class.

Basically, this middleware extracts the original values of the client request forwarded by the proxy and makes them available to you as first-hand values of the Request object. This saves you from writing specific code for your application when it runs behind a proxy.

To enable the Forwarded Headers middleware, include it in the request pipeline of your application by adding the line highlighted below to your Program.cs file:

// Program.cs

//...existing code

var app = builder.Build();

//πŸ‘‡ new code
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
  ForwardedHeaders = ForwardedHeaders.XForwardedProto |
                     ForwardedHeaders.XForwardedHost;
});
//πŸ‘† new code

app.UseAuthentication();

//...existing code

Make sure you call the UseForwardedHeaders() method before other middleware. The middleware order is important, and adding it as the first middleware in the request pipeline ensures that the values of the original request are propagated consistently.

In the example above, you enabled the mapping of the X-Forwarded-Proto and X-Forwarded-Host headers, which are needed to make the OpenID Connect middleware build the callback URL correctly. In addition, any statement that accesses HttpContext.Request.Scheme and HttpContext.Request.Host will get the value coming from the original request.

Optionally, you can also configure the Forwarded Headers middleware to only accept X-Forwarded-* headers coming from a trusted proxy, as shown in the following code snippet:

// Program.cs

//...existing code

var app = builder.Build();

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
  ForwardedHeaders = ForwardedHeaders.XForwardedProto |
                     ForwardedHeaders.XForwardedHost;
  KnownProxies.Add(IPAddress.Parse("10.0.0.42"); //πŸ‘ˆ new code
});

//...existing code

You can also add multiple trusted proxies and configure the middleware to accept custom header names. See the documentation for more details.

Not all proxies or other network appliances add the X-Forwarded-* headers by default. They may need additional configuration. Consult their documentation to learn how to configure it.

Try out Auth0 authentication for free.Get started β†’

No code alternative?

If you don't want to or cannot change the code of your ASP.NET Core application, an alternative exists. You can set the environment variable ASPNETCORE_FORWARDEDHEADERS_ENABLED to true, and the Forwarded Headers middleware is automatically added to the request pipeline and configured.

While this approach is very convenient, unfortunately it only maps the X-Forwarded-For and X-Forwarded-Proto headers. This means that it could not be sufficient if your scenario also requires using the original host name, as in the authentication case we are discussing here. Also, consider that this approach does not allow you to define your trusted proxies and handle customized headers.

Conclusion

Now you know the type of errors you can run into when you publish your application on an infrastructure a little more complex than a simple web server.

We have explored a number of cases where OpenID Connect-based authentication no longer works due to the loss of original HTTP request information. However, we have seen how to fix the problem simply by using the Forwarded Headers middleware.

Before concluding, we want to highlight that this type of problem is related to more than just authentication. It involves any functionality that requires dynamic URL generation, as well as client geolocation and other processing based on the client's original request. For more information on the different scenarios, see this document.