React OIDC signinSilent() function causing a page refresh

1k Views Asked by At

Using oidc in react:

import { useAuth } from "react-oidc-context";

//This is inside AuthProvider from react-oidc-context
const MyComponent: React.FC<MyComponentProps> = () => {
    const auth = useAuth();

    auth.events.addAccessTokenExpiring(() => {
        auth.signinSilent().then(user => {
            console.log('signinSilent finished', user);
            //this is where I reset auth token for http headers because it is being stored.
        }).catch(error => {
            console.log('signinSilent failed', error);
        });
    });
}

The config being used for OIDC is pretty simple:

const oidcConfig = {
    authority: authConfig.authority,
    client_id: authConfig.client_id,
    redirect_uri: authConfig.redirect_uri,
    scope: "openid offline_access",
};

This all ends up working. The addAccessTokenExpiring fires when the token is about done and I the signinSilent gets me a new one and I can reset my headers and then 401s won't happen when someone sits idle on the page for an hour.

The problem is signinSilent causes a refresh of the page to happen. If someone is sitting for an hour idle on the page, a refresh would most likely go unnoticed... However, if a form was halfway complete and they stepped away or something, that would just be gone on the page refresh.

Is there anyway to prevent the signinSilent from refreshing the page and actually just silently renewing the token?

3

There are 3 best solutions below

0
Tearf001 On

The hook useAuth that recalls DO the refresh. It is straightforward because of the returned values change.

0
Dmitry On

I have done the following experiment:

My index.tsx looks like that:

import { User } from 'oidc-client-ts';
import ReactDOM from 'react-dom/client';
import { AuthProvider } from "react-oidc-context";
import App from './App.tsx';
import './index.css';


// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onSigninCallback = (_user: User | void): void => {
  window.history.replaceState( {}, document.title, window.location.pathname )
}

const oidcConfig = {
  authority: "http://localhost:8080/realms/test-realm",
  client_id: "test-client",
  redirect_uri: "http://localhost:5173/",
  response_type: 'code',
  silent_redirect_uri: window.location.origin + "/silent-check-sso.html",
  onSigninCallback
};

ReactDOM.createRoot(document.getElementById('root')!).render(
  <AuthProvider  {...oidcConfig}>
    <App />
  </AuthProvider>
);

the App.tsx is the following:

import { useEffect, useState } from "react";
import { useAuth } from "react-oidc-context";

//token refresh triggers App to be re-executed
function App() {

  console.log("App refreshed");

  const [myCounter, setCounter] = useState(1);

  const auth = useAuth();

  // effect will be re-executed, because useAuth return new auth object.
  useEffect(() => {
    console.log("I am completely new Effect");
    // the `return` is important - addAccessTokenExpiring() returns a cleanup function
    return auth.events.addAccessTokenExpiring(() => {
      console.log("Refresh token About to expire");
      setCounter((prev) => {
        return prev + 1;
      })
    });
  }, [auth]);

  switch (auth.activeNavigator) {
    case "signinSilent":
      return <div>Signing you in...</div>;
    case "signoutRedirect":
      return <div>Signing you out...</div>;
  }

  if (auth.isLoading) {
    return <div>Loading...</div>;
  }

  if (auth.error) {
    return <div>Oops... {auth.error.message}</div>;
  }

  if (auth.isAuthenticated) {
    return (
      <div>
        <h5>{myCounter}</h5>
        <form><input type="text"/></form>
        Hello {auth.user?.profile.sub}{" "}
        <button onClick={() => void auth.removeUser()}>Log out</button>
      </div>
    );
  }
  return (<button onClick={() => void auth.signinRedirect()}>Log in</button>)
}

export default App;

  1. The myCounter keeps increasing after each refresh, which says that App function is only re-evaluated and not a new one is created. That is a good starting point.
  2. When I put text into the input it does not get wiped out on each refresh.

The token refresh triggers re-evaluation of the App function. Which might have side-effects one might want to avoid. For instance it leads to the useEffect re-evaluation because useAuth returns a new auth object which is set as the dependency to useEffect.

To minimize side-effects React provides mechanisms to skip re-evaluating children if nothing changed in the component in the meantime:

In particular memo is of most interest. It will skip Component re-evaluation if it's probes have not changed.

here is an example:

import { memo } from 'react';

const Greeting = memo(function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
});

export default Greeting;

0
Gouri Patil On

signinSilent is a method provided by react-oidc that attempts to renew the user's authentication token in the background.

const refreshTokens = async () => {
    try {
        await auth.signinSilent();
        console.log("Token refreshed successfully");
    } catch (error) {
        console.error("Error refreshing token:", error);
    }
};