Azure,  Power Apps

Calling Microsoft Graph using @azure/msal-react from PCF Control

We are going to revisit some of the concepts that we’ve covered in my earlier posts (here and here) which used Microsoft Authentication Library (MSAL) to acquire tokens from the Microsoft identity platform to authenticate users and access secured web APIs from the PCF control. So what has changed? Since I posted those articles, Microsoft released a new Microsoft Authentication Library for React which is now in v1.0.0-beta.0, so let’s explore this new library and how we can utilise it in our PCF controls.

Here is what we are going to cover in this post:

  • Microsoft’s Official Microsoft Authentication Library for React
  • What changed between MSAL (1.x) and MSAL (2.x)?
  • Authentication flows – Implicit Flow vs. Authorization Code Flow with PKCE
  • Let’s put it together – Microsoft Graph User Lookup control
    • Set up Azure App Registration
    • Set up PCF control
    • Using @azure/msal-react to authenticate the user
    • Using @azure/msal-react to acquire an access token to call MS Graph API

 

Microsoft’s Official Microsoft Authentication Library for React

The announcement of Microsoft’s own official MSAL React library is pretty exciting. It is still in beta, so not recommended for production use but we can start thinking about utilising it in our future projects. Previously we had to either implement our own wrapper around msal.js or consume libraries like react-aad-msal to access the underlying MSAL functions such as login, logout, acquiring tokens, fetching user details, etc. in our React applications. Microsoft has done that heavy lifting for us with @azure/msal-react library!

Let’s take a step back to see how this came about: In July 2020, Microsoft made MSAL.js 2.0 generally available which allowed us to start using the authorization code flow in our browser-based applications in production. With this, they introduced a new npm package @azure/msal-browser, along with its dependency packages like @azure/msal-common to expose the MSAL implementation.

Before moving on, let’s briefly compare MSAL.js 1.x and MSAL.js 2.x in the sections below and why we should use MSAL.js 2.x for our future projects.

 

What Changed Between MSAL (1.x) and MSAL (2.x)?

A few high-level items to outline the differences between the two versions from an implementation/configuration point-of-view:

MSAL 1.xMSAL 2.x
Authentication FlowImplicit grant flowAuthorization code flow with PKCE
NPM Packagenpm install msalnpm install @azure/msal-browser
MSAL Instance Instantiationimport * as msal from "msal";

const msalInstance = new msal.UserAgentApplication(config);
import * as msal from "@azure/msal-browser";

const msalInstance = new msal.PublicClientApplication(config);
Acquiring Silent TokenIf no valid token is found in the cache of the browser storage, it will send a silent token request to the Microsoft Identity platform from a hidden iframe using third-party cookies.
Call msalInstance.acquireTokenSilent
It uses the refresh_token found in the cache of the browser storage to send a silent token request to acquire new tokens and a new refresh token.
Call msalInstance.acquireTokenSilent
Azure App Registration
(Azure Portal)
Under Manage > Authentication
Section – Platform Configurations
Add a platform, select Web

Section – Implicit grant and hybrid flows
Select ☑ Access Tokens and ☑ ID Tokens
Under Manage > Authentication
Section – Platform Configurations
Add a platform, select Single-page application

Section – Implicit grant and hybrid flows
❌ Do not select anything!

 

Authentication Flows – Implicit Flow vs. Authorization Code Flow with PKCE

Let’s compare the two authentication flows:

MSAL 1.x – Implements OAuth 2.0 Implicit Grant Flow

Key things to note:

  • ID tokens and/or access tokens are returned directly from the /authorize endpoint instead of the /token endpoint. It does not return a refresh token
  • With many browsers blocking third-party cookies (cookies of requests which cross domains), implicit flow is no longer suitable as authentication flow for single-page applications as it relies on third-party cookies to silently acquire access tokens when additional resources are required on request or when the token is expired. If blocked, it will force the users to sign in again

 

MSAL 2.x – Implements OAuth 2.0 Authorization Code Flow with PKCE

Key things to note:

  • The authorization code is obtained on request to /authorize endpoint, then the authorization code can be redeemed for the access token and the refresh token on /token endpoint
  • The authorization code flow does not depend on third-party cookies to acquire a new access token, rather, it uses the refresh token. For security reasons, the lifetime of the refresh token is limited to 24 hours to minimise the risk of using stolen refresh tokens. After 24 hours, the application must obtain a new authorization code to request the tokens

 

Let’s put it Together! Microsoft Graph User Lookup Control

Let’s build a PCF control that utilises the @azure/msal-react npm package to call Microsoft Graph to do a simple user lookup. Here is a screenshot of what we will be building:

