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.46k stars 10.03k forks source link

AddAuthentication with AddOpenIdConnect, and webapp behind a proxy produce different callback URIs depending on client side rendering or server side rendering #57916

Closed jan-johansson-mr closed 1 month ago

jan-johansson-mr commented 1 month ago

Is there an existing issue for this?

Describe the bug

The environment have a reverse proxy server (yarp) (front-end facing), with an identity server (using OpenIddict) and a web app (Blazor). All nodes runs on docker images. I have two solutions for the web app. One is client side rendering and the other is server side rendering (dotnet 8). The proxy server forward headers to the nodes behind, and each node behind is configured to handle the forwarded headers, i.e.

    var forwardedHeaderOptions = new ForwardedHeadersOptions
    {
        ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost,
    };

    forwardedHeaderOptions.KnownNetworks.Clear();
    forwardedHeaderOptions.KnownProxies.Clear();

    app.UseForwardedHeaders(forwardedHeaderOptions);

Both web apps are identical with the core logic and configuration, i.e.

    builder.Services
        .AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.SignOutScheme = OpenIdConnectDefaults.AuthenticationScheme;
            options.Authority = "https://my.domain.net/identity";
            options.ResponseType = OpenIdConnectResponseType.Code;
            options.GetClaimsFromUserInfoEndpoint = true;
            options.ClientId = "mvc";
            options.ClientSecret = "...";
            options.UsePkce = true;
            options.Scope.Add("email");
            options.Scope.Add("roles");
            options.MapInboundClaims = false;
            options.TokenValidationParameters.NameClaimType = JwtRegisteredClaimNames.Name;
            options.TokenValidationParameters.RoleClaimType = "roles";
            options.CallbackPath = "/callback/login";
            options.RemoteSignOutPath = "/local_logout";
            options.SignedOutCallbackPath = "/callback/logout";
            options.SignedOutRedirectUri = "https://my.domain.net/cookie_logout";
            options.Events.OnRemoteSignOut = (context) =>
            {
                return Task.CompletedTask;
            };
        });

The login path works great, with the correct callback login URI (my.domain.net/callback/login) regardless of client side rendering or server side rendering. The logout path differs. It has the correct callback logout URI (my.domain.net/callback/logout) with the client side rendering solution, but wrong callback logout URI (my.docker.node.host/callback/logout) with the server side rendering solution (I'm running one solution at a time, on the same docker host, i.e my.docker.node.host). I have no idea why it should differ, since the environments are identical, with the only difference is that one solution is client side rendering and the other is server side rendering. And even more confusing is that both solutions have the correct callback login.

I also verified the difference by setting a break point at the OnRemoteSignOut event, and exploring the context object. The Host attribute with the request to the identity server have my.docker.node.host value, and not the expected my.domain.net value when running the server side rendering solution.

Expected Behavior

That the forwarded headers with host information should apply to the HttpContext Host value, and be reflected in the produced callback URI given to the Identity Server. This behavior is correct in both solutions when login, but differs when logout. Both solutions have the same configuration, and core logic.

Steps To Reproduce

No response

Exceptions (if any)

No response

.NET Version

Microsoft.AspNetCore.App 8.0.8, and Microsoft.NETCore.App 8.0.8

Anything else?

No response

jan-johansson-mr commented 1 month ago

As a follow-up to the issue.

When I switch the order of how I do a sign-out, using CookieAuthenticationDefaults.AuthenticationScheme (instead of OpenIdConnectDefaults.AuthenticationScheme) and using the event OnRemoteSignOut to redirect the sign-out flow to an endpoint that manually sign-out with the identity server, using OpenIdConnectDefaults.AuthenticationScheme, then the callback URI is correct and the flow works, both with sign-in and sign-out, independent of where the rendering takes place (client or server).

I'm still puzzled about why the issue exists in the first place.

Thanks!

mkArtakMSFT commented 1 month ago

Thanks for contacting us. This looks like an issue with the reverse proxy setup, rather than the auth configuration.

Is there a call to the .UseAuthentication() after your app.UseForwardedHeaders call?

jan-johansson-mr commented 1 month ago

Thanks @mkArtakMSFT,

No, I have app.UseForwardedHeaders() as the first middleware, and then later app.UseAuthorization(), and before app.UseAntiforgery().

This is the middleware setup

Maybe this will help!

MackinnonBuck commented 1 month ago

Thanks, @jan-johansson-mr.

Could you please provide us with a minimal repro project so that we can investigate this further? Please also include a docker compose file so that we can ensure that our environment matches yours. Ideally the project would be hosted in a public GitHub repository.

jan-johansson-mr commented 1 month ago

Thanks @MackinnonBuck,

I'll se what I can do, to make a stripped down version with only essentials.

Best regards!

MackinnonBuck commented 1 month ago

@jan-johansson-mr, note that UseAuthentication() should be called before UseAuthorization() and after UseForwardedHeaders(). Just throwing this out there in case it fixes the problem.

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

Hi @jan-johansson-mr. 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.

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

This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 4 days. It will be closed if no further activity occurs within 3 days of this comment. If it is closed, feel free to comment when you are able to provide the additional information and we will re-investigate.

See our Issue Management Policies for more information.