intermittent "Invalid state" missing_transaction error from loginWithRedirect()
Checklist
- [X] The issue can be reproduced in the auth0-react sample app (or N/A).
- [X] I have looked into the Readme, Examples, and FAQ and have not found a suitable solution or answer.
- [X] I have looked into the API documentation and have not found a suitable solution or answer.
- [X] I have searched the issues and have not found a suitable solution or answer.
- [X] I have searched the Auth0 Community forums and have not found a suitable solution or answer.
- [X] I agree to the terms within the Auth0 Code of Conduct.
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:
-
withAuthenticationRequiredwrapping multiple page components -
loginWithRedirect()on our index page (not generally open in normal use, as our home screen is on/home) -
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:
- Chrome is loading the page multiple times from within a single tab triggering multiple overlapping calls to
loginWithRedirect()viawithAuthenticationRequired - 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)
- 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:
- Removed
loginWithRedirect()from our index page (just rely onwithAuthenticationRequiredthat is on our other pages instead) - Added logging via the
withAuthenticationRequiredoptiononBeforeAuthenticationto console and Sentry - Added a check on
isAuthenticatedbefore allowing calls togetAccessTokenSilently()
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
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().
I get the identical error. I literaly followed the SDK React tutorial with React Vite and I cannot handle redirect correctly.
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:
- Fixed our
Auth0Providerincluding switching onuseRefreshTokensFallback - Removed all direct
loginWithRedirect()calls except the login button - Only call
getAccessTokenSilently()whenisAuthenticatedistrue
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:
-
loginWithRedirect()from index.js - Auth0Provider initialisation when the app starts up, which in turn calls
getAccessTokenSilently() -
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.
The issue has been resolved, as the solution was provided in the comments. I’m closing it now, but feel free to reopen if needed.