Skip to content

Remote authentication not working as expected #228

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
br3nt opened this issue Oct 12, 2022 · 13 comments · Fixed by #229
Closed

Remote authentication not working as expected #228

br3nt opened this issue Oct 12, 2022 · 13 comments · Fixed by #229
Assignees

Comments

@br3nt
Copy link
Contributor

br3nt commented Oct 12, 2022

Describe the bug

I have followed the incremental migration blog post series.

The setup correctly proxies to pages from the ASP.NET MVC application. I am able to login and navigate the existing site as an authenticated user. I can also see remote session variables are being requested successfully and accessible in ASP.NET Core MVC controller actions.

When I protect an endpoint in the ASP.NET Core MVC app with [Authorize] such as , UserInfoController.Index , I can see that a request to the endpoint results in a request to the remote authentication endpoint, http://localhost:52187/systemweb-adapters/authenticate?original-url=%2FUserInfo, as expected. However, the authentication fails and I am redirected to the ASP.NET MVC login page.

By adding a breakpoint in AccountController.Login, I can verify that HttpContext.User.Identity.IsAuthenticated is true as I expect because I haven't logged out.

To debug the what was happening in /systemweb-adapters/authenticate, I had to copy several internal classes from Microsoft.AspNetCore.SystemWebAdapters, and configure them manually.

I found that context.User.Identity.IsAuthenticated in RemoteAppAuthenticationHttpHandler.ProcessRequest() is false.
This results in a 401 response, hence why I am being redirected back to the login page.
But then, in AccountController.Login the user is authenticated.

I don't know how to debug further than this. I don't understand why context.User.Identity.IsAuthenticated in RemoteAppAuthenticationHttpHandler.ProcessRequest() is false and HttpContext.User.Identity.IsAuthenticated in AccountController.Login is true. According to the blog post, the request should be authenticated and the user redirected back to the page from ASP.NET Core MVC app.

To Reproduce

I am unsure how to reproduce this.

I have simply followed the examples in the incremental migration blog post series.

Exceptions (if any)

None.

Further technical details

Please include the following if applicable:

ASP.NET Framework Application:

  • Technologies and versions used (i.e. MVC/WebForms/etc): MVC, FormsAuthentication
  • .NET Framework Version: 4.8

ASP.NET Core Application:

  • Targeted .NET version: 6.0
  • .NET SDK version: As per migration
@zsharp-gls
Copy link

Hi! I just ran into this as well. I don't have a fix, but I think I'm one step nearer to a solution.

I noticed that when I hit the RemoteAppAuthenticationHttpHandler.ProcessRequest(), there's only one cookie: __RequestVerificationToken. But then when I'm in AccountController.Login(), there are 3 cookies: __RequestVerificationToken, along with ASP.NET_SessionId and MYAPPAUTH (which comes from the <authentication> node in my web.config). I suspect those two missing cookies are causing me trouble.

@br3nt, can you confirm if you're seeing that as well?

@zsharp-gls
Copy link

zsharp-gls commented Oct 12, 2022

Frustratingly, when I try to inspect the traffic with Telerik Fiddler, the issue disappears. But running without Fiddler, I only get the one cookie. When Fiddler is open and I'm getting all 3 cookies, login works correctly.

@br3nt
Copy link
Contributor Author

br3nt commented Oct 12, 2022

Hi @zsharp-gls. Nice to have some company :)

I also have a one cookie when RemoteAppAuthenticationHttpHandler.ProcessRequest() but it has a different name, _ga. I'm not sure how a google analytics cookie gets to hang around if that's what it is.

A normal authenticated request has lots of cookies set:

  • .AspNetCore.Antiforgery.4_99IRX4FZI
  • .AspNetCore.Antiforgery.kAf4-Q45_vg
  • .AspNetCore.Antiforgery.qTjQ5s7KoFw
  • .AspNetCore.Antiforgery.QWwkyZwc8hM
  • .AspNetCore.Cookies
  • .AspNetCore.CookiesC1
  • .AspNetCore.CookiesC2
  • .AspNetCore.Session
  • .ASPXAUTH
  • __RequestVerificationToken
  • _ga
  • _gat
  • _gid
  • ASP.NET_SessionId
  • idsrv
  • idsrv.session

The <authentication> node in Web.config in my app looks like this:

<authentication mode="Forms">
  <forms loginUrl="~/Account/Login" timeout="2880" />
</authentication>

I'll see if I can inspect the traffic too.

@br3nt
Copy link
Contributor Author

br3nt commented Oct 13, 2022

I have used the "Open Browser" button in Telerik Fiddler to inspect the traffic.

