How can I persist authentication across different applications and domains in a micro-frontend architecture?

Is there a way to persist authentication between different applications that are hosted on different domains and may also be embedded within another website?
For example, suppose I have the following domains (all using the same Auth0 tenant):

I would like a user to log in on one of these sites and automatically be logged in on the others as well. For instance, if a user logs in on will-be-embedded.com, they should also be authenticated when they visit host1.com or another-host2.com.

I am particularly interested in persisting authentication in a micro-frontend architecture, where one website is embedded within another. For example, if a user is on host1.com and logs in through will-be-embedded.com (which is embedded within host1.com), they should be authenticated across the entire website, not just within the embedded component. Currently, I am experiencing issues with this scenario.

On my dummy test websites based on this Auth0 React sample, it seems to work. However, when I try to implement it with real websites, it does not work as expected.

Does anyone have any ideas or recommendations on how to implement this kind of functionality?

Hi @pruchay

Welcome to the Auth0 Community!

I am sorry about the delayed response to your inquiry!

If all the mentioned websites use Auth0 as their authentication method (under a single tenant) and all of them are using users stored inside the tenant, then you should be able to perform silent authentication whenever the user accesses either one of the websites after authenticating only in one of them:

if(isAuthenticated)
{
 getAccessTokenSilently();
}

By doing so, basically the user will be able to log in via SSO as long as their token is valid and by authenticating in a single website.

I understand that by using the sample React application, this behaviour appears to work as intend, most probably your application is not checking the user’s session properly or it was not configured to do so.

If I can help with any other questions on the matter, let me know!

Kind Regards,
Nik

Thank you for your response.

most probably your application is not checking the user’s session properly or it was not configured to do so

How to check it? I assume it should be done automatically by the @auth0/auth0-react library.

In general, when I embed one website that uses Auth0 into another (the host website), both using Auth0, and then try to log in through the embedded website, the user is authenticated and redirected to the host website. In this case, does the host website receive the appState in the onRedirectCallback? Because from what I can see, it doesn’t, but maybe I did something wrong.

Hi again

As mentioned in the onRedirectCallback documentation:

By default this removes the code and state parameters from the url when you are redirected from the authorize page. It uses window.history but you might want to overwrite this if you are using a custom router, like react-router-dom See the EXAMPLES.md for more info.

I believe that the state and code parameters are not being retrieved since it uses the default behaviour.
You might want to look into handleRedirectCallback which states:

After the browser redirects back to the callback page, call handleRedirectCallback to handle success and error responses from Auth0. If the response is successful, results will be valid according to their expiration times.

Parameters

  • Optional url: string

The URL to that should be used to retrieve the state and code values. Defaults to window.location.href if not given.

Otherwise, I would suggest to perform a silent authentication using getAccessTokenSilently after the user logs in using the embedded website.

Kind Regards,
Nik

Hello, both websites already have their own onRedirectCallback, each responsible for its own application.

For example, in the web component I’m embedding, my Auth0Provider contains:

onRedirectCallback={(appState) => {
  console.log('appState', appState);
  navigate(`/${appState?.returnTo ?? "/"}`, {
    replace: true,
    state: { skipWelcomePage: true },
  });
}}

On the host website, where I embed my web component, I also have onRedirectCallback:

  const onRedirectCallback = (appState) => {
    console.log('appState', appState);
    if (appState?.returnTo.includes("https")) {
      window.location.href = appState?.returnTo;
    } else {
      setPendingRedirect(appState?.returnTo || window.location.pathname);
    }
  };

If I log in directly through the host website, I’m correctly redirected to the previous page and successfully logged in. I can also see the appState log in the browser console.
The same happens if I log in through the standalone web component.

However, if I try to log in on the host website via the embedded web component, I am redirected to the /callback page with state and code params in the URL, but I do not see any appState log in the browser console.

Do you know what could cause this behavior?

Additionally, one interesting thing. You previously mentioned using the following code:

if(isAuthenticated)
{
 getAccessTokenSilently();
}

In my case, it doesn’t work because isAuthenticated returns false (even if the user previously came back from authentication). But if I flip the condition to if (!isAuthenticated), I get an error Login required in the browser console, but the user is silently authenticated.

Hi again!

Thanks for all the additional info on the matter!

