okta / okta-react

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

Chrome re-authenticates after token expires #269

Open justisom opened 11 months ago

justisom commented 11 months ago

Describe the bug

My web application is on a rotating refresh token that, once expired, should return the user to the login screen. This works well enough in Firefox and Safari. However in Chrome it appears to be in the middle of a re-authentication after the expiration. It briefly jumps to the login screen and then right back to the protected home route.

[App.js] This is the configuration for the OktaAuth instance

const config = {
  issuer: process.env.OKTA_ISSUER,
  clientId: process.env.OKTA_CLIENT_ID,
  redirectUri: window.location.origin + '/oidc/callback',
  scopes: ['openid', 'profile', 'email', 'offline_access'],
  tokenManager: {
    storage: 'sessionStorage'
  },  
}
const oktaAuth = new OktaAuth(config);

And App function. I'm routing all protected urls though RequireAuth since I can't use SecureRoute with react-router-dom v6. This is based off of the example in the okta-react repo with a few modifications.

const App = () => {
  const navigate = useNavigate();
  const restoreOriginalUri = async (_oktaAuth,  originalUri) => {
    navigate(toRelativeUrl(originalUri || '/', window.location.origin));
  };

  return (
      <Security
        oktaAuth={oktaAuth} 
        restoreOriginalUri={restoreOriginalUri}
      >
        <CacheProvider value={cache}>
          <Box
            id='app-root-container'
            sx={{
              display: 'flex',
              flexDirection: 'column',
              height: '100vh',
              m: -1,
            }}
          >
              <Fragment>
                <Suspense fallback={<LoadingIcon />}>
                  <Routes>
                    <Route path='/' exact={true} element={<RequireAuth />}>
                      <Route path='' element={<Home />} />
                    </Route>
                    <Route path='/login' element={<Login />} />
                    <Route path='/oidc/callback' element={<LoginCallback />} />
                    <Route path='*' element={<PageNotFound404 />} />
                  </Routes>
                </Suspense>
              </Fragment>
          </Box>
        </CacheProvider>
      </Security>  
  )
}

[SecureRoute.js] This is very similar to the sample code. I'm using a notLoggedIn state to facilitate the return to the /login route upon de-authentication. The reason for doing this was because I was getting stuck in an endless loop of going to the /login route.

export const RequireAuth = () => {
  const { oktaAuth, authState } = useOktaAuth();
  const [ notLoggedIn, setNotLoggedIn ] = useState(false);

  useEffect(() => {
    if (!authState) {
      console.log('authState not initialized.')
      return;
    }

    if (!authState?.isAuthenticated) {
      console.log('user not authenticated')
      setNotLoggedIn(true);
    } else {
      console.log('user is authenticated!')
      setNotLoggedIn(false)
    }
  }, [oktaAuth, !!authState, authState?.isAuthenticated]);

  if (!authState || !authState?.isAuthenticated) {
    if (notLoggedIn) {
      console.log("load login page");
      return <Navigate to='/login' replace={true} />
    } else {
      console.log(
        "Route not Authenticated. Waiting for authentication page to load."
      );
      return <LoadingIcon />
    }
  }

  return (<Outlet />);
}

[Login.js] This will check to see if the user is already authenticated. If so it simply jumps to the home screen.

const Login = () => {
  const { oktaAuth, authState } = useOktaAuth();
  const theme = useTheme();

  const handleLogin = async () => {
    await oktaAuth.signInWithRedirect();
  }

  const layout = () => {
    if (authState?.isAuthenticated) {
      console.log('already authenticated. Going to main page.')
      return <Navigate to='/' replace={true} />
    } else {
      return (
         ...lots of UI code including a button that uses the handleLogin callback
      )
    }
  } 

  return layout();
}

This is the post-login console output in Chrome. As you can see fetchRequest is unable to validate the token everything seems to de-authenticate and return to the login page. Then fetchRequest executes again and gets a new token and the whole thin returns to the protected home route. Screenshot 2023-12-06 at 9 22 37 PM

This is the post-login console output in Firefox. I'm not sure why it shows the GET to v1/authorize in Firefox and not Chrome. I assumed they would both do this. Firefox might just be showing it where Chrome does not. I'm not sure if this is significant. At any rate once the token is expired Firefox returns to login. All session tokes are cleared (except for the ID token). Screenshot 2023-12-06 at 9 27 36 PM

Hopefully this is good enough to start. Happy to share more details as needed.

Thanks in advance!

Reproduction Steps?

I've uploaded an example repo pared down to just a login route and a protected home route. It won't work as-is but it should provide further context to the overall flow of the application. Here

SDK Versions

"@emotion/react": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.11.16",
"@mui/material": "^5.13.2",
"@okta/okta-auth-js": "^7.3.0",
"@okta/okta-react": "^6.7.0",
"axios": "^1.4.0",
"core-js": "^3.33.2",
"history": "^5.3.0",
"npm": "^9.6.7",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.11.2"

Additional Information

Here's my Okta SPA configuration:

frontapp_settings

My authorization server access rules. The token rotation are set to as short an interval as I could set in order to test without waiting forever. If there's a better way to test this I'd love to hear it!

authserverrules

jaredperreault-okta commented 11 months ago

@justisom In your Chrome dev console, there is a request to /v1/token which fails (presumably because the refreshToken has expired), So SecureRoute redirects to Login, there then seems to be another request to /v1/token, does that 2nd fetch succeed?

Edit: Also, thank you for filing such a thorough report!

justisom commented 11 months ago

Yes, the first POST fails, then there's a message (I'm assuming the caught exception) that says "Fetch failed loading: POST..." Then once SecureRoute confirms that the user is not authenticated it goes to the login page. The Login page reiterates the the user is not authenticated. At this point it should render the loading screen. It actually does do this if I pause on exceptions or use POSTs as breakpoints. So then the 2nd fetch occurs (triggered by what, I have no idea). That fetch presumably succeeds because now the login page confirms that the user is authenticated and proceeds to the home screen. This 2nd fetch never occurs in Firefox.

Thanks!

jaredperreault-okta commented 10 months ago

@justisom Are you still experiencing this issue? I haven't been able to repro

justisom commented 9 months ago

Yeah. I'm still having the issue. I think I found a work-around by storing a boolean value in session storage when the user is no longer authenticated. I then check that stored boolean in the Login script before proceeding. It's not super-elegant but it works in this case. I just wish I knew what it was acting in this way in the first place.