When using the Telerik browser there is no difference in the cookies when navigating to a page from the .NET app vs from /UserInfo in the .NET Core app:

  • .ASPXAUTH
  • __RequestVerificationToken
  • _ga
  • _gat
  • _gid
  • ASP.NET_SessionId

In RemoteAppAuthenticationHttpHandler.ProcessRequest(), context.User.Identity.IsAuthenticated returns true as I would expect normally.

However, the UserInfoController.Index() never gets hit. A blank page is displayed instead.

I don't know what to debug from here.

I'm curious why Fiddler produces a different response than when using the default browser.
I'm curious why UserInfoController.Index() never gets hit after successful authentication from RemoteAppAuthenticationHttpHandler.ProcessRequest().
I assume the reduced number of cookies is because different parts of my application create cookies necessary for their operation.

@br3nt
Copy link
Contributor Author

br3nt commented Oct 13, 2022

So, I've started debugging the .Net Core side of things.

I had similar troubles debugging because all the classes and interfaces are internal... I had to make local copies and manually configure them into the DI container.

The response returned to RemoteAppAuthenticationService.AuthenticateAsync() from /systemweb-adapters/authenticate has the following:

  • StatusCode - 302 Found
  • Location - /Account/Login?ReturnUrl=%2fsystemweb-adapters%2fauthenticate%3foriginal-url%3d%252FUserInfo&original-url=%2FUserInfo

This is an unexpected response as RemoteAppAuthenticationHttpHandler in the .Net app sets the StatusCode to 401 because context.User.Identity.IsAuthenticated is false. So something else must be intersecting the request after that. This is a bit of a red herring. The .Net Core side of things is just redirecting to the Location returned as a response from /systemweb-adapters/authenticate.

It still doesn't explain why the request to /systemweb-adapters/authenticate don't have access to the expected cookies that will enable it to authenticate the user.

Again, I'm now a little lost as to what I should try to debug next.

Are cookies meant to be added to the authRequest instance in RemoteAppAuthenticationService.AuthenticateAsync()?
I can't see any code that does that.

There's are details of the cookies stored in the browser:
image

@zsharp-gls

This comment was marked as off-topic.

@zsharp-gls
Copy link

Are cookies meant to be added to the authRequest instance in RemoteAppAuthenticationService.AuthenticateAsync()?
I can't see any code that does that.

Yes, cookies are one of the headers listed in the _options.RequestHeadersToForward list in the RemoteAppAuthenticationService.

I looked closer at the cookie header that the .NET Framework app is receiving (in RemoteAppAuthenticationHttpHandler.ProcessRequest()).

When it's not working, context.Request.Cookies has one entry, with key "__RequestVerificationToken" and value "A8LL0oSrx4T3iDzkhIFkLbfgjMDnrDsnG6US7CiDvPlAd28sx-scg9ZhbFjE8wtd041KhY9tXtHdkYIL2qDnHOmZ0d__OykxQVCgG0wK5RHj6puBiz2rxvYwMJToIDmSK4nXN-kG_s61dg2, MYAPPAUTH=1592A28EF08ACBFD787FD02405C99AA254DC64D10FA93C9F0087FC91A11AE19E10422BEE3A9E01677583681429926D8BF9FE1F1EE581309717A6E60B64E1EB36EB691979A922D0FF36E22DBF0E93137ECAA6C151C878D3BD4AD45728A269A1C7A5721191AFCB9653E7F98EA821AFF92AFB495A47E85821C982, ASP.NET_SessionId=3hpepecup4gk5cs2gylcvj2j"

The cookies are being forwarded from the ASP.NET Core app to the /systemweb-adapters/authenticate endpoint comma-separated, when they should be semicolon-separated, and that causes issues. I was able to confirm with Wireshark that when it's working (through Fiddler,) the cookies are semicolon separated.

This seems to me to be the core issue, but I'm not sure how to confirm that or how to fix it.

@adityamandaleeka
Copy link
Member

@br3nt @zsharp-gls Thanks for reporting this and doing some debugging! We'll take a look.

@br3nt
Copy link
Contributor Author

br3nt commented Oct 14, 2022

@adityamandaleeka @mjrousos have you had any success?

I have continued trying to look into this issue.

Thank you @zsharp-gls for identifying the Cookie delimiter issue.

The problem of the Cookie header format was also logged here: dotnet/aspnetcore#22351
That issue was closed because it was stale rather than actually being resolved.

According to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie and https://httpwg.org/specs/rfc6265.html#cookie, the correct Cookie header format is to delimit the cookie values with %x3B and %x20 (; ).

I can confirm that when I manually set the Cookie header to use ; delimiters, the ASP.NET app is able to access all the required cookies, context.User.Identity.IsAuthenticated is true, and the ClaimsPrincial from context.User or context.User.Identity is serialised and written to the response, which is then returned to the ASP.NET Core app.