Before we start looking at the code, let’s first set up the Azure App Registration.

 

1️⃣ Set Up Azure App Registration

From the Azure Portal, search for App Registrations from the global search, select to create a new registration. In the Register an application form, make sure to select Single-page application (SPA) and type in your Power Apps environment URL:

Make sure to take a copy of the Application (client) ID and Directory (tenant) ID from the Overview page to configure the PCF control later:

Setting up the app registration for the authorisation flow is pretty simple, only one other thing is to configure the API permissions required by the PCF control to call the Microsoft Graph API. These delegated permissions can be consented by the users upon them signing in, hence no need to grant admin consent:

When the user signs in for the first time, this is the consent dialog they will see to grant consent to the specified scopes:

 

2️⃣ Set Up PCF Control

First, create your PCF control project using the PCF CLI command pac pcf init. We need to install the following npm packages:

npm install react react-dom @fluentui/react @azure/msal-react @azure/msal-browser @microsoft/microsoft-graph-client async-lock

The structure of the project:

components/
    Login.tsx
    UserLookup.tsx
helpers/
    EnvironmentHelper.ts
    MsalHelper.ts
    MsGraphHelper.tsx
interfaces/
    IConfig.ts
    IUserDetail.ts
ui/
    Layout.tsx
App.tsx
AppContext.tsx
ControlManifest.Input.xml
index.ts

For this PCF control, we need to configure a couple of environment-specific values which can be customised uniquely across the Power Apps environments. The Environment Variable is perfect for this and we will use the same concept along with the helper class that I wrote about in a separate post.

Here I’ve set up two environment variables:

NameData TypeValue
trh_MSALConfigurationJSON{
    “clientId”: “<app-registration-client-id>”,
    “authority”: “https://login.microsoftonline.com/<azure-tenant-id>”,
    “redirectUri”: “https://<your-powerapps-environment>.crm6.dynamics.com”
}
trh_ScopesTextUser.ReadBasic.All,Presence.Read.All
  • trh_MSALConfiguration – a JSON value to collect information necessary to instantiate a PublicClientApplication object
  • trh_Scopes – a list of scopes that need to be consented to by the signed-in user to access resources using Microsoft Graph API. This is stored as a comma-delimited value

These environment variable names will be used to customise the control on the form as follows:

 

3️⃣ Using @azure/msal-react to Authenticate the User

In index.ts, we retrieve the environment variable values using the properties customised on the PCF control. We initialise an instance of a PublicClientApplication object and PopupRequest object with the scopes:

private _msalInstance: PublicClientApplication;
private _tokenRequest: PopupRequest;
private _environmentHelper: EnvironmentHelper;

public async updateView(context: ComponentFramework.Context<IInputs>): Promise<void> {
	this._lock.acquire("init", async () => {
		if (this._msalInstance == null || 
			context.updatedProperties.includes("env_msalConfig")) {
			this._msalInstance = 
				await this.getMsalConfig(context.parameters.env_msalConfig.raw!);
		}

		if (this._tokenRequest == null || 
			context.updatedProperties.includes("env_scopes")) {
			this._tokenRequest = {
				scopes: await this.getScopes(context.parameters.env_scopes.raw || "")
			};
		}
	}).then(() => {
		if (this._msalInstance) {
			this._props = {
				componentContext: context,
				msalInstance: this._msalInstance,
				tokenRequest: this._tokenRequest
			};
			ReactDOM.render(
				React.createElement(App, this._props),
				this._container
			);
		}
	});
}

private getMsalConfig = async (envVarName: string) => {
	const config: IConfig = await this._environmentHelper.getValue(envVarName);
	return new PublicClientApplication({
		auth: {
			clientId: config.clientId,
			authority: config.authority || "https://login.microsoftonline.com/common",
			redirectUri: config.redirectUri || window.location.href,
			postLogoutRedirectUri: config.postLogoutRedirectUri || window.location.href
		}
	} as Configuration);
}

private getScopes = async (envVarName: string) => {
	let scopes: string[] = [];
	if (envVarName) {
		scopes = (<string>(await this._environmentHelper.getValue(envVarName)))
					.split(",")
					.map(s => s.trim());
	}
	return scopes;
}

In App.tsx, we initialise our own React context, AppProvider, as well as the MsalProvider React context defined in @azure/msal-react. All components underneath MsalProvider will have access to the PublicClientApplication instance via context as well as using hooks:

