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
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 :
src/components/Auth/SignIn.js :
src/config/authConfig.js :
I added the below URL in Azure AD under Authentication URIs as a Single-Page Application.
Output :
I successfully signed in and signed out as shown below.
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
andloginPopup
. 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 usingPublicRoute
andPrivateRoute
components for protected routes, which redirected users toyour_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
andwindow.location.pathname
to check the authentication state correctly.Original
PrivateRoute
Component:Updated
PrivateRoute
Component: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!