okta-react
okta-react copied to clipboard
SPA token refresh with multiple tabs using Authorization flow with PKCE
I am using okta hosted login for my react SPA. Token is obtained via the authorization code with PKCE. I have also enabled the early access refresh rotation feature for SPA (ref: https://developer.okta.com/docs/guides/refresh-tokens/refresh-token-rotation/)
The problem with this is I can't seem to implement a solid refresh strategy when my application is opened on multiple tabs. Both tabs, would trigger a call to the token
endpoint to perform the refresh ~~obviously one of them will fail for exact issue that was observed), which would then trigger a page refresh on that tab~~ (This is inaccurate. Refer comment for the exact issue that was observed). This page refresh is a deal breaker for me.
Is there a design pattern that I can follow to gracefully handle the above scenario? Something like a silent failure when the refresh fails since the successful call on the other tab would eventually update localStorage
with the latest token information.
My auth related code is pretty much what's shown in the sample code and is implemented as follows:
Okta Auth Configuration
const oktaAuthConfig = {
clientId,
issuer,
redirectUri,
"scopes": "openid,profile,email,groups,offline_access",
"pkce": true,
tokenManager: {
expireEarlySeconds,
}
};
App.tsx
import React from 'react';
import {useHistory} from "react-router-dom";
import {Security} from "@okta/okta-react";
import {OktaAuth} from "@okta/okta-auth-js";
import {oktaAuthConfig} from "./config/config";
const App = () => {
const oktaAuth = new OktaAuth(oktaAuthConfig);
const history = useHistory();
const restoreOriginalUri = async (oktaAuth: any, originalUri: any) => {
history.replace('/login');
};
const onAuthRequired = () => {
history.push('/login');
};
return (
<Security
oktaAuth={oktaAuth}
restoreOriginalUri={restoreOriginalUri}
onAuthRequired={onAuthRequired}
>
<Switch>
<Route path='/login' exact component={Auth}/>
<Route path={config.callbackUrlPath} component={LoginCallback} />
<Routes /> // secure application routes
</Switch>
</Security>
);
};
export default App;
Auth.tsx that directs user to okta hosted login if not authenticated.
import * as React from 'react';
import {useOktaAuth} from "@okta/okta-react";
import AuthWrapper from "./AuthWrapper";
import {Redirect} from "react-router";
const Auth = () => {
const { authState, oktaAuth } = useOktaAuth();
const handleLoginRedirect = () => oktaAuth.signInWithRedirect();
if(authState.isPending) {
return (
<div>Loading authentication...</div>
);
}
if (authState.isAuthenticated) {
return <Redirect to={'/'}/>;
}
handleLoginRedirect(); // user is not authenticated and auth is not pending. redirect to okta hosted login.
return (null);
};
export default Auth;
@priyath Thanks for your report! If I understand your problem correctly, you are using localStorage
as the token storage to support multiple tabs scenario, then when token refresh fails in one tab, it triggers page refresh
.
Can you explain more about the page refresh
part? Does it redirect the app to the okta hosted sign-in page, or just reload the page but still keep the user authenticated?
Also, can you try the okta-hosted-login sample to see if you still can reproduce the issue? Thanks
@shuowu apologies for the delay. I had a better look into the problem and also played around with the okta-hosted-login sample. I observed the following issues for my scenario with the sample code:
Setup:
- okta-hosted-login sample with early access feature Refresh Token Rotation enabled on okta
- Requested scopes: ['openid', 'profile', 'email', 'offline_access']
-
@okta/okta-auth-js
version 4.9.0 and@okta/okta-react
version 5.1.1
Start the application, perform login, and on a single tab observe the network tab and wait for token rotation (I configured expireEarlySeconds
to trigger the refresh every 30 seconds).
Observations:
- Every 30 seconds, multiple calls were being made to the
authorize
,token
, anduserinfo
endpoints for a single token refresh. (I believe there should not be anauthorize
request when refresh token is used for token rotation?) - Sometimes as many as 5
token
andauthorize
endpoint requests were getting triggered repeatedly for a single token refresh. - With multiple tabs, the same behavior was observed. Additionally, the
authorize
request fails sporadically if kept the app running on multiple tabs long enough. - The page refresh that I mentioned initially may be related to these failed
authorize
calls. However, I could not reliably recreate this issue.
Interestingly, if I bump @okta/okta-auth-js
down to 4.8.0, the issue does not happen. There are no authorize
calls during token refresh. Instead there is a single token
request being made, followed by a single userinfo
request.
It seems like each time the application re-renders, an additional token
request will be made during the next refresh. Possibly because a setTimeout
is not getting cleared? I have raised a separate issue with my observations: https://github.com/okta/okta-react/issues/121
Interestingly, if I bump
@okta/okta-auth-js
down to 4.8.0, the issue does not happen. There are noauthorize
calls during token refresh. Instead there is a singletoken
request being made, followed by a singleuserinfo
request.
This may be related to https://github.com/okta/okta-react/issues/114. I see that okta-react
v5.1.0/5.1.1 pulls in okta-auth-js
v4.8, so when we use v4.9 or higher, we end up with two okta-auth-js
copies (v4.8 and v4.9) bundled by webpack.