javascriptreactjskeycloakreact-router-domreact-oidc-context

Problem in browser back button after Keycloak authentication in React application


In my React application when user authenticates with Keycloak, and after redirecting back from Keycloak (React page refreshes), on pressing browser back button the user is not able to navigate to the previous React route and again gets redirected to Keycloak URL. I have tried the following solutions and all of them work, but after page refreshing (redirecting to Keycloak) none of them work:

I use react-oidc-context.

Solution 1

window.onpopstate = function() {
  navigate("/component1");
}

Solution 2

window.history.pushState({}, '', "/component1");

Solution 3

window.addEventListener("popstate", () => {
  navigate("/component1");
});

I need to mention that, I have tested all the above codes with useEffect, but nothing worked.

These are the Keycloak configurations in React:

  export const userManager = new UserManager({
    authority: 'https://example.com/realms/example-realm',
    client_id: 'example-clientid',
    redirect_uri: 'https://www.example.com/',
    response_type: 'code',
    post_logout_redirect_uri: window.location.origin
  });

export const onSigninCallback = () => {    
   window.history.replaceState({}, document.title, "/"); 
};

Then I use the above configurations in the index.tsx:

import { BrowserRouter } from 'react-router-dom';
import { AuthProvider} from 'react-oidc-context';
import { onSigninCallback, userManager } from './config';

ReactDOM.render(
  <AuthProvider userManager={userManager} onSigninCallback={onSigninCallback}>
    <Provider store={store}>
      <React.StrictMode>
       <BrowserRouter>
        <App />
       </BrowserRouter>
     </React.StrictMode>
    </Provider>
    
  </AuthProvider>,
  document.getElementById("root")
  
);

Then in a component I check whether the user is authenticated or not. After authentication, From here, I want to go back to the previous route which I was on, but when clicking on back, I get redirected again to Keycloak:

const [hasTriedSignin, setHasTriedSignin] = useState(false);
    const nav=useNavigate();
    const auth = useAuth();
    useEffect(() => {
      if (!hasAuthParams() && !auth.isAuthenticated && !auth.activeNavigator && !auth.isLoading && !hasTriedSignin) {
        auth.signinRedirect();
        setHasTriedSignin(true);
      }
      
    });
window.onpopstate = function() {
  navigate("/previousRoute");
}

In the Keycloak configuration section, I have even tried not to use OnSignInCallBack, to avoid any history modification, But did not help.


Solution

  • Issue

    The main issue here is that your app code is not properly maintaining the history stack such that any back navigation action takes the user back to the correct page. You have some additional forward (PUSH) navigation actions getting in the way and users seem to get stuck or get bounced between your app's redirection to your auth service and it's handling redirecting to the correct page after getting redirected back. Trying to listen for and handle any POP actions after fact is a bit of a red herring.

    The first forward navigation action is with the signinRedirect and the RedirectNavigator it uses. "assign" is the default value assigned here.

    export class UserManagerSettingsStore extends OidcClientSettingsStore {
        ...
        public readonly redirectMethod: "replace" | "assign";
        ...
    
        public constructor(args: UserManagerSettings) {
            const {
                ...
                redirectMethod = "assign",
                ...
            } = args;
    
            super(args);
    
            ...
            this.redirectMethod = redirectMethod;
            ...
        }
    }
    

    The second forward navigation action is from your App.tsx file where you check the Keycloak result and issues a forward navigation action to your RedirectMe page with navigate("/redirectMe");. This uses a PUSH action by default.

    Solution Suggestion

    The default redriect method in auth.signinRedirect is "assign" which issues a PUSH action instead of a REPLACE action.

    You can specify redirectMethod: "replace" to actually redirect to your auth service.

    export const userManager = new UserManager({
      authority: 'https://example.com/realms/example-realm',
      client_id: 'example-clientid',
      redirect_uri: 'https://www.example.com/',
      response_type: 'code',
      post_logout_redirect_uri: window.location.origin,
      redirectMethod: "replace", // <-- specify to REPLACE instead of PUSH a new history entry
    });
    

    or

    auth.signinRedirect({
      redirectMethod: "replace", // <-- specify to REPLACE instead of PUSH a new history entry
    });
    

    After users are redirected back to your app and it handles unpacking any payloads/query params it should also redirect (REPLACE) instead of PUSHing to your "/redirectMe" path.

    if (params?.get('code')){
      navigate("/redirectMe", { replace: true }); // <-- REPLACE, not PUSH
    }
    

    And then finally if your "/redirectMe" also redirects anywhere these also should be REPLACE actions.

    Examples:

    navigate(finalPath, { replace: true });
    
    return <Navigate to={finalPath} replace />;