okta-auth-js
okta-auth-js copied to clipboard
Page reload on redirecting from okta login
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
Thanks for the report, we will look into this
Internal Ref: OKTA-519226
@annovo I've been experiencing the same issue. Did you ever figure out a workaround for this?
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?
@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 Bummer. I'll give that a shot. Thanks for the response!
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
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 how are you prompting users to login (redirecting them to Okta) when isAuthenticated=false
?
@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:
-
oktaAuth.signInWithRedirect
is triggered whenauthState.isAuthenticated === false
- 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 whyoktaAuth.signInWithRedirect
is triggered again in your app. - your app is stabilized once in memory state/tokens is updated.
So to fix the issue, you can try the following options:
- okta-auth-js exposes
authClient.isLoginRedirect
method, you can use it in your useEffect hook to avoid callingsignInWithRedirect
multiple times - You can try scope that useEffect hook in its own component, so it will not be triggered during login redirect process.
@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 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])
Having this problem with v7.2.0 also. Is there a workaround?
@elafito what versions of auth-js and react-router are you using?
@jaredperreault-okta react-router-dom: 6.6.1 @okta/okta-auth-js: 7.2.0 @okta/okta-react: 6.7.0
@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?
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 do you observe this behavior in the routing samples linked above? https://github.com/okta/okta-auth-js/issues/1265#issuecomment-1375788572
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 @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'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>
);