export const App: React.FC<IAppProps> = (props: IAppProps) => {
    const { componentContext, msalInstance, tokenRequest } = props;

    return (
        <AppProvider
            componentContext={componentContext}
            tokenRequest={tokenRequest}>
            <MsalProvider instance={msalInstance}>
                <Layout>
                    <AuthenticatedTemplate>
                        <UserLookup />
                    </AuthenticatedTemplate>
                    <UnauthenticatedTemplate>
                        <Text>Please use the Sign in button to login.</Text>
                    </UnauthenticatedTemplate>
                </Layout>
            </MsalProvider>
        </AppProvider>
    );
}

As the name suggests, AuthenticatedTemplate and UnauthenticatedTemplate components will render their children depending on the authentication state of the user.

The Layout component sets the UI container around the child components, this places the Login component in the overall layout.

export const Layout: React.FC<ILayoutProps> = (props: ILayoutProps) => {
    return (
        <div className={classNames.container}>
            <header>
                <Login />
            </header>
            <main>
                {props.children}
            </main>
        </div>
    );
}

The @azure/msal-react library provides several React hooks, the Login component uses the useMsal hook which returns the instance of the PublicClientApplication, the status of MSAL progress and the accounts that are currently signed in. We can use the loginPopup and logout APIs on the MSAL instance to expose the user authentication process on the control. Personally, I find the popup interaction to present the login screen to the users, as opposed to redirecting the browser window, to be most effective as it feels natural from the model-driven app. The loginPopup waits for the authentication flow to finish and verifies and returns to the redirect URI. We have configured the redirect URI to be the Power Apps environment URL on the App Registration as well as in our MSAL configuration, it is crucial that these values match!

export const Login: React.FC = () => {
    const { instance, inProgress, accounts } = useMsal();
    const account = useAccount(accounts[0] || {});
    const { tokenRequest } = useAppContext();
    
    return (
        <Stack
            horizontalAlign={"end"}
            horizontal 
            tokens={{ childrenGap: 4 }}
            verticalAlign={"center"}>
            <AuthenticatedTemplate>
                <ActionButton
                    iconProps={{ iconName: "Signout" }}
                    onClick={()=> instance.logout()}>
                    Sign Out {account?.name ? `(${account.name})`: ''}
                </ActionButton>
            </AuthenticatedTemplate>
            <UnauthenticatedTemplate>
                <ActionButton
                    iconProps={{ iconName: "Signin" }}
                    onClick={() => instance.loginPopup(tokenRequest)}
                    disabled={inProgress === InteractionStatus.Login}>
                    Sign in
                </ActionButton>
            </UnauthenticatedTemplate>
        </Stack>
    );
}

To get more information about the signed-in user, we take the first account from the accounts list to retrieve the AccountInfo object by using the useAccount hook. This hook will return null if the account is not signed-in or doesn’t contain a valid identifier, homeAccountId, to retrieve the information. The AccountInfo object returned has the following structure:

export type AccountInfo = {
    homeAccountId: string;
    environment: string;
    tenantId: string;
    username: string;
    localAccountId: string;
    name?: string;
    idTokenClaims?: object;
};

 

4️⃣ Using @azure/msal-react to Acquire Access Token to Call MS Graph API

Before we can call the MS Graph API, we must first acquire an access token. Underneath the hood, MSAL caches the tokens (i.e. ID token, access token and refresh token) upon initially acquiring them and later retrieves them from the cache when requested. When we use the acquireTokenSilent method, it handles the renewal of these tokens automatically when expired before returning the access token.

By default, you can see these tokens cached on your browser by opening up the Developer Tool (F12) > Application > Session Storage (I’m using Chrome):

In MsalHelper.ts:

import { AccountInfo, IPublicClientApplication, PopupRequest, InteractionRequiredAuthError } from "@azure/msal-browser";

export const acquireTokenRequest = (instance: IPublicClientApplication, account: AccountInfo | null, tokenRequest: PopupRequest) => {
    if (account) {
        return instance.acquireTokenSilent({
            ...tokenRequest,
            account: account
        }).catch(error => {
            if (error instanceof InteractionRequiredAuthError) {
                return instance.acquireTokenPopup(tokenRequest);
            }
        });
    };
}

Here we also cater for those situations where silent call fails and we need to fallback on user interaction by calling acquireTokenPopup.

Once we have the access token we can call the MS Graph API. We are going to utilise the @microsoft/microsoft-graph-client npm package for this. In MsGraphHelper.ts, we have methods to retrieve user resources such as user profile, image and presence. For each method, we pass in the access token that we’ve acquired to instantiate an instance of the MS Graph client to call the APIs:

