okta-auth-js icon indicating copy to clipboard operation
okta-auth-js copied to clipboard

Page reload on redirecting from okta login

Open annovo opened this issue 2 years ago • 1 comments

Describe the bug?

Hi everyone!

I am trying to switch to the new okta-auth-js v6.7.3 and have some troubles. I have no issues using okta-auth-js v4.9.2 or v5.1.1 but with v6.7.3 I have the page refreshing on login while redirecting from okta. It looks like this /login/callback -> / -> (reload) -> /login/callback -> /.

Also, I don't have this behavior if I add the login button that calls the same oktaAuth.signInWithRedirect() but I have it with a conditional call based on the authState.

What is expected to happen?

Only one redirect from login page

What is the actual behavior?

Double redirecting / refresh

Reproduction Steps?

The application can be reduced to

const config = {
  clientId: "someid",
  issuer: "your-issuer",
  redirectUri: window.location.origin + "/login/callback",
  scopes: ["openid", "profile", "email"],
  pkce: true,
  disableHttpsCheck: false,
};

const oktaAuth = new OktaAuth(config);

const OktaAuthProvider: React.FC = ({ children }) => {
  const history = useHistory();

  const restoreOriginalUri = async (
    _oktaAuth: OktaAuth,
    originalUri: string
  ) => {
    const basepath = history.createHref({});
    const originalUriWithoutBasepath =
      originalUri?.replace(basepath, "/") || "/";
    history.replace(
      toRelativeUrl(originalUriWithoutBasepath, window.location.origin)
    );
  };

  return (
    <Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}>
      {children}
    </Security>
  );
};

function App() {
  const { oktaAuth, authState } = useOktaAuth();
  const logout = () => oktaAuth.signOut();

  useEffect(() => {
     if (authState && !authState.isAuthenticated) {
       oktaAuth.signInWithRedirect();
     }
  },[authState])

  return (
    <BrowserRouter>
      <OktaAuthProvider>
          <Route path="/" exact={true}>
              <div>
                <p>Logged in!</p>
                <button onClick={logout}>Logout</button>
              </div>
          </Route>
      <Route path="/login/callback" exact={true} component={LoginCallback} />
      </OktaAuthProvider>
    </BrowserRouter>
  );
}

SDK Versions

"@okta/okta-react": "6.6.0" "@okta/okta-auth-js": "6.7.3" "react-router": "5.3.3"

Execution Environment

browser

Additional Information?

No response

annovo avatar Jul 28 '22 23:07 annovo

Thanks for the report, we will look into this

Internal Ref: OKTA-519226

jaredperreault-okta avatar Jul 29 '22 13:07 jaredperreault-okta

@annovo I've been experiencing the same issue. Did you ever figure out a workaround for this?

zaneadix avatar Nov 08 '22 19:11 zaneadix

We have noticed a similar behaviour in our login flow, where the observed behaviour is:

Login page -> submit -> login callback page -> login page -> dashboard (authenticated) page.

Feels related to this?

pzi avatar Nov 09 '22 02:11 pzi

@zaneadix unfortunately, I had to lock the okta-auth-js version to 5.11. I tried to upgrade to the latest version again a few weeks ago, but no luck so far

annovo avatar Nov 09 '22 03:11 annovo

@annovo Bummer. I'll give that a shot. Thanks for the response!

zaneadix avatar Nov 09 '22 14:11 zaneadix

I haven't had an opportunity to properly debug this, but perhaps the issue here is authState isn't a memoized value?

maybe trying something like this: https://github.com/okta/okta-react/blob/master/samples/routing/react-router-dom-v6/src/components/SecureRoute.tsx#L32

As an aside, your LoginCallback component must be mounted outside any auth checks, your user won't be considered authenticated until the oauth callback is processed (via rendering the <LoginCallback> component). Therefore checking isAuthenticated on the /login/callback route will result in an infinite loop of redirects

jaredperreault-okta avatar Nov 09 '22 15:11 jaredperreault-okta

For reference, this is my attempt which works fine with 5.11 but not with 7.0.1 I tried the useEffect dependency changes you linked @jaredperreault-okta but no luck.

const oktaAuth = new OktaAuth({
  issuer: '<our auth route>',
  clientId: config.OKTA_CLIENT_ID,
  redirectUri: window.location.origin + '/login/callback',
  pkce: true,
});

