aspnet-contrib / AspNet.Security.OAuth.Providers

OAuth 2.0 social authentication providers for ASP.NET Core
Apache License 2.0
2.37k stars 535 forks source link

Apple Sign In - 500 Status Code "The oauth state was missing or invalid." #431

Closed ncosentino closed 4 years ago

ncosentino commented 4 years ago

Describe the bug I've been struggling to get Apple sign in to work for far too long now, but this nuget package is the closest I feel like I've been able to get. After being presented the login dialog from Apple and successfully signing in, the redirect results in a 500 error. When I try it in postman using the same "state" form data, I'm able to see a 500 with the following data:

System.Exception: An error was encountered while handling the remote login.
 ---> System.Exception: The oauth state was missing or invalid.
   --- End of inner exception stack trace ---
   at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at MyAppServer.Startup.<>c.<<Configure>b__8_5>d.MoveNext() in C:\MyApp\Startup.cs:line 162
--- End of stack trace from previous location where exception was thrown ---
   at NWebsec.AspNetCore.Middleware.Middleware.CspMiddleware.Invoke(HttpContext context)
   at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)
   at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)
   at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)
   at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)
   at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)
   at AspNetCoreRateLimit.RateLimitMiddleware`1.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

HEADERS
=======
Cache-Control: no-cache
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Accept: */*
Accept-Encoding: gzip, deflate, br
Host: localhost:5001
User-Agent: PostmanRuntime/7.25.0
Content-Length: 225
Postman-Token: 428870ae-6cde-4101-acbc-fb25376702c5

I can't debug the flow properly locally because the redirect URLs for apple don't seem to allow localhost for trial/error... So I'm not exactly sure if what the hosted server is experiencing is the same. This is my best guess though.

From what I can tell, this can only happen here. To me suggests either the state isn't set (which I'm pretty confident it is) or the Unprotect() call seems to return null properties.

Steps To reproduce

Expected behaviour I can finally get an Apple OAuth token and move on from this living nightmare :)

Actual behaviour 500 error response

System information:

martincostello commented 4 years ago

Just to rule out config vs. our code vs your code, if you use the same OAuth configuration and key for your current code with this sample site instead does it work? https://github.com/martincostello/SignInWithAppleSample


From: Nick Cosentino notifications@github.com Sent: Friday, June 5, 2020 8:29:10 PM To: aspnet-contrib/AspNet.Security.OAuth.Providers AspNet.Security.OAuth.Providers@noreply.github.com Cc: Subscribed subscribed@noreply.github.com Subject: [aspnet-contrib/AspNet.Security.OAuth.Providers] Apple Sign In - 500 Status Code "The oauth state was missing or invalid." (#431)

Describe the bug I've been struggling to get Apple sign in to work for far too long now, but this nuget package is the closest I feel like I've been able to get. After being presented the login dialog from Apple and successfully signing in, the redirect results in a 500 error. When I try it in postman using the same "state" form data, I'm able to see a 500 with the following data:

System.Exception: An error was encountered while handling the remote login. ---> System.Exception: The oauth state was missing or invalid. --- End of inner exception stack trace --- at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler1.HandleRequestAsync() at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at MyAppServer.Startup.<>c.<<Configure>b__8_5>d.MoveNext() in C:\MyApp\Startup.cs:line 162 --- End of stack trace from previous location where exception was thrown --- at NWebsec.AspNetCore.Middleware.Middleware.CspMiddleware.Invoke(HttpContext context) at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context) at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context) at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context) at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context) at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context) at AspNetCoreRateLimit.RateLimitMiddleware1.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

HEADERS

Cache-Control: no-cache Connection: keep-alive Content-Type: application/x-www-form-urlencoded Accept: / Accept-Encoding: gzip, deflate, br Host: localhost:5001 User-Agent: PostmanRuntime/7.25.0 Content-Length: 225 Postman-Token: 428870ae-6cde-4101-acbc-fb25376702c5

I can't debug the flow properly locally because the redirect URLs for apple don't seem to allow localhost for trial/error... So I'm not exactly sure if what the hosted server is experiencing is the same. This is my best guess though.

From what I can tell, this can only happen herehttps://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers/blob/f7d418983c223afadc5a27794a0bdbbb911e40c5/src/AspNet.Security.OAuth.Apple/AppleAuthenticationHandler.cs#L218. To me suggests either the state isn't set (which I'm pretty confident it is) or the Unprotect() call seems to return null properties.

Steps To reproduce

Expected behaviour I can finally get an Apple OAuth token and move on from this living nightmare :)

Actual behaviour 500 error response

System information:

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHubhttps://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers/issues/431, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AAK7M3NGSSK3Q33DEPETJJDRVFBQNANCNFSM4NVAJTWQ.

ncosentino commented 4 years ago

@martincostello that's a great idea!

Just tried, and I can't even get as far because the redirect_uri issue. image

I think from my understanding this is because Apple won't allow redirect URI's with local host in them. So I'm not sure if I'm being super dense when trying to debug this stuff locally and I'm missing some secret to making that part work haha My apologies if that's the case!

The URL I have looks like: https://appleid.apple.com/auth/authorize?client_id=MY_SERVICE_ID&scope=name%20email&response_type=code&redirect_uri=https%3A%2F%2Flocalhost%3A44399%2Fsignin-apple&state=CfDJ8NG4BKNSfUxNtvh7P_N_wfokO7gS0rWSnQw7JuMM4bhuLVDC0CAze-X4A3o5gpOrwkdb1hyf2AtB1cylwt1F10R1ZLk8m4F2AU9VWobhJKzsltkXVwcM-LPgIRqe0Eq-oz80TG6ECVs4iFyBaaW6LaNhlmkQLH0SPVxk0RvsqxffbnWq10At9NtJIjp2HcnZwutUsq8TAwceU_p_OlehHI0&response_mode=form_post

I'm happy to try out any variations of things, but I only have my production site to push stuff to if I need to have it work with a proper domain name. Let me know! Thanks for the quick response!

martincostello commented 4 years ago

To test it out in the past I’ve been using an Azure App Service site with Visual Studio pushes to experiment when I’ve been trying to debug things. It avoids the localhost issue while being fairly rapid turnaround.

Alternatively try turning the logging on your own site up to Trace for the provider and see if it logs any addition detail about the 500.

ncosentino commented 4 years ago

Azure isn't an option for me unfortunately. Annnd I've been busy hacking up my logging so I had to convert some stuff to get my built-in logger to go to AWS !BUT! we're making progress now!

I used this on my original code again just because as mentioned I don't have another hosting site. I think it's revealed the problem but I'm not sure I understand how to fix it. Here's my log entries (I'll try to sanitize but sorry I couldn't make it format nicely no matter how hard I tried):

{ "TimestampUtc": "2020-06-05T21:14:39.7346838Z", "Level": "INFO", "Message": "AuthenticationScheme: Apple was challenged." } { "TimestampUtc": "2020-06-05T21:15:55.4157103Z", "Level": "DEBUG", "Message": "Generating new client secret for subject MY_SERVICE_ID that will expire at 12/05/2020 11:45:55." } { "TimestampUtc": "2020-06-05T21:15:55.4873141Z", "Level": "DEBUG", "Message": "Generated new client secret with value SUPER_DUPER_SECRET." } { "TimestampUtc": "2020-06-05T21:15:56.3269127Z", "Level": "INFO", "Message": "Creating ticket for Sign in with Apple." } { "TimestampUtc": "2020-06-05T21:15:56.3450292Z", "Level": "DEBUG", "Message": "Access Token: OMG_IT_MADE_AN_ACCESS_TOKEN" } { "TimestampUtc": "2020-06-05T21:15:56.3617245Z", "Level": "DEBUG", "Message": "Refresh Token: OMG_IT_MADE_A_REFRESH_TOKEN" } { "TimestampUtc": "2020-06-05T21:15:56.3784402Z", "Level": "DEBUG", "Message": "Token Type: Bearer" } { "TimestampUtc": "2020-06-05T21:15:56.4030842Z", "Level": "DEBUG", "Message": "Expires In: 3600" } { "TimestampUtc": "2020-06-05T21:15:56.4204568Z", "Level": "DEBUG", "Message": "Response: System.Text.Json.JsonDocument" } { "TimestampUtc": "2020-06-05T21:15:56.4381322Z", "Level": "DEBUG", "Message": "ID Token: A_VALID_ID_TOKEN" } { "TimestampUtc": "2020-06-05T21:15:56.4609573Z", "Level": "INFO", "Message": "Loading Apple public keys from https://appleid.apple.com/auth/keys." } { "TimestampUtc": "2020-06-05T21:15:56.5825845Z", "Level": "ERROR", "Message": "Connection id \"0HM09ILFAIUHS\", Request id \"0HM09ILFAIUHS:00000001\": An unhandled exception was thrown by the application.", "Exception": { "ClassName": "System.InvalidOperationException", "Message": "The authentication handler registered for scheme 'Bearer' is 'JwtBearerHandler' which cannot be used for SignInAsync. Did you forget to call AddAuthentication().AddCookies(\"Cookies\") and SignInAsync(\"Cookies\",...)?", "Data": null, "InnerException": null, "HelpURL": null, "StackTraceString": " at Microsoft.AspNetCore.Authentication.AuthenticationService.SignInAsync(HttpContext context, String scheme, ClaimsPrincipal principal, AuthenticationProperties properties)\n at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler1.HandleRequestAsync()\n at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)\n at MyApp.Startup.<>c.<<Configure>b__8_6>d.MoveNext() in /src/MyApp/Startup.cs:line 165\n--- End of stack trace from previous location where exception was thrown ---\n at NWebsec.AspNetCore.Middleware.Middleware.CspMiddleware.Invoke(HttpContext context)\n at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)\n at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)\n at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)\n at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)\n at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)\n at AspNetCoreRateLimit.RateLimitMiddleware1.Invoke(HttpContext context)\n at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication1 application)", "RemoteStackTraceString": null, "RemoteStackIndex": 0, "ExceptionMethod": null, "HResult": -2146233079, "Source": "Microsoft.AspNetCore.Authentication.Core", "WatsonBuckets": null } }

So I use another authenticator for firebase that I have configured: .AddJwtBearer(options => { options.Authority = $"https://securetoken.google.com/{firebaseProjectId}"; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = $"https://securetoken.google.com/{firebaseProjectId}", ValidateAudience = true, ValidAudience = firebaseProjectId, ValidateLifetime = true }; })

And I wonder if these are somehow conflicting? I need the firebase one there... And because I can't make apple Auth work via firebase directly, I was adding this as a mechanism to generate tokens for apple sign in.

Not sure if you're able to make any suggestions. It's kind of what I expected though (not a bug with what you wrote but a problem with how I have things set up).

martincostello commented 4 years ago

I can’t remember how to change it off the top of my head (on my phone atm), but I think you just need to configure Apple to use a different sign in/cookie scheme than firebase’s.

ncosentino commented 4 years ago

Indeed it looks like switching this to:

services .AddAuthentication(options => options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = "/apple-signin"; options.LogoutPath = "/apple-signout"; }) .AddApple(options => { // do the setup here });

Seems to have avoided the problem. But now two follow up issues:

martincostello commented 4 years ago

If I recall correctly if you set SaveTokens = true on the options they’ll be included on the claims. Otherwise you’ll need to set a handler onto one of the Events events to capture the tokens and do something with.

ncosentino commented 4 years ago

I figured I'd update this for anyone else curious but I don't think any of this is a bug just... Complexity with getting set up.

Then my controller looks like this:

        [AllowAnonymous]
        [HttpGet("SignIn")]
        public IActionResult SignIn()
        {
            // Instruct the middleware corresponding to the requested external identity
            // provider to redirect the user agent to its own authorization endpoint.
            // Note: the authenticationScheme parameter must match the value configured in Startup.cs
            return Challenge(
                new AuthenticationProperties { RedirectUri = "/Apple/InterceptCallback" },
                AppleAuthenticationDefaults.AuthenticationScheme);
        }

Where the above method is where my client will hit. What I didn't realize is THAT redirect URI above is actually where when EVERYTHING is done where you will navigate your client.

So I have a thing that intercepts it... Because I need to steal the cookie information. This is where I feel like I'm just absolutely hacking things up but... it got me my access token that I wanted.

[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
        [HttpGet("InterceptCallback")]
        public IActionResult InterceptCallback()
        {
            AuthenticationTicket authenticationTicket;
            try
            {
                authenticationTicket = Decrypt(HttpContext, Request.Cookies.First().Value);
            }
            catch (Exception ex)
            {
                _logger.Debug("Exception caught doing standard decrypt.", ex);
                return BadRequest("Could not decrypt cookie.");
            }

            var items = authenticationTicket.Properties.Items;
            // NOTE: could also just return this big ol' object in the body and leverage it?
            var authProps = new AuthProps()
            {
                AuthScheme = items[".AuthScheme"],
                AccessToken = items[".Token.access_token"],
                RefreshToken = items[".Token.refresh_token"],
                TokenType = items[".Token.token_type"],
                TokenExpiresAt = items[".Token.expires_at"],
                TokenNames = items[".TokenNames"],
                DateIssued = items[".issued"],
                ExpiryDate = items[".expires"],
            };

            var redirectQueryBuilder = new QueryBuilder();
            redirectQueryBuilder.Add("access_token", authProps.AccessToken);
            redirectQueryBuilder.Add("refresh_token", authProps.RefreshToken);

            var redirectUriBuilder = new UriBuilder("https://mydomain/apple/authenticated");
            redirectUriBuilder.Query = redirectQueryBuilder.ToString();
            var redirectUrl = redirectUriBuilder.ToString();
            return new RedirectResult(redirectUrl);
        }

And I have some borrowed code to do the decryption:

 public AuthenticationTicket Decrypt(HttpContext context, string cookie)
        {
            // unfortunately here's where we access that static reference to get access to "TicketDataFormat"
            AuthenticationTicket ticket = BigScaryCookieMonster.Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding(context));
            return ticket;
        }

        private string GetTlsTokenBinding(HttpContext context)
        {
            var binding = context.Features.Get<ITlsTokenBindingFeature>()?.GetProvidedTokenBindingId();
            return binding == null ? null : Convert.ToBase64String(binding);
        }

And my final route the caller gets sent to looks like:

        [HttpGet("Authenticated")]
        public IActionResult Authenticated(
            [FromQuery(Name = "access_token")]string accessToken,
            [FromQuery(Name = "refresh_token")]string refreshToken)
        {
            return Ok(new
            {
                access_token = accessToken,
                refresh_token = refreshToken,
            });
        }

I haven't verified with my actual coded client if this is going to suffice, but the end result is that I get a URL that has both the access_token and refresh_token on it. I've been going borderline insane for days now trying to find some example of this online, so I figured I'd put the whole thing here for the next poor soul to save them some time.

Thanks for all your assistance with debugging!