Just to see if I understood the scenarios correctly, when you are using the isAuthenticated condition (even if it does not behave correctly, does it successfully log in the user after they have authenticated through the embedded component or not?

Otherwise, as far as I know, in order for isAuthenticated to get set to true, the function in onRedirectCallback has to get called. If the redirectUri doesn’t load completely, the onRedirectCallback function will not get called. In this case, could you try something similar as outlined in this community post:

try {
    auth0.isAuthenticated().then(async function (authenticated) {
        if (!authenticated) {
            const query = window.location.search;
            const shouldParseResult = query.includes("code=") && query.includes("state=");
            if (shouldParseResult) {
                console.log("> Parsing redirect");
                try {
                    const result = await auth0.handleRedirectCallback();
                    console.log("Logged in!");
                } catch (err) {
                    console.log("Error parsing redirect:", err);
                }
                window.history.replaceState({}, document.title, "/");
            } else {
                auth0.loginWithRedirect({ redirect_uri: window.location.origin });
            }
        } else {
            auth0.getTokenSilently().then(function (token) {
                Ext.Ajax.setDefaultHeaders({ 'Authorization': 'Bearer ' + token });
            });
        }
    })
} catch (err) {
    console.log("Log in failed", err);
}

Let me know of the outcome and I will be coming back to you with an update as soon as possible!

Kind Regards,
Nik

Just to see if I understood the scenarios correctly, when you are using the isAuthenticated condition (even if it does not behave correctly, does it successfully log in the user after they have authenticated through the embedded component or not?

Yes, the user is successfully authenticated, but there is no redirect to the page from which they tried to log in - the user stays on the callback page with code and state in the URL. If I manually navigate to the original page, everything works well. At the same moment, on the dummy test page based on the auth0-react-samples, it works as expected - the user is redirected to the page from appState.returnTo.

Regarding the code snippet you provided in a previous message:
I need to rewrite it, because auth0.isAuthenticated().then(... is not possible - isAuthenticated is a boolean.

I assume it should be something like this:

const { getAccessTokenSilently, handleRedirectCallback, isAuthenticated, loginWithRedirect } =
    useAuth0();

  try {
    if (!isAuthenticated) {
      const query = window.location.search;
      const shouldParseResult = query.includes("code=") && query.includes("state=");
      if (shouldParseResult) {
        console.log("> Parsing redirect");
        try {
          (async () => {
            const result = await handleRedirectCallback();
            console.log("result: ", result);
            console.log("Logged in!");
          })();
        } catch (err) {
          console.log("Error parsing redirect:", err);
        }
        window.history.replaceState({}, document.title, "/");
      } else {
        loginWithRedirect({
          appState: {
            redirect_uri: window.location.origin,
            returnTo: window.location.href,
          },
        });
      }
    } else {
      console.log("Not authenticated: ");
      (async () => {
        await getAccessTokenSilently().then((token) => {
          console.log("token: ", token);
        });
      })();
    }
  } catch (err) {
    console.log("Log in failed: ", err);
  }

Could you please tell me exactly where I should place this code? I have tried different scenarios and get various errors, such as Error: You forgot to wrap your component in <Auth0Provider>. If I wrap it with Auth0Provider, I get nothing in the console log or encounter other strange errors.

Maybe this information will help:
I have a component called AuthWrapper where I use Auth0Provider and some other code for callback handling (I use this component for the /callback path and also for paths that require authentication):

export const AuthWrapper = ({ children }: PropsWithChildren) => {
  const navigate = useNavigate();
  const [authState, setAuthState] = useState({
    audience: "",
    clientId: "",
    domain: "",
  });

  const [urlSearchParams] = useSearchParams();

  const { config } = useViewerState();

  const gatherAuthState = useCallback(() => {
    const authSessionKey = Object.keys(sessionStorage).find((key) =>
      key.startsWith("a0.spajs.txs"),
    );
    const authSession = authSessionKey ? sessionStorage.getItem(authSessionKey) : null;

    if (authSession && !authState.audience && !authState.clientId && !authState.domain) {
      try {
        const {
          appState: {
            config: { authentication },
          },
          audience,
        } = JSON.parse(authSession) as Auth0State;

        setAuthState({
          audience,
          clientId: authentication?.clientId ?? "",
          domain: authentication?.domain ?? "",
        });
      } catch (error) {
        console.error("Failed to parse auth session:", error);
      }
    }
  }, [authState]);

  useEffect(() => {
    if (urlSearchParams.get("code") && urlSearchParams.get("state")) {
      gatherAuthState();
    }
  }, [gatherAuthState, urlSearchParams]);

  const isAuthConfigured = useMemo(
    () =>
      (config.authentication?.audience &&
        config.authentication?.domain &&
        config.authentication?.clientId) ??
      (authState.audience && authState.clientId && authState.domain),
    [
      authState.audience,
      authState.clientId,
      authState.domain,
      config.authentication?.audience,
      config.authentication?.clientId,
      config.authentication?.domain,
    ],
  );

  if (isAuthConfigured ?? config?.isLoginRequired) {
    return (
      <Auth0Provider
        authorizationParams={{
          audience: config.authentication?.audience ?? authState.audience,
          redirect_uri: `${window.location.origin}/callback`,
        }}
        cacheLocation="localstorage"
        clientId={config.authentication?.clientId ?? authState.clientId}
        domain={config.authentication?.domain ?? authState.domain}
        onRedirectCallback={(appState) => {
          navigate(`/${appState?.returnTo ?? "/"}`, {
            replace: true,
            state: { skipWelcomePage: true },
          });
        }}
      >
        <SilentAuthenticationHandler />
        {children}
      </Auth0Provider>
    );
  }

  return children;
};

Additional information: on my local test environment, I have the same clientId, but on, let’s say, staging environments, I have different clientIds. I assume this could cause incorrect behavior. What do you think? If that’s the case, what would be the best way to address it?

Hello, I just wanted to follow up and see if anyone might have any ideas or suggestions regarding my question.

Thank you in advance!

Hi again @pruchay

I am sorry for the delayed response to your updates on the matter!

In the code snippet you have provided, instead of using onRedirectCallback, could you please use handleRedirectCallback instead? You can also try calling handleRedirectCallback after using onRedirectCallback to see what is the behaviour of the application in that instance?

As mentioned in the Github documentation:

After the browser redirects back to the callback page,
call `handleRedirectCallback` to handle success and error
responses from Auth0. If the response is successful, results
will be valid according to their expiration times.

From what I am noticing, your application might have some kind of misconfiguration where it does not handle the redirect accordingly after being able to retrieve the token from Auth0 whenever the session is being detected from the embedded component session. I would advise you to look into the skipRedirectCallback documentation.

Otherwise, if none of these proposed options seem to be fixing the redirect issue that you are experiencing, I would highly advise to open an issue on the Auth0 React’s repository page.

Kind Regards,
Nik