okta / okta-react

Okta OIDC SDK for React
https://github.com/okta/okta-react
Other
114 stars 78 forks source link

SPA token refresh with multiple tabs using Authorization flow with PKCE #117

Open priyath opened 3 years ago

priyath commented 3 years ago

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;
shuowu commented 3 years ago

@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

priyath commented 3 years ago

@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:

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:

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.

priyath commented 3 years ago

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

khitrenovich commented 3 years ago

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.

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.