Race condition in cache initialization causes intermittent token claim loss under slower conditions after migration to v4
Core Library
MSAL.js (@azure/msal-browser)
Core Library Version
4.2.0
Wrapper Library
MSAL React (@azure/msal-react)
Wrapper Library Version
3.0.4
Public or Confidential Client?
Public
Description
I may have identified a significant race condition in MSAL's cache initialization process that leads to intermittent loss of token claims, particularly under slower processing conditions. The issue manifests when the cache access attempts occur before the initialization process (including decryption) completes fully. This timing-sensitive behavior causes authentication problems that are especially noticeable in Firefox and under constrained CPU conditions.
Error Message
No response
MSAL Logs
Trace Level Logs: chrome-msal-issue.log chrome-for-testing-4x-throttle-issue.log firefox-msal-issue.txt
Network Trace (Preferrably Fiddler)
- [ ] Sent
- [ ] Pending
MSAL Configuration
auth: {
clientId: publicRuntimeConfig.azureAdClientId,
authority: publicRuntimeConfig.azureAdAuthority,
navigateToLoginRequestUrl: false,
protocolMode: ProtocolMode.OIDC,
},
cache: {
cacheLocation: BrowserCacheLocation.LocalStorage,
secureCookies: true,
},
system: {
loggerOptions: {
logLevel: LogLevel.Trace,
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:
console.log(message);
return;
}
}
}
}
Relevant Code Snippets
Reproduction Steps
Next.js 14 Sample
Steps to reproduce are listed as part of its README.md
Expected Behavior
- Cache initialization, including all cryptographic operations (base64 decode, HKDF generation, decryption), completes fully
- Only after initialization is complete should any cache access attempts be allowed
- Token claims should be consistently available after successful authentication, regardless of browser or system performance
Identity Provider
Entra ID (formerly Azure AD) / MSA
Browsers Affected (Select all that apply)
Firefox, Chrome
Regression
@azure/[email protected] @azure/[email protected]
As per our initialization docs: "The initialize function is asynchronous and must resolve before invoking other MSAL.js APIs."
Additionally the docs for the useMsal hook specifically mention not relying on claims returned from this object: "Note: The accounts value returned by useMsal will only update when accounts are added or removed, and will not update when claims are updated. If you need access to updated claims for the current user, use the useAccount hook or call acquireTokenSilent instead."
Please use the useAccount hook for account info and ensure the inProgress state is "None" prior to invoking any MSAL APIs.
Thanks for the quick response @tnorling. I adapted my linked github example accordingly:
import {useAccount} from "@azure/msal-react";
import {handleLogout, handleMsalSilentLogin} from "@/auth/msal";
export default function RootPage() {
const account = useAccount();
const handleFetchTokenSilently = async () => {
const result = await handleMsalSilentLogin();
console.log('##### Result of silent login', result);
}
return (
<div className="items-center gap-y-2">
<h2>Active Account</h2>
<div className="justify-center">
<span>{JSON.stringify(account, null, 2)}</span>
</div>
<div className="flex flex-col items-center gap-y-2">
<button className="btn btn-primary" onClick={() => handleLogout()}>Logout</button>
<button className="btn btn-primary" onClick={() => handleFetchTokenSilently()}>Fetch token silently</button>
</div>
</div>
);
}
import {Inter} from "next/font/google";
import {MsalAuthenticationTemplate, useMsal} from "@azure/msal-react";
import {InteractionStatus, InteractionType} from "@azure/msal-browser";
import {msalRedirectRequest} from "@/auth/msal";
import RootPage from "@/components/root";
const Loading = () => <div>Loading...</div>
const Error = () => <div>Error...</div>
function Home() {
const redirectRequest = msalRedirectRequest();
const {inProgress} = useMsal();
if (inProgress !== InteractionStatus.None) {
return <div>Loading... current status {inProgress}</div>;
}
return (
<main className="container mx-auto w-full">
<MsalAuthenticationTemplate
interactionType={InteractionType.Redirect}
authenticationRequest={redirectRequest}
loadingComponent={Loading}
errorComponent={Error}
>
<RootPage />
</MsalAuthenticationTemplate>
</main>
);
}
Home.getInitialProps = () => {
return {}
}
export default Home;
The useAccount MSAL api invocation should now only occur after the inProgress state is InteractionStatus.None.
Though i am still able to reproduce this issue.. A manual use of the acquireTokenSilent API fails with the following error:
_app-174f937c57afd3ff.js:1 Uncaught (in promise) BrowserAuthError: no_account_error: No account object provided to acquireTokenSilent and no active account has been set. Please call setActiveAccount or provide an account on the request.
at tT (_app-174f937c57afd3ff.js:1:46136)
at nb.acquireTokenSilent (_app-174f937c57afd3ff.js:7:111675)
at nO.acquireTokenSilent (_app-174f937c57afd3ff.js:7:117274)
at u (index-e92370aaba9b696e.js:1:850)
at e (index-e92370aaba9b696e.js:1:1136)
at onClick (index-e92370aaba9b696e.js:1:1587)
at Object.eU (framework-ecc4130bc7a58a64.js:9:14908)
at eH (framework-ecc4130bc7a58a64.js:9:15062)
at framework-ecc4130bc7a58a64.js:9:33368
at re (framework-ecc4130bc7a58a64.js:9:33467)
Still the same steps to reproduce as before + click the Fetch token silently button after the page refresh to trigger the error above.
I also noticed that the event callback for the ACQUIRE_TOKEN_SUCESS event is missing the IdToken after the page refresh as well.
As there is still the Needs: Author Feedback label assigned, is there anything you need from my side ?
In the meantime i was playing around a bit more and was even able to reproduce it with the msal-react nextjs sample provided in this repo.
- Just add
BrowserCacheLocation.LocalStorageascacheLocationto the cache configuration toauthConfig.js.
cache: {
cacheLocation: BrowserCacheLocation.LocalStorage,
},
- Use the production build of the Next.js sample.
- Use chrome to open the MSAL-React Next.js sample
- Login using the redirect option
- Open the browser dev tools and check the local storage for the
msal.token.keys.<uuid>entry. - The idToken value should contain a reference to the local storage idToken entry
- Go to the Performance tab and enable 4x CPU slowdown
- Refresh the page using CMD + SHIFT + R
- Check the
msal.token.keys.<uuid>entry in the local storage again - The reference value for the idToken is gone
From here msal is unable to recover on it's own. Removing the CPU slowdown refreshing the page or even using acquireTokenRedirect does not help. Only a complete logout solves the issue.
Based on that i am pretty sure that not the way we integrated msal cause the issues but a problem directly emerging from the encryption introduced with msal v4.
In case you need anything else, feel free to reach out. I am happy to help wherever i can.
Updated the example msal dependencies to the latest versions
"@azure/msal-browser": "4.7.0",
"@azure/msal-react": "3.0.6"
Unfortunetely the issue still persists..
Hi @tnorling , sorry for the bump. i once again updated the example to the latest versions:
"@azure/msal-browser": "4.8.0",
"@azure/msal-react": "3.0.7",
But the issue in a CPU limited environment still occurs. I was able to manually recover from that invalid state by calling the silent login once and fallback to redirectLogin in case of an error. But that requires a manual user interaction.
const handleReAuthentication = async (): Promise<AuthenticationResult|void> => {
try {
return await handleMsalSilentLogin();
} catch (error: any) {
console.error('Failed to silent login, falling back to redirect.', error);
await handleMsalRedirectLogin();
}
}
To be honest this cant be intentional behaviour at all... And i don't see anything wrong with the way i integrated msal.
@ocindev I appreciate the details here. I've just merged a fix for this, please expect it to be included in our next release and reach out if you continue to have problems.
@tnorling
As per our initialization docs: "The initialize function is asynchronous and must resolve before invoking other MSAL.js APIs."
The getting started doc shows a usage where the PCA is instantiated but does not call the .initialize() method: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-react/docs/getting-started.md#initialization. Is there any reason for this? Does the MsalProvider component take care of awaiting this call?
My guess is that it probably is called, but does not await the call before rendering the component tree. So it's probably best to explicitly await the call to .initialize() before rendering the app to synchronize before beginning render of components (that may begin MSAL interactions).
Can you confirm?