How to recover React App state after loginWithRedirect

When a React application calls loginWithRedirect with default values for the options argument, the state of the application is cleared when the application is called back after the redirect.

The approach described here bundles the state to be recovered into a Javascript literal object and uses options.appState so that the inbound redirect contains the desired state.

This is all in the context of React front ends that rely on the useAuth0 hook.

I invite constructive feedback about issues this approach might create, especially around security. If sensitive information is being exchanged, then it might make sense to encrypt or hash the appStateJSON or the values it contains.

The general approach is:

Preparing loginWithRedirect:

  1. Assemble the parameters to be passed in a Javascript literal object.

    In this example, I pass principalID, isNewUser, localEmail, and localName. These capture application state that needs to be preserved across the redirect.

    const appState = {
      principalID: principalID,
      isNewUser: isNewUser,
      email: localEmail,
      name: localName,
    }
    
  2. Use JSON.stringify to obtain a JSON-encoded string. I call this “appStateJSON”. I use encodeURIComponent to sanitize this for use in a URL.

    const appStateJSON = encodeURIComponent(JSON.stringify(appState))
    
  3. In the “options” of the various redirect Auth0 calls, add a parameter binding with name “appState” and whose value is “appStateJSON” (formed above).

    const options = {
      appState: {appStateJSON: appStateJSON},
      redirectUri: redirectURI,
      login_hint: login_hint,
      prompt: 'login',
    }
    loginWithRedirect(options);
    

Handling the inbound redirect after login:

  1. I modified the current recommended code for Auth0ProviderWithHistory as follows:

      const onRedirectCallback = (appState) => {
        if (appState) {
          const appStateJSON = appState.appStateJSON;
          const queryString = `?appStateJSON=${appStateJSON}`
          window.location.search = queryString;
        }
        history.push(appState?.returnTo || window.location.pathname);
      };
    
    

    This pushes appStateJSON onto the window.location.search for use later.

  2. I add the following at or near the top of the React app that receives the redirect:

    const queryParameters = new URLSearchParams(window.location.search);
    const appStateJSON = queryParameters.get("appStateJSON")
      
    const transientStateFrom_ = (anAppStateJSON) => {
      const answer = {}
      if (anAppStateJSON) {
        // Parse and handle incoming redirect state
        const redirectState = JSON.parse(anAppStateJSON);
        // Destructure redirectState
        answer.principalID = redirectState.principalID;
        answer.name = redirectState.name;
        answer.email = redirectState.email;
        answer.isNewUser = redirectState.isNewUser
      }
      return answer
    };
      
    const transientState = useMemo(
      () => transientStateFrom_(appStateJSON),
      [
        appStateJSON
      ]
    );
    

    I store the bindings from the redirect call in an JS object called transientState. I have a convention of using a trailing underscore (“_”) to indicate that a parameter is expected. The transientFromAppState_ arrow function parses the incoming appStateJSON and destructures the values I need into transientState. Since this do not change (ever!), I use the useMemo hook to avoid thrashing on each render.

    I haven’t found a “destructuring” incantation (in Javascript) that works, so I’ve open coded it.

With this furniture in place, the React app can collect whatever it needs from transientState after a redirect.

In this example, ‘principalID’ is a React state variable created as follows:

const [principalID, setPrincipalID] = useState(null);

Here is a useEffect hook that reconstructs the principalID state on redirect:

  useEffect(
    () => {
      if (    (principalID === null)
           && (transientState)
           && (transientState.principalID)) {
        setPrincipalID(transientState.principalID)
      }
    },
    [
      principalID,
      transientState,
      setPrincipalID,
    ]
  );

So far as I know, this approach is able to pass through arbitrary state (including embedded objects and such). I intentionally chose structures that do not require special magic for JSON.stringify and JSON.parse.

I’ve been using this code for a few weeks now and it seems to work fine.

Developers who are using Auth0 from React apps will surely need this mechanism.

I strongly encourage the Auth0 team to provide something along these lines in the technical documentation.

I’m adding this reply just to indicate that this topic is informational, and doesn’t require a “solution”.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.