auth0 / auth0-spa-js

Auth0 authentication for Single Page Applications (SPA) with PKCE

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Correct error handling in handleRedirectCallback

zdicesare opened this issue · comments

Describe the problem

Hi,

I doubt this is an actual issue and rather an understanding gap, but I'm trying to make sure I understand the correct error handling to implement when using auth0-spa-js, specifically around handleRedirectCallback.

This function verifies that we have a transaction started and implements an anti-CSRF check.

Let's say we have the user authenticate successfully via loginWithRedirect, they are redirected back to the application callback at /login/callback, but the handleRedirectCallback call fails for any reason. This will throw an error, but a subsequent call to getTokenSilently will still succeed because the user did successfully authenticate. So, even if we presented the user with an error, if they simply navigate away from the callback URL, they will now be authenticated, because getTokenSilently will be able to obtain a token.

That is, a failure in handleRedirectCallback does not seem to block successful authentication unless the user authorization w/ Auth0 has itself failed. In that case, what exactly is blocked by handleRedirectCallback alone? Do we need to implement a call to logout within a catch block around handleRedirectCallback as a safeguard?

Fundamentally, I am not sure what additional value handleRedirectCallback itself provides if any errors can be avoided by navigating away from the callback URL and reloading the SPA.

What was the expected behavior?

An error thrown from handleRedirectCallback should not result in an authenticated session

Reproduction

This case happens when following the examples in this README or using a quickstart

Can the behavior be reproduced using the SPA SDK Playground?

This is more of a theoretical concern as I have not forced handleRedirectCallback itself to error

Environment

  • Version of auth0-spa-js used: 2.0.1
  • Which browsers have you tested in? Chrome
  • Which framework are you using, if applicable (Angular, React, etc): React
  • Other modules/plugins/libraries that might be involved:

Thanks for reaching out.

As you noticed, what we check in handleRedirectCallback is:

  • Do we have a transaction?
  • Does the transaction have a code_verifier
  • Does the transaction's state matches the returned state?

When u call getTokenSilently in the situation where handleRedirectCallback failed (or well also when u call it when handleRedirectCallback did succeed), we will be using a hidden iframe to do the exact same requests, but non-interactively.

The consequence of this is that there is no transaction, as we are not leaving the page.

Therefore we do not verify the code_verifier, but we do still verify the state: https://github.com/auth0/auth0-spa-js/blob/master/src/Auth0Client.ts#L900-L902

Meaning if the handleRedirectCallback failed because of a state mismatch, it's highly likely that getTokenSilently will fail for the same reason. If it doesn't, it does mean the silent request was fine and okay for the SDK to handle.

This can be seen as a feature, or as some arguably unexpected behavior. However, you can decide for yourself how you can handle this.

  • decide to call logout when handleRedirectCallback throws (this will log the user out from Auth0, and ensure getTokenSilently will fail as well)
  • decide to not do anything and allow for getTokenSilently to eventually try and rectify the authentication state.

I think alot depends on your situation.

Not sure this is helpful, but happy to continue the conversation as needed!

Hi,

Thank you, this is helpful.

If I understand correctly, the state that is verified within getTokenSilently is a new state param that is passed on the request and compared on the way back- it doesn't have a connection to the initial state value used in handleRedirectCallback.

Then, is it possible that we could have had a state mismatch in handleRedirectCallback and not in getTokenSilently?

And my larger question: if it's okay for handleRedirectCallback to fail because we can rely on getTokenSilently either completing or erroring, then what value is handleRedirectCallback providing by itself? Calling logout seems like a heavy handed solution (and at odds with all uses of the SDK I've seen in examples so far), but otherwise I'm still not quite seeing how to properly protect against errors here.

Sorry for the delay, this got a bit of my radar.

is it possible that we could have had a state mismatch in handleRedirectCallback and not in getTokenSilently?

I guess that's possible, and that would mean someone tried to mess with the first one, but not the second one. So the second one should be fine to handle, while the first one isn't.

if it's okay for handleRedirectCallback to fail because we can rely on getTokenSilently either completing or erroring, then what value is handleRedirectCallback providing by itself?

All handleRedirectCallback does is call oauth/token. getTokenSilently does more that is not neccesary when you are in the process of doing an interactive login (calling loginWithRedirect + handleRedirectCallback).

E.g. when u ignore refresh tokens, using OAuth with Auth Code flow involves two calls:

  • call /authorize
  • call /oauth/token

Calling loginWithRedirect calls /authorize, calling handleRedirectCallback calls /oauth/token, while calling getTokenSilently calls both /authorize and /oauth/token.

but otherwise I'm still not quite seeing how to properly protect against errors here.

Can you elaborate what errors you are seeing here that you are unsure how to handle? I am not sure I fully understand.

I see thanks- that is helpful again, and appreciate the time providing some education to me on this issue.

Is it fair to say then that the only "value add" of handleRedirectCallback is to get a token and populate the cache after we know the user has already been directed to /authorize?

That is, if an SPA did not implement handleRedirectCallback at all but simply called loginWithRedirect and then getTokenSilently- that there would be no security vulnerability, and we'd simply make some redundant calls to Auth0?

That's not something I'm suggesting doing, of course- but previously I thought we needed to call handleRedirectCallback for security purposes to validate the login and redirect. That led to confusion on my part about how to prevent further calls to getTokenSilently if handleRedirectCallback did happen to have an error.

But, if these calls are all actually independent , and getTokenSilently implements these checks on each call, and handleRedirectCallback is simply an "optimization" on the other side of a login call- then I think I'm all straightened out :)

That's correct. Calling getTokenSilently, without first calling handleRedirectCallback isn't very different from calling login again, followed by handleRedirectCallback.

Long story short, handleRedirectCallback is for handeling coming back from auth0 after doing An interactive login (calling loginWithRedirect and actualy going to auth0).

getTokenSilently, if we ignore refresh tokens, does the same thing but non interactively, meaning it relies on previously having called login (but not handleRedirectCallback).

Hope it makes some sense!