0

Context & Reproducible Scenario

I'm using the combination of these libraries and tools:

I'm using the Auth Code + PKCE redirect flow so this is the flow for users:

  1. They land on /, the home page

  2. They click a /me router link

  3. They go to Azure B2C to log in because said page has this logic:

    <MsalAuthenticationTemplate
      interactionType={InteractionType.Redirect}
      authenticationRequest={loginRequest}>
    

    where loginRequest.state is set to router.asPath (the "intended" page: /me)

    Note that the page is also wrapped in a <NoSsr> component based off Stack Overflow.

  4. User logs in on Azure B2C, gets redirected back to my app at / (the root)

  5. ⛔ Problem: the user now briefly sees the / (home) page

  6. After a very brief moment, the user gets sent to /me where they are signed in

The MSAL docs don't seem to have much on the state property from OIDC or this redirect behavior, and I can't find much about this in the MSAL sample for NextJS either.

In short: the issue

How do I make sure MSAL-React in my NextJS application send users to the "intended" page immediately on startup, without briefly showing the root page where the Identity Server redirects to?

Relevant extra information

Here's my custom _app.js component, which seems relevant because it is a component that triggers handleRedirectPromise which causes the redirect to intended page:

export default function MyApp({ Component, pageProps }) {
  return (
    <MsalProvider instance={msalInstance}>
      <PageHeader></PageHeader>
      <Component {...pageProps} />
    </MsalProvider>
  );
}

PS. To help folks searching online find this question: the behavior is triggered by navigateToLoginRequestUrl: true (is the default) in the configuration. Setting it to false plainly disables sending the user to the intended page at all.

Attempted solutions with middleware

I figured based on how APP_INITIALIZERs work in Angular, to use middleware like this at some point:

// From another file:
// export const msalInstance = new PublicClientApplication(msalConfig);

export async function middleware(_request) {
    const targetUrlAfterLoginRedirect = await msalInstance.handleRedirectPromise()
        .then((result) => {
            if (!!result && !!result.state) {
                return result.state;
            }
            return null;
        });

    console.log('Found intended target before login flow: ', targetUrlAfterLoginRedirect);

    // TODO: Send user to the intended page with router.
}

However, this logs on the server's console:

Found intended target before login flow: null

So it seems middleware is too early for msal-react to cope with? Shame, because middleware would've been perfect, to allow as much SSR for target pages as possible.

It's not an option to change the redirect URL on B2C's side, because I'll be constantly adding new routes to my app that need this behavior.

Note that I also tried to use middleware to just sniff out the state myself, but since the middleware runs on Node it won't have access to the hash fragment.

Animated GIF showing the flashing home page

Here's an animated gif that shows the /home page is briefly (200ms or so) shown before /me is properly opened. Warning, gif is a wee bit flashy so in a spoiler tag:

screen capture from a browser, of the steps in the question

Attempted solution with custom NavigationClient

I've tried adding a custom NavigationClient to more closely mimic the nextjs sample from Microsoft's repository, like this:

import { NavigationClient } from "@azure/msal-browser";

// See: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-react/docs/performance.md#how-to-configure-azuremsal-react-to-use-your-routers-navigate-function-for-client-side-navigation
export class CustomNavigationClient extends NavigationClient {
  constructor(router) {
    super();
    this.router = router;
  }

  async navigateInternal(url, options) {
    console.log(' Navigating Internal to', url);
    const relativePath = url.replace(window.location.origin, "");
    if (options.noHistory) {
      this.router.replace(relativePath);
    } else {
      this.router.push(relativePath);
    }

    return false;
  }
}

This did not solve the issue. The console.log is there allowing me to confirm this code is not run on the server, as the Node logs don't show it.

Attempted solution: go through MSAL's SSR docs

Another thing I've tried is going through the documentation claiming @azure/msal-react supports Server Side Rendering (SSR) but those docs nor the linked samples demonstrate how to solve my issue.

Attempted solution in _app.tsx

Another workaround I considered was to sniff out the hash fragment client side when the user returns to my app (and make sure the intended page is also in that state). I can successfully send the OpenID state to B2C like this...

const extendedAuthenticationRequest = {
  ...authenticationRequest,
  state: `~path~${asPath}~path~`,
};

...and see it returned in the Network tab of the dev tools.

However, when I try to extract it in my _app.tsx still doesn't work. I tried this code from another Stack Overflow answer to get the .hash:

const [isMounted, setMounted] = useState(false);

useEffect(() => {
  if (isMounted) {
    console.log('====> saw the following hash', window.location.hash);
    const matches = /~path~(.+)~path~/.exec(window.location.hash);
    if (matches && matches.length > 0 && matches[1]) {
      const targetUrlAfterOpenIdRedirect = decodeURIComponent(matches[1]);
      console.log("Routing to", targetUrlAfterOpenIdRedirect);
      router.replace(targetUrlAfterOpenIdRedirect);
    }
  } else {
    setMounted(true);
  }
}, [isMounted]);

if (!isMounted) return null;

// else: render <MsalProvider> and the intended page component

This does find the intended page from the state and executes routing, but still flashes the /home page before going to the intended page.

Footnote: related GitHub issue

Submitted an issue at MSAL's GitHub repository too.

Jeroen
  • 60,696
  • 40
  • 206
  • 339

0 Answers0