Search code examples
reactjsnext.jsmsal-reactapp-route

BrowserAuthError:uninitialized_public_client_application . Getting this error when trying SSO with MSAL for React and Next.js application


Iam creating a React application with Next.js 14 and App router. Iam trying single Sign On (SSO) using Azure Msal.

I have created my Msal.ts which has MSAL configuration which iam using it my layout.tsx which is a wrapper for my web application.

When i tried to trigger Login., i see following error

BrowserAuthError: uninitialized_public_client_application: You must call and await the initialize function before attempting to call any other MSAL API.

This is my Msal.ts code:

import {
  PublicClientApplication,
  Configuration,
  LogLevel,
} from "@azure/msal-browser";

const msalConfig: Configuration = {
  auth: {
    clientId: process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID || "default-client-id", // Azure AD client id
    authority: `https://login.microsoftonline.com/${process.env.NEXT_PUBLIC_AZURE_AD_TENANT_ID}`, // zure AD tenant id
    redirectUri: process.env.NEXT_PUBLIC_APP_REDIRECT_URL, // app redirect URL
    postLogoutRedirectUri: "/", // app logout URL
  },
  cache: {
    cacheLocation: "sessionStorage", // This configures where your cache will be stored
    storeAuthStateInCookie: false, // If you are having issues on IE11, you may need to set this to true
  },
  system: {
    loggerOptions: {
      loggerCallback: (level, message, containsPii) => {
        if (containsPii) {
          return;
        }
        switch (level) {
          case LogLevel.Error:
            console.error(message);
            return;
          case LogLevel.Info:
            console.info(message);
            return;
          case LogLevel.Verbose:
            console.debug(message);
            return;
          case LogLevel.Warning:
            console.warn(message);
            return;
        }
      },
      logLevel: LogLevel.Info,
      piiLoggingEnabled: false,
    },
  },
};

const msalInstance = new PublicClientApplication(msalConfig);

export default msalInstance;

This Msal.ts., iam importing it to my Layout.tsx:

"use client";
import React, { useEffect, useRef } from "react";
import "./globals.css";
import Header from "./components/Header";
import { usePathname, useRouter } from "next/navigation";
import { Box, Grid } from "@mui/material";
import { AppStore, makeStore, persistor } from "@/lib/store";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { persistStore } from "redux-persist";
import Leftsidebar from "./components/Leftsidebar";
import msalInstance from "@/lib/Msal";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  const isAuthenticated = async () => {
    const accounts = await msalInstance.getAllAccounts();
    console.log("accounts", accounts);
    return accounts.length > 0;
  };

  const router = useRouter();
  const pathName = usePathname();

  useEffect(() => {
    const checkAuth = async () => {
      const auth = await isAuthenticated();
      // restrict unauthorized users to access inner pages
      if (!auth && pathName !== "/") {
        router.push("/");
      }
      // restrict authorized users to signout before login again
      if (auth && pathName === "/") {
        router.push("/dashboard");
      }
    };
    checkAuth();
  }, [pathName, msalInstance]);

  const storeRef = useRef<AppStore>();
  if (!storeRef.current) {
    storeRef.current = makeStore();
  }
  return (
    <html lang="en">
      <body>
        <Provider store={storeRef.current}>
          <PersistGate loading={null} persistor={storeRef.current.__persistor}>
            <Box sx={{ display: "flex" }}>
              <Grid container sx={{ backgroundColor: "#F8F8F9" }}>
                <Grid item xs={true}>
                {children}
                </Grid>
              </Grid>
            </Box>
          </PersistGate>
        </Provider>
      </body>
    </html>
  );
}

This is my handleLoginClick functionality:

const handleLoginClick = async () => {
  // msalInstance.loginRedirect();
  try {
    const loginRequest = {
      scopes: ["openId", "profile", "User.Read"],
    };
    await msalInstance.loginRedirect(loginRequest);
    const accountInfo = msalInstance.getAllAccounts()[0];
    if (accountInfo) {
      console.log("accountInfo", accountInfo);
      const stateData = {
        message: "Dashboard page",
        loggedinUser: accountInfo,
      };
      dispatch(setDashboardData(stateData));
      router.push("/dashboard");
    }
  } catch (error) {
    console.error(error);
  }
  
};

I believe this is issue with asynchronous behaviour with Next.js. Not sure where to make changes for it to work. Can you help me with what is wrong in this code?


Solution

  • This indeed is asynchronous behaviours issue. in your layout.tsx., you need to initialize msalInstance before calling it. This will ensure msalInstance is ready.

    Here is the updated code:

    const isAuthenticated = async () => {
        await msalInstance.initialize(); // Add this line
        const accounts = await msalInstance.getAllAccounts();
        console.log("accounts", accounts);
        return accounts.length > 0;
      };