import { ResponseType } from "@microsoft/microsoft-graph-client";
import graph = require("@microsoft/microsoft-graph-client");

export const getUser = async (accessToken: string, upn: string) => {
    const client = await getAuthenticatedClient(accessToken);
    const user = client.api(`/users/${upn}`).get();
    return user;
}

export const getUserPresence = async (accessToken: string, id: string) => {
    const client = await getAuthenticatedClient(accessToken);
    const userPresence = client.api(`/users/${id}/presence`).get();
    return userPresence;
}

export const getUserImage = async (accessToken: string, id: string) => {
    const client = await getAuthenticatedClient(accessToken);
    const response = (await client.api(`/users/${id}/photo/$value`)
        .responseType(ResponseType.RAW)
        .get()) as Response;

    if (response.status === 404 || !response.ok) {
        return null;
    }

    const base64Image = await blobToBase64(await response.blob());
    return base64Image;
}

const getAuthenticatedClient = async (accessToken: string) => {
    return graph.Client.init({
        authProvider: (done: any) => {
            done(null, accessToken);
        }
    });
}

const blobToBase64 = async (blob: Blob): Promise<string> => {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onerror = reject;
        reader.onload = _ => {
            resolve(reader.result as string);
        };
        reader.readAsDataURL(blob);
    });
}

We can now consume these methods from UserLookup.tsx to search for the user via their user principal name (email) or Azure object id. The majority of the logic is in searchUser method:

export const UserLookup: React.FC = () => {
    const { instance, accounts } = useMsal();
    const { tokenRequest } = useAppContext();
    const account = useAccount(accounts[0] || {});
    const [userDetail, setUserDetail] = useState<IUserDetail | undefined>();
    const [errorMessage, setErrorMessage] = useState<string>();
    const [processing, setProcessing] = useState<boolean>(false);
    const inputEl = useRef<TextFieldBase>(null);

    const searchUser = () => {
        if (inputEl.current?.value) {
            setProcessing(true);
            resetSearch();

            acquireTokenRequest(instance, account, tokenRequest)
                ?.then((response: AuthenticationResult | undefined) => {
                    if (response) {
                        getUser(response.accessToken, inputEl.current?.value!)
                            .then((user: any) => Promise.all([
                                getUserPresence(response.accessToken, user.id),
                                getUserImage(response.accessToken, user.id)
                            ])
                                .then((results: any[]) => {
                                    const userDetails: IUserDetail = {
                                        displayName: user.displayName,
                                        givenName: user.givenName,
                                        id: user.id,
                                        jobTitle: user.jobTitle,
                                        mail: user.mail,
                                        surname: user.surname
                                    };

                                    if (results && results.length === 2) {
                                        if (results[0]) {
                                            userDetails.activity = results[0]?.activity;
                                            userDetails.availability = results[0]?.availability;
                                        }
                                        if (results[1]) {
                                            userDetails.photo = results[1];
                                        }
                                    }
                                    setUserDetail(userDetails);
                                })
                            )
                            .catch(_ => {
                                setErrorMessage(`User '${inputEl.current?.value!}' could not be retrieved.`);
                                setProcessing(false);
                            })
                            .finally(() => {
                                setProcessing(false);
                            })
                    }
                });
        } else {
            resetSearch();
        }
    }

    const resetSearch = () => {
        setErrorMessage("");
        setUserDetail(undefined);
    }

    const userDetailEl = userDetail
        ? <Persona
            size={PersonaSize.size100}
            text={userDetail.displayName}
            secondaryText={userDetail.jobTitle}
            tertiaryText={userDetail.mail}
            imageUrl={userDetail.photo}
            optionalText={mapPresenceActivity(userDetail.activity)}
            presence={mapPresenceAvailability(userDetail.availability)}
        />
        : processing ? null : <Text>{errorMessage ? errorMessage : "No user to show!"}</Text>

    return (
        <Stack tokens={stackTokens}>
            <Stack horizontal verticalAlign="end" tokens={stackTokens}>
                <TextField
                    componentRef={inputEl}
                    autoComplete="off"
                    label="Lookup User"
                    placeholder="Enter User Principal Name or Object Id"
                    styles={textFieldStyles}
                />
                <PrimaryButton text="Search" onClick={searchUser} allowDisabledFocus />
            </Stack>
            {userDetailEl}
        </Stack>
    );
}

Here is a demo of the finished PCF control:

Hope you enjoyed the post, let me know your thoughts in the comments below or any of my contact channels 😎

Updated 25/03/2022: Some of you have asked the demo source control to be available on Github so I released it here.