microsoft-authentication-library-for-js: Cannot update a component (`MsalProvider`)

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 a Cannot 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

image

Msal Logs

image

MSAL Configuration

export const msalConfig: Configuration = {
  auth: {
    clientId: `${process.env.REACT_APP_WEB_CLIENT_ID}`,
    authority: `https://login.microsoftonline.com/${
      process.env.REACT_APP_TENANT_ID
    }`,
    redirectUri: `${process.env.REACT_APP_REDIRECT_URI ?? "http://localhost:3000"}`,
  },
  cache: {
    cacheLocation: "sessionStorage", // This configures where your cache will be stored
    storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
  },
}

Relevant Code Snippets

This is a single index.tsx incorporating the MSAL components used, the urql Provider and a simple query:


import { useEffect } from "react"
import ReactDOM from "react-dom/client"
import { MsalAuthenticationTemplate, MsalProvider } from "@azure/msal-react"
import { InteractionType, PublicClientApplication } from "@azure/msal-browser"
import { useQuery, Operation, makeOperation } from "urql"

// self-host fonts
import "@fontsource/inter"
import "./index.css"
import { GRAPHQL_URL, APP_MODE } from "./conf/config"
import { msalConfig } from "./conf/ssoConfig"
import { createClient, Provider, fetchExchange } from "urql"
import { contextExchange } from "./exchanges/contextExchange"
import { FetchingLabel } from "./common/FetchingLabel"
import { UhOh } from "./common/UhOh"

console.log(`App mode is ${APP_MODE}`)
console.log(`Using graphql at ${GRAPHQL_URL}`)

const SITES = `
query utags {
  sites {
    id
    name
    description
  }
}
`
const msalInstance = new PublicClientApplication(msalConfig)

// get the msal access token from cache or acquire a new one
const acquireAccessToken = async (): Promise<string> => {
  const activeAccount = msalInstance.getActiveAccount() // This will only return a non-null value if you have logic somewhere else that calls the setActiveAccount API
  const accounts = msalInstance.getAllAccounts()

  if (!activeAccount && accounts.length === 0) {
    console.error("No accounts found. Please login.")
    /*
     * User is not signed in. Throw error or wait for user to login.
     * Do not attempt to log a user in outside of the context of MsalProvider
     */
  }
  const request = {
    scopes: ["User.Read"],
    account: activeAccount || accounts[0],
  }

  const authResult = await msalInstance.acquireTokenSilent(request)
  return authResult.accessToken
}

// add the access token to the exchange operation context
const addTokenToContext = async (operation: Operation) => {
  // if operation is not a query or mutation, return the operation
  if (operation.kind !== "query" && operation.kind !== "mutation") {
    return operation.context
  }

  const token = await acquireAccessToken()

  const fetchOptions =
    typeof operation.context.fetchOptions === "function"
      ? operation.context.fetchOptions()
      : operation.context.fetchOptions || {}

  return makeOperation(operation.kind, operation, {
    ...operation.context,
    fetchOptions: {
      ...fetchOptions,
      headers: {
        ...fetchOptions.headers,
        Authorization: `Bearer ${token}`,
      },
    },
  }).context
}

const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)


const exchanges = [
  contextExchange({
    getContext: addTokenToContext,
  }),
  fetchExchange,
]

// setup urql client for use with msal
const client = createClient({
  url: GRAPHQL_URL,
  exchanges,
})

root.render(
  <MsalProvider instance={msalInstance}>
    <MsalAuthenticationTemplate interactionType={InteractionType.Redirect}>
      <Provider value={client}>
        <SimpleQuery />
      </Provider>
    </MsalAuthenticationTemplate>
  </MsalProvider>,
)

export function SimpleQuery() {
  const [result] = useQuery({
    query: SITES,
  })
  const { data, fetching, error } = result

  useEffect(() => {
    if (data?.sites) {
      console.log(data.sites)
    }
  }, [data])

  if (fetching) {
    return <FetchingLabel>Fetching universal tags</FetchingLabel>
  }
  if (error) {
    return <UhOh error={error}>We were unable to find any Sites</UhOh>
  }

  return <div>Simple Query</div>
}


### Reproduction Steps

1. `npm start` with a React CRA setup

### Expected Behavior

We would expect the MSALProvider not to generate a React error on silent token request

### Identity Provider

Azure AD / MSA

### Browsers Affected (Select all that apply)

Chrome

### Regression

_No response_

### Source

External (Customer)

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 1
  • Comments: 15 (3 by maintainers)

Most upvoted comments

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();
};

Closing as it’s not clear this is an issue with MSAL. If anyone disagrees please feel free to open a new issue thanks!

We faced the same problem when using the method useSuspenseQuery from the library (Tanstack Query). @kawazoe fix works well.

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?

Interesting. FWIW we migrated away from urql to the Apollo client and all the MsalProvider issues went away.