Duplicate refresh access token call (automatic silent renew) when React Strict Mode is enabled
Describe the bug
The refresh token endpoint is called twice simultaneously when React Strict Mode is enabled.
This occurs when:
- It's the first rendering of
<AuthProvider> - Or, after waiting for a few minutes, the access token refresh is processed
depending on whether the access token is still valid.
How to Reproduce
(Not always reproducible)
Follow the instructions in the README to create the simplest application is sufficient (but with the <StrictMode>).
// src/index.jsx
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { AuthProvider } from "react-oidc-context";
import App from "./App";
const oidcConfig = {
authority: "<your authority>",
client_id: "<your client id>",
redirect_uri: "<your redirect uri>",
// ...
};
ReactDOM.render(
<StrictMode>
<AuthProvider {...oidcConfig}>
<App />
</AuthProvider>
</StrictMode>,
document.getElementById("app")
);
// src/App.jsx
import React from "react";
import { useAuth } from "react-oidc-context";
function App() {
const auth = useAuth();
switch (auth.activeNavigator) {
case "signinSilent":
return <div>Signing you in...</div>;
case "signoutRedirect":
return <div>Signing you out...</div>;
}
if (auth.isLoading) {
return <div>Loading...</div>;
}
if (auth.error) {
return <div>Oops... {auth.error.kind} caused {auth.error.message}</div>;
}
if (auth.isAuthenticated) {
return (
<div>
Hello {auth.user?.profile.sub}{" "}
<button onClick={() => void auth.removeUser()}>Log out</button>
</div>
);
}
return <button onClick={() => void auth.signinRedirect()}>Log in</button>;
}
export default App;
Then, sign in with your account and wait for a few minutes for the access token refresh to be processed.
Expected behavior
The refresh token endpoint SHOULD NOT be called twice simultaneously when React Strict Mode is enabled.
Screenshots/Video
In the screenshot, the 2
token requests are sent simultaneously. You can refer to the request call stack to see how this happened.
Version
Happens in both:
- 3.3.0
- 2.3.1
when using [email protected] (any 18+ version should be fine)
Possible Reasons
In AuthProvider.ts, UserManagerImpl is instantiated in the useState's initializer function.
https://github.com/authts/react-oidc-context/blob/b6d9e22e4a034652c36a10cae96824a913eb719d/src/AuthProvider.tsx#L168-L173
When a new UserManager is constructed, it appears to schedule a timer to refresh the access token (a side effect).
https://github.com/authts/oidc-client-ts/blob/521df1316ba353c2b22e84214326002149f38eed/src/UserManager.ts#L107-L109
According to https://react.dev/reference/react/useState#usestate, the initializer function must be a pure function, and React will call the initializer function twice in order to help us find impurities when the strict mode is enabled. This results in the initializer function being called twice, creating 2 UserManager instances and scheduling two separate timers to refresh the access token simultaneously.
Why It Is a Problem
- Inconsistent State and Race Conditions: When two
UserManagerinstances are created, they may independently attempt to refresh the token. This can lead to race conditions where the client receives conflicting responses (e.g., one token is valid, the other is expired), causing the application to lose track of the user's authentication state. Additionally, if the server invalidates the token after the first refresh request, the second request may fail, leaving the client in an inconsistent state. This could force the user to re-authenticate or cause unexpected errors in the application. - Unintended Server Load: The simultaneous invocation of the refresh token endpoint doubles the number of requests sent to the authentication server. This can overwhelm the server, especially under high traffic, leading to potential rate-limiting, increased latency, or even service degradation.
The simultaneous invocation of the refresh token endpoint doubles the number of requests sent to the authentication server.
StrictMode has no effect in production.
The simultaneous invocation of the refresh token endpoint doubles the number of requests sent to the authentication server.
StrictModehas no effect in production.
This is true, but having just spent several hours trying to track down the reason for this behaviour when doing some local testing it is unhelpful at best! The second call often errors as mentioned, causing application error handling to kick in.
Strict mode is there for a reason and trying to code around it is messy. Happy to try and come up with a PR to resolve if that would be helpful!
See also this unresolved issue from 2022: https://github.com/authts/react-oidc-context/issues/390
That one contains a reference to a change auth0-react had done around that time (https://github.com/auth0/auth0-react/pull/355) with the intention of solving a similar issue (albeit without the timer complication). The solution they implemented, however, is discouraged by React (using a ref to escape an effect).
If I understand your analysis correctly @ibara1454, the issue isn't really with this package, but with the implementation of UserManager in oidc-client-ts?