skip to Main Content

I am failing to understand the error that I am getting while trying to authenticate a user with the Micrsoft Authentication library for React (@azure/msal-react). I need help understanding why it fails when attempting to sign in a user using the loginPopup method.
Code :

import { useEffect, useState, useContext } from "react";
import { useNavigate } from "react-router-dom";
import { Link } from "react-router-dom";
import Card from "@mui/material/Card";
import MDBox from "components/MDBox";
import MDTypography from "components/MDTypography";
import MDButton from "components/MDButton";
import CoverLayout from "layouts/authentication/components/CoverLayout";
import { useMsal } from "@azure/msal-react";
import { loginRequest, groupId } from "authConfig";
import bgImage from "assets/images/btbbackground.jpg";
import { GlobalStateContext } from "GlobalStateContext";
import {userLoginResponse} from "./loginResponse";

function Basic() {
  const navigate = useNavigate();
  const { instance } = useMsal();
  const [rememberMe, setRememberMe] = useState(false);
  const { updateStateObj } = useContext(GlobalStateContext);

  const handleSetRememberMe = () => setRememberMe(!rememberMe);

  const handleSignIn = async () => {
    try {
      let loginResponse = await instance.loginPopup(loginRequest);
      // let loginResponse = userLoginResponse;
      console.log('loginResponse from handleSignIn is : ', loginResponse);
      if (loginResponse) {
        handleSuccessfulLogin(loginResponse);
      }
    } catch (error) {
      console.error('Error during authentication', error);
    }
  };

  const handleSuccessfulLogin = (loginResponse) => {
    const group_list = loginResponse.idTokenClaims.groups || [];
    console.log('Group list', group_list);

    if (group_list.length > 0) {
      const IsCallAssuranceAgent = group_list.includes(groupId.AICallAssuranceAgent);
      const IsCallAssuranceMgr = group_list.includes(groupId.AICallAssuranceMgr);
      const IsCallSummary = group_list.includes(groupId.AICallSummary);

      const stateItem = {
        agentId: loginResponse.account.name,
        email: loginResponse.account.username,
        AICallAssuranceAgent: IsCallAssuranceAgent,
        AICallAssuranceMgr: IsCallAssuranceMgr,
        AICallSummary: IsCallSummary,
        groupId: group_list.join()
      };

      // Update global state
      updateStateObj(stateItem);

      // Navigate based on roles
      if (IsCallSummary) {
        navigate("/callsummary");
      } else if (IsCallAssuranceAgent || IsCallAssuranceMgr) {
        navigate("/callassurance");
      }
    } else {
      navigate("/unauthorized");
    }
  };

  useEffect(() => {
    console.log('in instance use effect , ');
    instance.handleRedirectPromise()
      .then((response) => {
        if (response) {
          console.log('Login response from handleRedirectPromise', response);
          handleSuccessfulLogin(response);
        }
      })
      .catch((error) => {
        console.error('Error handling redirect response', error);
      });
  }, [instance]);

AuthConfir.js:
hash_empty_error

Error details:

authConfig.js:19  [Sun, 06 Oct 2024 17:36:03 GMT] : [019262e6-c629-7327-96d7-5a73bd4f352b] : [email protected] : Error - The request has returned to the redirectUri but a fragment is not present. It's likely that the fragment has been removed or the page has been redirected by code running on the redirectUri page.
loggerCallback @ authConfig.js:19
executeCallback @ Logger.ts:152
logMessage @ Logger.ts:136
error @ Logger.ts:160
NK @ ResponseHandler.ts:29
(anonymous) @ FunctionWrappers.ts:43
acquireTokenPopupAsync @ PopupClient.ts:279
await in acquireTokenPopupAsync
acquireToken @ PopupClient.ts:120
acquireTokenPopup @ StandardController.ts:795
loginPopup @ StandardController.ts:1869
loginPopup @ PublicClientApplication.ts:279
onClick @ index.js:25
qe @ react-dom.production.min.js:52
Ye @ react-dom.production.min.js:52
(anonymous) @ react-dom.production.min.js:53
Ir @ react-dom.production.min.js:100
Er @ react-dom.production.min.js:101
(anonymous) @ react-dom.production.min.js:113
De @ react-dom.production.min.js:292
(anonymous) @ react-dom.production.min.js:50
Nr @ react-dom.production.min.js:105
Xt @ react-dom.production.min.js:75
Jt @ react-dom.production.min.js:74
t.unstable_runWithPriority @ scheduler.production.min.js:18
Ko @ react-dom.production.min.js:122
_e @ react-dom.production.min.js:292
Qt @ react-dom.production.min.js:73
Show 25 more frames
Show less
authConfig.js:20 [Sun, 06 Oct 2024 17:36:03 GMT] : [] : @azure/[email protected] : Info - Emitting event: msal:loginFailure
authConfig.js:20 [Sun, 06 Oct 2024 17:36:03 GMT] : [] : @azure/[email protected] : Info - MsalProvider - msal:loginFailure results in setting inProgress from login to none
index.js:32  Error during authentication BrowserAuthError: hash_empty_error: Hash value cannot be processed because it is empty. Please verify that your redirectUri is not clearing the hash. For more visit: aka.ms/msaljs/browser-errors
    at jW (BrowserAuthError.ts:359:12)
    at NK (ResponseHandler.ts:32:19)
    at FunctionWrappers.ts:43:28
    at DK.acquireTokenPopupAsync (PopupClient.ts:279:34)
    at async onClick (index.js:25:11)

Can some one please check and help me to resolve this error.

I need some suggestions in my code to fix the above error.

Routing configuration:
app.js

import React, { useState, useEffect, useContext } from "react";
import { Routes, Route, Navigate, useLocation, useNavigate } from "react-router-dom";
import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import Icon from "@mui/material/Icon";
import MDBox from "components/MDBox";
import Sidenav from "examples/Sidenav";
import Configurator from "examples/Configurator";
import theme from "assets/theme";
import themeDark from "assets/theme-dark";
import CallassuranceComponent from "layouts/callassurance";
import CallsummaryComponent from "layouts/callsummary";
import SignIn from "layouts/authentication/sign-in"; // Import the SignIn component
import { useMaterialUIController, setMiniSidenav, setOpenConfigurator } from "context";
import { PublicClientApplication, EventType, InteractionStatus } from "@azure/msal-browser";
import { MsalProvider, useIsAuthenticated, useMsal } from "@azure/msal-react";
import routes from "routes";
import { msalConfig } from "./authConfig";
// Images
import brandWhite from "assets/images/logos/BT_logo.png";
import brandDark from "assets/images/logos/BT_logo.png";
import { GlobalStateContext } from 'GlobalStateContext';

const pca = new PublicClientApplication(msalConfig);

function PrivateRoute({ children }) {
  const { inProgress } = useMsal();
  const isAuthenticated = useIsAuthenticated();
  const { stateObj } = useContext(GlobalStateContext);
  if (inProgress === InteractionStatus.Startup) {
    // MSAL is still checking for cached tokens, so show a loading state
    return <div>Loading...</div>;
  }
  // Check if the user is authenticated and stateObj is not null and contains some values
  if (!isAuthenticated || !stateObj || Object.keys(stateObj).length === 0) {
    // Redirect to sign-in page if not authenticated or stateObj is empty
    return <Navigate to="/authentication/sign-in" />;
  }
  // If everything is valid, render the route's children
  return children;
}


function App() {
  const location = useLocation();
  const navigate = useNavigate();
  const { email } = location.state || {};
  const [controller, dispatch] = useMaterialUIController();
  const {
    miniSidenav,
    direction,
    layout,
    openConfigurator,
    sidenavColor,
    transparentSidenav,
    whiteSidenav,
    darkMode,
  } = controller;
  const [onMouseEnter, setOnMouseEnter] = useState(false);
  const { pathname } = useLocation();
  const [allowedPages, setAllowedPages] = useState(["sign-in", "sign-up", "report", "authentication/sign-in", "unauthorized"]);

  const { instance, accounts, inProgress } = useMsal();
  const isAuthenticated = useIsAuthenticated();
  const { stateObj } = useContext(GlobalStateContext);

  useEffect(() => {
    const fetchAllowedPages = async () => {
      const allRouteKeys = routes.map(route => route.key);
      setAllowedPages(allRouteKeys);
    };

    fetchAllowedPages();
  }, []);

  useEffect(() => {
    document.body.setAttribute("dir", direction);
  }, [direction]);

  useEffect(() => {
    if (stateObj) {
      const { AICallAssuranceAgent, AICallAssuranceMgr, AICallSummary } = stateObj;
      var allRouteKeys = AICallSummary ? allowedPages : allowedPages.filter(route => route !== 'callsummary');
      allRouteKeys = AICallAssuranceAgent || AICallAssuranceMgr ? allRouteKeys : allRouteKeys.filter(route => route !== 'callassurance');
      // Check if allowedPages needs to be updated
      if (JSON.stringify(allRouteKeys) !== JSON.stringify(allowedPages)) {
        setAllowedPages(allRouteKeys);
      }
    }
    document.documentElement.scrollTop = 0;
    document.scrollingElement.scrollTop = 0;
  }, [stateObj, allowedPages]);

  useEffect(() => {
    navigate(location.pathname);
  }, [inProgress, accounts, navigate, location.pathname]);

  useEffect(() => {
    const handleMsalEvent = (event) => {
      if (event.eventType === EventType.LOGIN_SUCCESS) {
        instance.setActiveAccount(event.payload.account);
      }
    };

    const callbackId = instance.addEventCallback(handleMsalEvent);

    return () => {
      if (callbackId) {
        instance.removeEventCallback(callbackId);
      }
    };
  }, [instance]);

  const handleOnMouseEnter = () => {
    if (miniSidenav && !onMouseEnter) {
      setMiniSidenav(dispatch, false);
      setOnMouseEnter(true);
    }
  };

  const handleOnMouseLeave = () => {
    if (onMouseEnter) {
      setMiniSidenav(dispatch, true);
      setOnMouseEnter(false);
    }
  };

  const handleConfiguratorOpen = () => setOpenConfigurator(dispatch, !openConfigurator);

  const filteredRoutes = routes.filter(route => allowedPages.includes(route.key));

  const configsButton = (
    <MDBox
      display="flex"
      justifyContent="center"
      alignItems="center"
      width="3.25rem"
      height="3.25rem"
      bgColor="white"
      shadow="sm"
      borderRadius="50%"
      position="fixed"
      right="2rem"
      bottom="2rem"
      zIndex={99}
      color="red"
      sx={{ cursor: "pointer" }}
      onClick={handleConfiguratorOpen}
    >
      <Icon fontSize="small" color="inherit">
        settings
      </Icon>
    </MDBox>
  );

  return (
    <MsalProvider instance={pca}>
      <ThemeProvider theme={darkMode ? themeDark : theme}>
        <CssBaseline />
        {layout === "dashboard" && (
          <>
            <Sidenav
              allowedpages={allowedPages}
              color={sidenavColor}
              brand={(transparentSidenav && !darkMode) || whiteSidenav ? brandDark : brandWhite}
              brandName="AI For Agents"
              routes={filteredRoutes}
              onMouseEnter={handleOnMouseEnter}
              onMouseLeave={handleOnMouseLeave}
            />
            <Configurator />
          </>
        )}
        {layout === "vr" && <Configurator />}
        <Routes>
          <Route path="/authentication/sign-in" element={<SignIn />} />
          {filteredRoutes.map((route) => (
            <Route
              exact
              path={route.route}
              element={
                <PrivateRoute>
                  {route.component}
                </PrivateRoute>
              }
              key={route.key}
            />
          ))}
          <Route path="*" element={<Navigate to="/callsummary" />} />
        </Routes>
      </ThemeProvider>
    </MsalProvider>
  );
}

export default App;

2

Answers


  1. I successfully authenticated a user with the Microsoft Authentication Library (@azure/msal-react) in the following React application.

    Here is the complete code from the GitHub repository.

    src/App.js :

    import React, { useEffect, useState, useContext } from "react";
    import { Routes, Route, Navigate, useLocation, useNavigate } from "react-router-dom";
    import { ThemeProvider } from "@mui/material/styles";
    import CssBaseline from "@mui/material/CssBaseline";
    import Icon from "@mui/material/Icon";
    import MDBox from "./components/UI/MDBox";
    import theme from "./styles/theme";
    import themeDark from "./styles/themeDark";
    import SignIn from "./components/Auth/SignIn";
    import PrivateRoute from "./components/Layout/PrivateRoute";
    import routes from "./routes/routes";
    import { msalConfig } from "./config/authConfig";
    import { MsalProvider, useMsal, useIsAuthenticated } from "@azure/msal-react";
    import { PublicClientApplication, EventType } from "@azure/msal-browser";
    import { GlobalStateProvider, GlobalStateContext } from "./contexts/GlobalStateContext";
    
    const pca = new PublicClientApplication(msalConfig);
    
    function AppContent() {
      const location = useLocation();
      const navigate = useNavigate();
      const [controller, setController] = useState({
        miniSidenav: false,
        direction: "ltr",
        layout: "dashboard",
        openConfigurator: false,
        sidenavColor: "blue",
        transparentSidenav: false,
        whiteSidenav: false,
        darkMode: false,
      });
      const {
        miniSidenav,
        direction,
        layout,
        openConfigurator,
        sidenavColor,
        transparentSidenav,
        whiteSidenav,
        darkMode,
      } = controller;
      const [onMouseEnter, setOnMouseEnter] = useState(false);
      const [allowedPages, setAllowedPages] = useState(["sign-in", "sign-up", "report", "authentication/sign-in", "unauthorized"]);
      const { instance, accounts, inProgress } = useMsal();
      const isAuthenticated = useIsAuthenticated();
      const { stateObj } = useContext(GlobalStateContext);
    
      useEffect(() => {
        const fetchAllowedPages = async () => {
          const allRouteKeys = routes.map((route) => route.key);
          setAllowedPages(allRouteKeys);
        };
        fetchAllowedPages();
      }, []);
      useEffect(() => {
        document.body.setAttribute("dir", direction);
      }, [direction]);
      useEffect(() => {
        if (stateObj) {
          const { AICallAssuranceAgent, AICallAssuranceMgr, AICallSummary } = stateObj;
          let allRouteKeys = AICallSummary ? allowedPages : allowedPages.filter((route) => route !== "callsummary");
          allRouteKeys = AICallAssuranceAgent || AICallAssuranceMgr ? allRouteKeys : allRouteKeys.filter((route) => route !== "callassurance");
          if (JSON.stringify(allRouteKeys) !== JSON.stringify(allowedPages)) {
            setAllowedPages(allRouteKeys);
          }
        }
        document.documentElement.scrollTop = 0;
        document.scrollingElement.scrollTop = 0;
      }, [stateObj, allowedPages]);
    
      useEffect(() => {
        navigate(location.pathname);
      }, [inProgress, accounts, navigate, location.pathname]);
      useEffect(() => {
        const handleMsalEvent = (event) => {
          if (event.eventType === EventType.LOGIN_SUCCESS) {
            instance.setActiveAccount(event.payload.account);
          }
        };
        const callbackId = instance.addEventCallback(handleMsalEvent);
        return () => {
          if (callbackId) {
            instance.removeEventCallback(callbackId);
          }
        };
      }, [instance]);
      const handleOnMouseEnter = () => {
        if (miniSidenav && !onMouseEnter) {
          setController((prev) => ({ ...prev, miniSidenav: false }));
          setOnMouseEnter(true);
        }
      };
      const handleOnMouseLeave = () => {
        if (onMouseEnter) {
          setController((prev) => ({ ...prev, miniSidenav: true }));
          setOnMouseEnter(false);
        }
      };
      const handleConfiguratorOpen = () => {
        setController((prev) => ({ ...prev, openConfigurator: !prev.openConfigurator }));
      };
      const filteredRoutes = routes.filter((route) => allowedPages.includes(route.key));
      const configsButton = (
        <MDBox
          display="flex"
          justifyContent="center"
          alignItems="center"
          width="3.25rem"
          height="3.25rem"
          sx={{ backgroundColor: "white" }}
          shadow="sm"
          borderRadius="50%"
          position="fixed"
          right="2rem"
          bottom="2rem"
          zIndex={99}
          color="red"
          onClick={handleConfiguratorOpen}
        >
          <Icon fontSize="small" color="inherit">
            settings
          </Icon>
        </MDBox>
      );
      return (
        <ThemeProvider theme={darkMode ? themeDark : theme}>
          <CssBaseline />
          {}
          {configsButton}
          <Routes>
            <Route path="/authentication/sign-in" element={<SignIn />} />
            {filteredRoutes.map((route) => (
              <Route
                key={route.key}
                path={route.route}
                element={
                  <PrivateRoute>
                    {route.component}
                  </PrivateRoute>
                }
              />
            ))}
            <Route path="*" element={<Navigate to="/callsummary" />} />
          </Routes>
        </ThemeProvider>
      );
    }
    function App() {
      return (
        <MsalProvider instance={pca}>
          <GlobalStateProvider>
            <AppContent />
          </GlobalStateProvider>
        </MsalProvider>
      );
    }
    export default App;
    

    src/components/Auth/SignIn.js :

    import React from 'react';
    import { useMsal } from '@azure/msal-react';
    import MDBox from '../UI/MDBox'; 
    import { Button, TextField } from '@mui/material';
    
    const SignIn = () => {
        const { instance } = useMsal();
        const handleLogout = () => {
            instance.logout();
        };
        const handleSignIn = (event) => {
            event.preventDefault();
            instance.loginPopup().catch((error) => {
                console.error("Login failed: ", error);
            });
        };
        return (
            <MDBox>
                <form onSubmit={handleSignIn}>
                    <Button type="submit" variant="contained" color="primary">
                        Sign In
                    </Button>
                    <Button onClick={handleLogout} variant="outlined" color="secondary">
                        Logout
                    </Button>
                </form>
            </MDBox>
        );
    };
    export default SignIn;
    

    src/config/authConfig.js :

    import { LogLevel } from "@azure/msal-browser";
    
    export const msalConfig = {
      auth: {
        clientId: "<clientID>", 
        authority: "https://login.microsoftonline.com/<tenantID>", 
        redirectUri: "http://localhost:3000", 
        postLogoutRedirectUri: "http://localhost:3000",
      },
      cache: {
        cacheLocation: "localStorage", 
        storeAuthStateInCookie: false, 
      },
      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;
              default:
                return;
            }
          },
          logLevel: LogLevel.Info,
          piiLoggingEnabled: false,
        },
      },
    };
    export const loginRequest = {
      scopes: ["User.Read"],
    };
    

    I added the below URL in Azure AD under Authentication URIs as a Single-Page Application.

    http://localhost:3000
    

    enter image description here

    Output :

    I successfully signed in and signed out as shown below.

    http://localhost:3000/authentication/sign-in
    

    enter image description here

    enter image description here

    enter image description here

    Login or Signup to reply.
  2. Title: Hash Empty Error with MSAL React Authentication in Mobile Browsers

    Tags: react, msal, azure, authentication, mobile-browsers


    Body:

    I’m using the Microsoft Authentication Library (MSAL) with a React Single Page Application (SPA) configured for Azure Entra ID, following the official documentation here.

    I have set up two authentication methods: loginRedirect and loginPopup. While the desktop browser login works reliably, I’m facing unpredictable login issues on mobile browsers. Sometimes, users can log in successfully, but often they encounter problems.

    Issue:

    I discovered that the root cause was a hash_empty_error occurring on mobile. My routing logic involved using PublicRoute and PrivateRoute components for protected routes, which redirected users to your_frontend_url+'/login' when not authorized. This routing setup was inadvertently clearing my hash value, leading to session storage being cleared and users becoming unauthorized after several login attempts.

    To resolve this, I started using window.location.hash and window.location.pathname to check the authentication state correctly.

    Original PrivateRoute Component:

    import { AuthenticatedTemplate, useMsal } from "@azure/msal-react";
    import PropTypes from "prop-types";
    import { Navigate } from "react-router-dom";
    
    export const PrivateRoute = ({ element }) => {
      const { accounts } = useMsal();
      const isAuthenticated = accounts && accounts.length > 0;
    
      if (isAuthenticated) {
        return <AuthenticatedTemplate>{element}</AuthenticatedTemplate>;
      } else {
        return <Navigate to="/login" />;
      }
    };
    
    PrivateRoute.propTypes = {
      element: PropTypes.element.isRequired,
    };
    

    Updated PrivateRoute Component:

    import Loading from "@assets/Loading";
    import { AuthenticatedTemplate, useMsal } from "@azure/msal-react";
    import PropTypes from "prop-types";
    import { Navigate } from "react-router-dom";
    
    export const PrivateRoute = ({ element }) => {
      const { accounts, inProgress } = useMsal();
      const isAuthenticated = accounts && accounts.length > 0;
      const hashValue = window.location.hash;
    
      if (inProgress === "login" || inProgress === "handleRedirect") {
        return (
          <div className="h-[100dvh] w-full flex flex-col justify-center items-center gap-4">
            <Loading height={10} />
            <p>Login in progress ...</p>
          </div>
        );
      }
    
      if (isAuthenticated || (window.location.pathname === "/" && hashValue !== "")) {
        return <AuthenticatedTemplate>{element}</AuthenticatedTemplate>;
      } else {
        return <Navigate to="/login" />;
      }
    };
    
    PrivateRoute.propTypes = {
      element: PropTypes.element.isRequired,
    };
    

    Question:

    While this update seems to mitigate the issue, I would like to know if there are any best practices or alternative approaches to handle this type of authentication flow more reliably, especially for mobile browsers. Any insights or experiences would be greatly appreciated!

    Did this solution help you? If so, please consider giving it an upvote to support our community!

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search