I was able to get this to work with the addition of AddCookieHeaders() added to RemoteAppAuthenticationService .AddHeaders():

internal partial class RemoteAppAuthenticationService : IRemoteAppAuthenticationService
{
    // Add configured headers to the request, or all headers if none in particular are specified
    private static void AddHeaders(IEnumerable<string> headersToForward, HttpRequest originalRequest, HttpRequestMessage authRequest)
    {
        // Add x-forwarded headers so that the authenticate API will know which host the HTTP request was addressed to originally.
        // These headers are also used by result processors - to fix-up redirect responses, for example, to redirect back to the
        // correct host.
        authRequest.Headers.Add(AuthenticationConstants.ForwardedHostHeaderName, originalRequest.Host.Value);
        authRequest.Headers.Add(AuthenticationConstants.ForwardedProtoHeaderName, originalRequest.Scheme);

        // The migration authentication request header indicates that the request is from the ASP.NET Core app
        // with the intention of authenticating the user. Without this header, the request will be interpreted
        // as a callback after authenticating with an identity provider.
        authRequest.Headers.Add(AuthenticationConstants.MigrationAuthenticateRequestHeaderName, "true");

        IEnumerable<string> headerNames = originalRequest.Headers.Keys;
        if (headersToForward.Any())
        {
            headerNames = headerNames.Where(headersToForward.Contains);
        }

        foreach (var headerName in headerNames)
        {
            switch (headerName)
            {
                case "Cookie":
                    AddCookieHeaders(originalRequest, authRequest);
                    break;
                default:
                    authRequest.Headers.Add(headerName, originalRequest.Headers[headerName].ToArray());
                    break;
            }
        }
    }

    private static void AddCookieHeaders(HttpRequest originalRequest, HttpRequestMessage authRequest)
    {
        var cookies = originalRequest.Cookies.ToArray();
        var cookieValues = cookies.Select(c => $"{c.Key}={c.Value}").ToArray();
        var cookieHeaderValue = string.Join("; ", cookieValues);
        if (cookies.Any()) authRequest.Headers.Add("Cookie", cookieHeaderValue);
    }
}

It seems like a workaround will be required in this library, until System.Net.Http.Headers.HttpHeaders.Add() can be updated to add Cookie values in the expected way. I assume there is a Parser for the HeaderDescriptor for Cookie that can be updated. But I imagine that is outside the scope of this project. Hence why a work around is required for the interim.

Unfortunately, there may be additional issues past this point. Event though the request is authenticated and the ASP.NET Core app receives the ClaimsPrincipal, the browser receives a completely blank page. There is not HTML in the response to the browser.

@dazinator
Copy link

dazinator commented Oct 14, 2022

Just chucking an alternative approach out here: perhaps you could allow your asp.net core app to understand the forms authentication cookie natively and login / restore the claims principal from it, without having to do remote proxy auth. You can do that with this library I made: https://github.com/dazinator/AspNetCore.LegacyAuthCookieCompat

@mjrousos
Copy link
Member

mjrousos commented Oct 14, 2022

@br3nt, I think you're right that the issue is the cookie delimiter. It seems that HttpClient is incorreclty delimiting with , instead of ;. There was actually a similar workaround needed in our YARP code.

That same code change is likely to address this issue. I'll test it out and get a PR opened.

@Tratcher
Copy link
Member

Note we tracked this down to an HTTP/2 bug in Kestrel and fixed it in .NET 7. We'll add a workaround here for prior versions.

mjrousos added a commit that referenced this issue Oct 14, 2022
In cases of multi-headers, HttpClient concatenates the headers with , . In the case of multi-header cookie headers, though (which can happen because of dotnet/aspnetcore#26461), RFC 6265 specifies that multiple cookies must be concatenated with ; when writing the Cookie header.

HttpClient doesn't special case the Cookie header as it should, so this updates the RemoteAppAuthenticationService to special case cookie headers and concatenate them itself, if needed.

Fixes #228
@dazinator
Copy link

dazinator commented Oct 26, 2022

For anyone looking for alternative options, I believe @br3nt has got this working now using native forms authentication for .net core rather than remote proxy auth - based on his updates here (of course which I am quite proud, because re-implementing legacy forms auth with all of it's varying modes is not trivial):

There are also benefits to doing this natively - mainly:-

  • eliminate the remote call for authentication
  • optionally, you could also choose to migrate your login page from the legacy webforms app, over into the .net core app (so you produce the sign in cookie from the .net core app instead), whilst not breaking any existing authentication cookies. Then later on down the line once your app is fully .net core, you could chose a time to ditch legacy forms auth as the format, for something better / more modern if you wanted to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants