auth0 / auth0-react

Auth0 SDK for React Single Page Applications (SPA)
MIT License
891 stars 259 forks source link

intermittent "Invalid state" missing_transaction error from loginWithRedirect() #785

Open yellowtailfan opened 4 months ago

yellowtailfan commented 4 months ago

Checklist

Description

Our production app using auth0-react intermittently produces an "Invalid state" error on our callback page, with error missing_transaction.

Reproduction

The error is intermittent. I have made many attempts to reproduce it but so far none succeeded.

It happens mostly to myself, several times a day if I am actively using the app on multiple tabs in production on Chrome. It rarely happens for our users. It rarely or never happens on our development server or in local testing. It doesn't seem to affect our other developers using Firefox.

It tends to happen when I have multiple tabs open with our app running, and I switch back to one of those tabs.

Additional context

Our app had 3 authentication triggers:

  1. withAuthenticationRequired wrapping multiple page components
  2. loginWithRedirect() on our index page (not generally open in normal use, as our home screen is on /home)
  3. getAccessTokenSilently() before each call to our own API

We are using Nextjs with static export. We are using auth0-react because that was available when we started writing the app several years ago. We are using refresh tokens and the useCookiesForTransactions is unset which I believe means loginWithRedirect() is using single-tab session storage.

My current guesses as to the cause:

  1. Chrome is loading the page multiple times from within a single tab triggering multiple overlapping calls to loginWithRedirect() via withAuthenticationRequired
  2. Chrome possibly discards tabs when I switch away from them, then restarts them in some odd way that interferes with the session storage (although I tried manually discarding tabs and rapidly switching tabs but could not reproduce the error)
  3. Chrome deletes the session storage at an inconvenient time (I have seen some reports that Chrome sometimes deletes session storage after a short timeout)

I considered whether loginWithRedirect() on our index page was causing a race condition, but I can't see how that would happen as the index page is never visited after login during normal use.

I have made some changes and will keep an eye out to see if the bug has gone:

  1. Removed loginWithRedirect() from our index page (just rely on withAuthenticationRequired that is on our other pages instead)
  2. Added logging via the withAuthenticationRequired option onBeforeAuthentication to console and Sentry
  3. Added a check on isAuthenticated before allowing calls to getAccessTokenSilently()

I have also had a detailed discussion with one of your support team on a support ticket.

auth0-react version

2.2.4

React version

17.0.2

Which browsers have you tested in?

Chrome

yellowtailfan commented 4 months ago

One suggestion I have seen in your support forms and your GitHub issues is to recover gracefully when receiving an "Invalid state" error by retrying. For example calling getAccessTokenSilently() with a local storage counter to avoid infinite loops.

I considered trying that, but because I can't reproduce the bug it's hard for me to test the fix. I suppose I could modify my local copy of auth0-react to randomly create the error then check my handling of it that way. But I haven't tried that yet.

It would be great if there was a working and tested example of this kind of retry mechanism. Even better of course would be if the auth0-react library could be made more resilient to transient failures in session storage or overlapping calls to loginWithRedirect().

matija2209 commented 1 month ago

I get the identical error. I literaly followed the SDK React tutorial with React Vite and I cannot handle redirect correctly.

yellowtailfan commented 1 month ago

The good news is I was able to fix the problem in our app. We haven't seen an invalid state error for several months.

There were three main changes:

  1. Fixed our Auth0Provider including switching on useRefreshTokensFallback
  2. Removed all direct loginWithRedirect() calls except the login button
  3. Only call getAccessTokenSilently() when isAuthenticated is true

Here's the before and after on the provider options (some of this before case was a mistake I made when upgrading to version 2 of the auth0-react SDK):

Before:

      <Auth0Provider
        domain={process.env.NEXT_PUBLIC_AUTH0_DOMAIN}
        clientId={process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID}
        onRedirectCallback={onRedirectCallback}
        authorizationParams={ {
          redirect_uri: process.env.NEXT_PUBLIC_AUTH0_REDIRECT_URI,
          audience: process.env.NEXT_PUBLIC_AUTH0_AUDIENCE,
          scope: 'profile email write:data',
          cacheLocation: 'localstorage',
          useRefreshTokens: true,
          useRefreshTokensFallback: true,
        } }
      >

After:

      <Auth0Provider
        domain={process.env.NEXT_PUBLIC_AUTH0_DOMAIN}
        clientId={process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID}
        onRedirectCallback={onRedirectCallback}
        cacheLocation='localstorage'
        useRefreshTokens={true}
        useRefreshTokensFallback={true}
        authorizationParams={{
          audience: process.env.NEXT_PUBLIC_AUTH0_AUDIENCE,
          redirect_uri: process.env.NEXT_PUBLIC_AUTH0_REDIRECT_URI,
          scope: 'profile email write:data',
        }}
      >

I also discovered that overlapping calls to the Auth0 authorize endpoint can cause a state mismatch error which gets displayed to the user as "Invalid state".

There are 3 ways that our code could trigger a call to authorize:

  1. loginWithRedirect() from index.js
  2. Auth0Provider initialisation when the app starts up, which in turn calls getAccessTokenSilently()
  3. getAccessTokenSilently() when calling our API

getAccessTokenSilently() has a lock to avoid overlapping requests to the authorize endpoint. But loginWithRedirect() can also call the authorize endpoint and it doesn't check the getAccessTokenSilently() lock.

To work around this I have changed our API calls from the client to avoid calling getAccessTokenSilently() if the user is not authenticated (using isAuthenticated to check the status). This is because when isAuthenticated is false then a loginWithRedirect() might be already in progress. When loginWithRedirect() is finished, then isAuthenticated becomes true and we can call getAccessTokenSilently().

This assumes that the main way isAuthenticated can switch from true back to false is if the user logs out.