dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.38k stars 9.99k forks source link

OIDC middleware will not use forwarded headers in challenge construction #58455

Closed brgrz closed 6 days ago

brgrz commented 1 week ago

Is there an existing issue for this?

Describe the bug

Having ASP.NET Core application running in a container on Azure Kubernetes Services cluster behind an ingress-nginx with this program.cs

...
            var authOptions = builder.Configuration.GetRequiredOptions<Auth0Options>(Auth0Options.ConfigKey);

            builder.Services
                .AddAuthentication(cfg =>
                {
                    cfg.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    cfg.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
                })
                .AddCookie()
                .AddOpenIdConnect(cfg =>
                {
                    cfg.Authority = authOptions.Authority;
                    cfg.ClientId = authOptions.ClientId;
                    cfg.ClientSecret = authOptions.ClientSecret;
                    cfg.ResponseType = "code";
                    cfg.UsePkce = true;
                    cfg.SaveTokens = true;

                    cfg.Scope.Clear();
                    foreach (var scope in authOptions.Scopes.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
                    {
                        cfg.Scope.Add(scope);
                    }
                });

            builder.Services.Configure<ForwardedHeadersOptions>(options =>
            {
                options.ForwardedHeaders =
                    ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
            });

            var app = builder.Build();

            app.UseForwardedHeaders(new ForwardedHeadersOptions
            {
                ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor
            });

            // mind the order

            // #1
            app.UseAuthentication();

            // #2
            app.Use(async (context, next) =>
            {
                if (context.User.Identity is null || !context.User.Identity.IsAuthenticated)
                {
                    // here I verify that correct x-forwarded-* headers are currently present
                    Log.Information("Headers: {Headers}", context.Request.Headers);

                    await context.ChallengeAsync();
                    return;
                }

                await next();
            });

            // #3
            app.UseAuthorization();

            ...

I observe that when ChallengeAsync() is called and the redirect is made to Auth0 Identity Provider the redirect_uri inserted into the request URL will lose the https scheme and use http.

Headers: ["[Accept, text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7]", "[Host, hangfire.dev.domain.com]", "[User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0]", "[Accept-Encoding, gzip, deflate, br, zstd]", "[Accept-Language, en]", "[Upgrade-Insecure-Requests, 1]", "[X-Request-ID, a2eae22eec38ed62a5a35cf4bcd0a449]", "[X-Real-IP, **]", "[X-Forwarded-For, **]", "[X-Forwarded-Host, hangfire.dev.domain.com]", "[X-Forwarded-Port, 443]", "[X-Forwarded-Proto, https]", "[X-Forwarded-Scheme, https]", "[X-Scheme, https]", "[sec-ch-ua, \"Microsoft Edge\";v=\"129\", \"Not=A?Brand\";v=\"8\", \"Chromium\";v=\"129\"]", "[sec-ch-ua-mobile, ?0]", "[sec-ch-ua-platform, \"Windows\"]", "[DNT, 1]", "[sec-fetch-site, none]", "[sec-fetch-mode, navigate]", "[sec-fetch-user, ?1]", "[sec-fetch-dest, document]", "[priority, u=0, i]"]

https://dev-*.eu.auth0.com/authorize?client_id=v3fq3h4wtaYs71R1q3w0QffYiB4CuqA5&redirect_uri=http%3A%2F%2Fhangfire.dev.domain.com%2Fsignin-oidc&response_type=code&scope=openid%20profile%20email&code_challenge=a7Zle0HiuPZd_RPoSUFjwFKI&code_challenge_method=S256&response_mode=form_post&nonce=638646732839944170&state=XG-uoQem9jrNi028&x-client-SKU=ID_NET8_0&x-client-ver=8.1.2.0

The result is that Auth0 denies the request because URL with a http scheme has not been added into the Allowed redirect URLs with Auth0 (correctly so and won't be added).

The nginx ingress correctly provides the x-forwarded-* headers and those are present in the current context/request. So the app knows that the original request was made to the https scheme and terminated by the nginx proxy which also correctly forwarded this information on.

I found this SO question with an answer that offers a workaround but imo this shouldn't need a workaround, this is a bug within the OIDC flow in ASP.NET Core.

Expected Behavior

The ASP.NET Core OIDC auth provider should construct the redirect_uri query parameter for the OIDC IdP call with the correct/original scheme even though the app is running within the http scheme BUT it is behind a proxy which handles SSL termination (and also enforces https).

Steps To Reproduce

Take a sample ASP.NET Core project and copy the provided code into Program.cs and you'll also need an application registered with one of the OIDC providers (Azure Entra, Auth0, Okta, Keycloak).

Exceptions (if any)

No response

.NET Version

8.0.403

Anything else?

No response

halter73 commented 1 week ago

This looks similar to #57650

Can you include the trace-level ASP.NET Core logs from the ForwardedHeadersMiddleware?

You might be running into an issue because the middleware verifies that the client claiming to be the proxy has the expected remote IP address as noted in the doc's troubleshooting section.

The request's original remote IP must match an entry in the KnownProxies or KnownNetworks lists before forwarded headers are processed. This limits header spoofing by not accepting forwarders from untrusted proxies. When an unknown proxy is detected, logging indicates the address of the proxy:

dbug: Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware[1]
     Unknown proxy: [::ffff:10.0.0.100]:54321

By default, KnownProxies and KnownNetworks only contain [::1] and 127.0.0.0/8 respectively. I'm a little surprised that this only gets checked when ForwardedHeaders.XForwardedFor is selected and not ForwardedHeaders.XForwardedProto, but being able to spoof the scheme does seem like less of a security risk than being able to spoof a remote IP address.

If you know the IP address or subnet of the reverse proxy, I recommend adding it to KnownProxies or KnownNetworks. Otherwise, you can clear both lists to skip the check, but you need to beware that any client that can open a connection to your backend server will be able to spoof their IP address.

Another option is to set the ASPNETCORE_FORWARDEDHEADERS_ENABLED environment variable to true or otherwise set the "ForwardedHeaders_Enabled" IConfiguration to true.

To forward the scheme from the proxy in non-IIS scenarios, enable the Forwarded Headers Middleware by setting ASPNETCORE_FORWARDEDHEADERS_ENABLED to true. Warning: This flag uses settings designed for cloud environments and doesn't enable features such as the KnownProxies option to restrict which IPs forwarders are accepted from.

https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-8.0#forward-the-scheme-for-linux-and-non-iis-reverse-proxies

If you do this, you will no longer need to call Configure<ForwardedHeadersOptions>(... or app.UseForwardedHeaders(); yourself. WebApplicationBuilder will set that up for you when it sees the "ForwardedHeaders_Enabled" config.

dotnet-policy-service[bot] commented 1 week ago

Hi @brgrz. We have added the "Needs: Author Feedback" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

brgrz commented 6 days ago

Another option is to set the ASPNETCORE_FORWARDEDHEADERS_ENABLED environment variable to true or otherwise set the "ForwardedHeaders_Enabled" IConfiguration to true.

To forward the scheme from the proxy in non-IIS scenarios, enable the Forwarded Headers Middleware by setting ASPNETCORE_FORWARDEDHEADERS_ENABLED to true. Warning: This flag uses settings designed for cloud environments and doesn't enable features such as the KnownProxies option to restrict which IPs forwarders are accepted from.

https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-8.0#forward-the-scheme-for-linux-and-non-iis-reverse-proxies

I chose this option and indeed it solved the issue when redirect_uri was made with http scheme. It now includes the correct, https, scheme in redirect_uri.

I wish this was more clearly documented but I know it is hard bc there might be many moving parts in each different hosting configuration and/or cloud provider. I do think this section NGINX configuration and the section you linked to should be placed together in docs and also, very important, maybe differences be pointed out more clearly and also it couldn't hurt if specifically Kubernetes, AKS or ingress were mentioned because atm it stands out that it talks about Azure Linux App Service, Azure Linux VM and when as a reader you see that you immediatelly skip it and look elsewhere, thinking it is not your use case.

If you do this, you will no longer need to call Configure<ForwardedHeadersOptions>(... or app.UseForwardedHeaders(); yourself. WebApplicationBuilder will set that up for you when it sees the "ForwardedHeaders_Enabled" config.

I can confirm this, I was able to removed those lines and it still works.

While at it, if anyone else stumbles across this issue, after the redirection starts to work you might also run into 502 Bad Gateway at the nginx side with upstream sent too big header while reading response header from upstream issue in the nginx ingress logs. This is another piece of the problem which took some time from me thinking it was application related but it is not. The solution is to configure ingress annotations like so:

## Fixed the ingress issue of "upstream sent too big header while reading response header from upstream" by setting ingress buffer size
nginx.ingress.kubernetes.io/proxy-buffer-size: 256k

refer to the nginx ingress annotations for more info https://github.com/kubernetes/ingress-nginx/blob/main/docs/user-guide/nginx-configuration/annotations.md