Closed timburgess closed 1 year ago
@timburgess So token acquisition works but only the component update
fails?
Yes, the addTokenToContext
acquires a token and we can use that successfully with the API
@timburgess Is it possible this can be an issue with urql
? We found some similar issues closed on their end, one example here. cc @tnorling
@timburgess This issue has been automatically marked as stale because it is marked as requiring author feedback but has not had any activity for 5 days. If your issue has been resolved please let us know by closing the issue. If your issue has not been resolved please leave a comment to keep this open. It will be closed automatically in 7 days if it remains stale.
See urql #3088
This issue requires attention from the MSAL.js team and has not seen activity in 5 days. @sameerag please follow up.
@timburgess Closing this and please check the above comments for the context. Please let us know here if something else is needed fro. MSAL JS team.
Hello,
Me and my team have been trying to integrate MSAL in a React + Relay application and are getting a similar behaviour. I've looked at the issue in urql mentioned before in this thread, but they never actually fixed the problem. They simply stated that the problem was with MSAL, and they added a patch on their side to silence the error.
In your case, it's likely caused by some kind of synchronisation that happens between msalInstance.acquireTokenSilent and MsalProvider, but it's hard to tell.
Hence, the PR I've linked (https://github.com/urql-graphql/urql/pull/3095) silences these warnings. It doesn't address the root cause that often causes them because that's out of our control...
Per our investigation, this error happens every time acquireTokenSilent
falls into a scenario were it triggers an event which is handled by the MsalProvider.
In a Relay application, the call is done in the fetch function provided to the Network.create
factory in the RelayEnvironement
module.
const fetchFn: FetchFunction = async (request, variables) => {
const accessToken = await getAccessToken();
const resp = await fetch(`${config.API_URL}/graphql`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(accessToken != null ? { Authorization: `bearer ${accessToken}` } : {}),
},
body: JSON.stringify({
query: request.text, // <-- The GraphQL document composed by Relay
variables,
}),
});
return await resp.json();
};
async function getAccessToken(): Promise<string | null> {
// The same instance given to the MsalProvider.
const accounts = msalInstance.getAllAccounts();
if (accounts) {
const account = accounts[0];
const tokenRequest = { ...apiRequest, account };
const { accessToken } = (await msalInstance.acquireTokenSilent(tokenRequest)) || {};
return accessToken;
}
return null;
}
While the issue was previously closed as being on the urql side, seeing this in Relay as well makes me think there might be an architectural issue in Msal that enables this issue.
@sameerag Care to give your opinion on this?
For anyone coming late like me, I had the same issue with React + MSAL + Relay. After a lot of trial and error I realized the simplest way to fix this was to simply make sure none of the Relay queries happen in the render cycle by moving them into useEffect. Here's a custom hook that illustrates one of the changes involved.
export const usePreloadedQueryOnce = <T extends OperationType>(
query: GraphQLTaggedNode,
initialVariables?: VariablesOf<T>,
options?: UseQueryLoaderLoadQueryOptions,
): PreloadedQuery<T> | null | undefined => {
const [queryRef, loadQuery] = useQueryLoader<T>(query);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
if (!isLoaded) {
loadQuery(initialVariables || {}, options);
setIsLoaded(true);
}
}, [isLoaded, loadQuery, initialVariables, options]);
return queryRef;
};
Interesting. FWIW we migrated away from urql
to the Apollo client and all the MsalProvider issues went away.
@eberridge This isn't really a solution. Since you cannot use useLazyLoadQuery anymore, and can't load content on first render, you'll have a hard time using fetchOptions that provides data immediately. You'll always flicker with your loading state for at least a frame.
Closing as it's not clear this is an issue with MSAL. If anyone disagrees please feel free to open a new issue thanks!
After some investigations, I have found a better workaround for relay applications like mine and @eberridge.
The fix is the ensure that the call to acquireTokenSilent never happen during the react render phase. Here's a version of the Relay project setup sample modified with the fix.
const fetchFn: FetchFunction = async (request, variables) => {
await Promise.resolve(); //< This call make sure we're now running in a different MicroTask.
const accessToken = // It is safe to call acquireTokenSilent() here.
const resp = await fetch(`${config.API_URL}/graphql`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Accept-Language': resolvedLanguage(i18n),
'Content-Type': 'application/json',
...(accessToken != null ? { Authorization: `bearer ${accessToken}` } : {})
},
body: JSON.stringify({
query: request.text, // <-- The GraphQL document composed by Relay
variables,
}),
});
return await resp.json();
};
Oh wow @kawazoe -- I can't tell you how many hours I've spent fighting this exact same issue on different projects where I was using msal-react! Thank you!! Adding await Promise.resolve();
is exactly what's needed. I'm guessing the underlying issue is that acquireTokenSilent()
completes synchronously for the typical case, at which point the MSAL instance (which we're of course referencing in the component tree via <MsalProvider />
) is being updated while still in the same MicroTask as a React rendering step.
In my case I'm using Jotai and trying to initialize an atom asynchronously on first use with an API call (that requires getting a token from MSAL), but I've also seen this issue pop up with Recoil.js. No amount of useEffect(...)
-foo was ever really helpful, and in one case I ended up tearing out msal-react altogether in frustration and managing authentication entirely outside of the React tree.
@tnorling Could you add some guidance for this type of scenario in the msal-react tutorial step/documentation for getting an access token?
We faced the same problem when using the method useSuspenseQuery from the library (Tanstack Query). @kawazoe fix works well.
Core Library
MSAL.js v2 (@azure/msal-browser)
Core Library Version
2.34.0
Wrapper Library
MSAL React (@azure/msal-react)
Wrapper Library Version
1.5.4
Public or Confidential Client?
Public
Description
We use a GraphQL API to provide a React SPA with access to various data across our systems. The SPA GraphQL client we're using is Formidable's urql - https://formidable.com/open-source/urql/
We're trying to introduce
msal
as a valid authentication method and we can successfully acquire tokens silently and use those for access to the backend. The urql graphQL operation handling can be extended by providing callback functions for specific purposes. We're providing a callback that requests the token and then adds it to the urql operation.However using the urql
useQuery
hook in the same pattern we have used elsewhere, we get aCannot update a component (
MsalProvider) while rendering a different component error
- this occurs regardless of whether the token is existing in cache or newly acquired.I've provided a stripped down
index.tsx
to demonstrate the issue.Error Message
Msal Logs
MSAL Configuration
Relevant Code Snippets