let AuthStateWrapper = ({ children }) => {
  const { authState } = useOktaAuth();
  useEffect(() => {
    if (authState) {
      const { idToken, isAuthenticated, isPending } = authState;
      if (idToken && isAuthenticated && !isPending) {
        Cookies.set('id_token', idToken.idToken, {
          domain: config.GQL_COOKIE_DOMAIN,
          sameSite: 'None',
          secure: true,
        });
      }
    }
  }, [authState?.isAuthenticated]);

  return children;
};

const SecurityProvider = ({ children }) => {
  const history = useHistory();
  const restoreOriginalUri = async (_oktaAuth, originalUri) => {
    history.replace(toRelativeUrl(originalUri || '/', window.location.origin));
  };

  return (
    <Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}>
      <Route path="/login/callback" exact={true} component={LoginCallback} />
      <AuthStateWrapper>{children}</AuthStateWrapper>
    </Security>
  );
};

export { SecurityProvider };

zaneadix avatar Nov 09 '22 15:11 zaneadix

@zaneadix how are you prompting users to login (redirecting them to Okta) when isAuthenticated=false?

jaredperreault-okta avatar Nov 09 '22 21:11 jaredperreault-okta

@annovo I think the issue comes from the global useEffect in your App component.

useEffect(() => {
     if (authState && !authState.isAuthenticated) {
       oktaAuth.signInWithRedirect();
     }
  },[authState])

Here is what may happen in the app:

  1. oktaAuth.signInWithRedirect is triggered when authState.isAuthenticated === false
  2. Okta redirects back to /login/callback route after user signin from the hosted login page, but at this time authState actually has not been changed, since the SPA still need to parse params from the callback url and exchange code for tokens to update the in memory authState. That's why oktaAuth.signInWithRedirect is triggered again in your app.
  3. your app is stabilized once in memory state/tokens is updated.

So to fix the issue, you can try the following options:

  1. okta-auth-js exposes authClient.isLoginRedirect method, you can use it in your useEffect hook to avoid calling signInWithRedirect multiple times
  2. You can try scope that useEffect hook in its own component, so it will not be triggered during login redirect process.

shuowu-okta avatar Nov 28 '22 16:11 shuowu-okta

@shuowu-okta thank you for the suggestion, I've just tried it, but unfortunately, this didn't solve the issue 😞

Also, I would be curious if that's a code issue since the same code works perfectly with the different okta-auth-js version as I mentioned in the description.

annovo avatar Nov 29 '22 02:11 annovo

@annovo You can compare current authState with previousAuthState to decide the redirect logic in hook

useEffect(() => {
    const previousAuthState = oktaAuth.authStateManager.getPreviousAuthState();
    if (previousAuthState?.isAuthenticated && previousAuthState.isAuthenticated !== authState?.isAuthenticated) {
      oktaAuth.signInWithRedirect();
    }
  },[authState])

shuowu-okta avatar Dec 06 '22 22:12 shuowu-okta

Having this problem with v7.2.0 also. Is there a workaround?

elafito avatar Jan 06 '23 17:01 elafito

@elafito what versions of auth-js and react-router are you using?

jaredperreault-okta avatar Jan 09 '23 14:01 jaredperreault-okta

@jaredperreault-okta react-router-dom: 6.6.1 @okta/okta-auth-js: 7.2.0 @okta/okta-react: 6.7.0

elafito avatar Jan 09 '23 14:01 elafito

