microsoft-authentication-library-for-js icon indicating copy to clipboard operation
microsoft-authentication-library-for-js copied to clipboard

Race condition in cache initialization causes intermittent token claim loss under slower conditions after migration to v4

Open ocindev opened this issue 10 months ago • 5 comments

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

  1. Cache initialization, including all cryptographic operations (base64 decode, HKDF generation, decryption), completes fully
  2. Only after initialization is complete should any cache access attempts be allowed
  3. 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]

ocindev avatar Feb 06 '25 23:02 ocindev

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.

tnorling avatar Feb 07 '25 00:02 tnorling

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.

chrome-trace-logs.log

ocindev avatar Feb 07 '25 08:02 ocindev

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.

  1. Just add BrowserCacheLocation.LocalStorage as cacheLocation to the cache configuration to authConfig.js.
cache: {
        cacheLocation: BrowserCacheLocation.LocalStorage,
 },
  1. Use the production build of the Next.js sample.
  2. Use chrome to open the MSAL-React Next.js sample
  3. Login using the redirect option
  4. Open the browser dev tools and check the local storage for the msal.token.keys.<uuid> entry.
  5. The idToken value should contain a reference to the local storage idToken entry
  6. Go to the Performance tab and enable 4x CPU slowdown
  7. Refresh the page using CMD + SHIFT + R
  8. Check the msal.token.keys.<uuid> entry in the local storage again
  9. 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.

ocindev avatar Feb 11 '25 21:02 ocindev

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..

ocindev avatar Mar 12 '25 08:03 ocindev

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 avatar Apr 01 '25 13:04 ocindev

@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 avatar Apr 28 '25 20:04 tnorling

@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?

loucadufault avatar Jul 11 '25 16:07 loucadufault