@elafito I just tested against this sample (https://github.com/okta/okta-react/tree/master/samples/routing/react-router-dom-v6) and did not observe the issue described in this thread. Do you notice any differences between your code and the sample?

jaredperreault-okta avatar Jan 09 '23 15:01 jaredperreault-okta

We have been facing with the same issue. We downgraded our okta-auth-js from 6.7.0 to 5.11.0 and issue is temporarily resolved.

baykalucararcelik avatar Apr 25 '23 08:04 baykalucararcelik

@baykalucararcelik do you observe this behavior in the routing samples linked above? https://github.com/okta/okta-auth-js/issues/1265#issuecomment-1375788572

jaredperreault-okta avatar Apr 25 '23 12:04 jaredperreault-okta

We are still facing this issue. We had to use version 5.11.0 to fix it. Thanks, @baykalucararcelik !

However, version 5.x is retired and there are many key changes after this version which we will miss out - https://github.com/okta/okta-auth-js/compare/okta-auth-js-5.11...okta-auth-js-7.5 Also, Okta recommends using the most stable version. But using the latest breaks the login/landing flow for us.

@jaredperreault-okta , Do we have any resolution for this?

PrathameshPhadke avatar Jan 12 '24 18:01 PrathameshPhadke

@PrathameshPhadke @annovo @baykalucararcelik @elafito @pzi @zaneadix

The effect code posted by @annovo in 1st message is wrong. It leads to described issue with 2 redirects to login callback page (explained in this comment). Also it forces the redirect to login page when user is not authenticated.

If you really want to force users for authenticate (and not render "Login" button since global effect in 1st message will always redirect to login page automatically), please use the following effect instead:

import { isRedirectUri } from '@okta/okta-auth-js';

  useEffect(() => {
    if (authState && !authState.isAuthenticated && !isRedirectUri(location.href, oktaAuth)) {
      oktaAuth.signInWithRedirect();
    }
  }, [authState]);

If you want to auto redirect user to the login page if he was authenticated but now is not (tokens expired), then you should compare authState with previous one before calling oktaAuth.signInWithRedirect:

useEffect(() => {
  const previousAuthState = oktaAuth.authStateManager.getPreviousAuthState();
  if (previousAuthState?.isAuthenticated && previousAuthState.isAuthenticated !== authState?.isAuthenticated) {
    oktaAuth.signInWithRedirect();
  }
}, [ authState ])
Sample code for React 18 and react-router 6
import React, { useMemo, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter, useNavigate, Routes, Route } from "react-router-dom";
import { Security, useOktaAuth, LoginCallback } from '@okta/okta-react';
import { OktaAuth, toRelativeUrl, isRedirectUri } from '@okta/okta-auth-js';


const config = { } // <-- your config
const oktaAuth = new OktaAuth(config);

const OktaAuthProvider: React.FC = ({ children }) => {
  const navigate = useNavigate();

  const restoreOriginalUri = useMemo(() => async (
    _oktaAuth: OktaAuth,
    originalUri: string
  ) => {
    const url = toRelativeUrl(originalUri || '/', window.location.origin);
    navigate(url, {
      replace: true
    });
  }, [ navigate ]);

  return (
    <Security
      oktaAuth={oktaAuth}
      restoreOriginalUri={restoreOriginalUri}
    >
      {children}
    </Security>
  );
};

const Home = () => {
  const { authState, oktaAuth } = useOktaAuth();

  const handleLogin = () => oktaAuth.signInWithRedirect();
  const handleLogout = () => oktaAuth.signOut();
  const handleClearTokens = () => oktaAuth.tokenManager.clear();

  return (
    !authState || !authState.isAuthenticated ?
    (
      <>
        <p>Please log in</p>
        <button type="button" onClick={handleLogin}>Login</button>
      </>
    ) :
    (
      <>
        <p>You&apos;re logged in!</p>
        <button type="button" onClick={handleLogout}>Logout</button>
        <button type="button" onClick={handleClearTokens}>Clear tokens (should trigger signInWithRedirect in effect)</button>
      </>
    )
  );
}


const AppRoutes = () => {
  return (
    <Routes>
      <Route path='/' element={<Home />} />
      <Route path='login/callback' element={<LoginCallback />} />
    </Routes>
  );
};

function App() {
  const { oktaAuth, authState } = useOktaAuth();

  // auto redirect to login if user is not authenticated
  // useEffect(() => {
  //   if (authState && !authState.isAuthenticated && !isRedirectUri(location.href, oktaAuth)) {
  //     oktaAuth.signInWithRedirect();
  //   }
  // }, [authState]);

  // auto redirect to login if tokens expired
  useEffect(() => {
    const previousAuthState = oktaAuth.authStateManager.getPreviousAuthState();
    if (previousAuthState?.isAuthenticated && previousAuthState.isAuthenticated !== authState?.isAuthenticated) {
      oktaAuth.signInWithRedirect();
    }
  }, [authState]);

  return (
    <AppRoutes />
  );
}



const container = document.getElementById('root');
const root = createRoot(container);
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <OktaAuthProvider>
        <App />
      </OktaAuthProvider>
    </BrowserRouter>
  </React.StrictMode>
);

denysoblohin-okta avatar Feb 01 '24 18:02 denysoblohin-okta