Azure,  Power Apps

Consuming Microsoft Graph API from PCF Control

There are often situations where data from other Microsoft 365 services are required from within your model-driven Power Apps. One such scenario is to dynamically retrieve additional information about the system users which does not get synchronised from Office 365/Azure AD.

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

  • An overview of authentication and authorisation for Microsoft Graph API
  • Setting up the PCF control and implementation decisions around state management
  • Consuming the Microsoft Graph API from the PCF control to retrieve user information and user presences

 

What is Microsoft Graph API?

In a nutshell, Microsoft has provided the developers with a single endpoint, https://graph.microsoft.com to give access to a tremendous amount of data and insights across Microsoft 365 services. This endpoint can be accessed via REST APIs with the help of SDKs and client-side libraries. So why is it useful in the context of Power Apps? Power Apps is essentially a platform sitting on top of the ecosystem of Azure and Microsoft 365. And there are many other services such as Outlook/Exchange, Planner, SharePoint to name a few, that are also sitting within the same integrated space. By using Microsoft Graph API, we can build more user-centric experiences for the end-users allowing them to be more productive within one platform. You can learn more about Microsoft Graph here. Go and experiment with the Graph Explorer, a handy web-based tool to explore possibilities with the Microsoft Graph APIs.

OK, enough introductions, let’s see some action!

 

Setting up the PCF Control

The main objective of this post is to set up a PCF control to consume the Microsoft Graph API. To keep the exercise simple, we are going to create a data-set PCF control to display a list of system users with their current availability and activity presence retrieved using the Microsoft Graph Client library.

The screenshot below shows the outcome of this post:

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 @microsoft/microsoft-graph-client msal

The folder structure for the project:

components/
    Login.tsx
    Message.tsx
    Users.tsx
interfaces/
    IConfig.ts
    IUser.ts
store/
    auth/
        actions.ts
        reducers.ts
    userlist/
        actions.ts
        reducers.ts
    index.ts
utils/
    errors.ts
App.tsx
AppContext.tsx
AuthProvider.ts
ControlManifest.Input.xml
GraphService.ts
index.ts

At this point, you might be thinking, what are all these files for? Won’t we be building a simple control? Yes and no. Things can get pretty messy and hairy as soon as there are various global states to be managed within your control. In this particular implementation, we are going to use React Context API with React Hooks to manage state across the components.

I'm no expert in React. My experience comes from learning, experimenting and building React PCF controls. For any React experts out there reading this, I would greatly appreciate any comments on how I can improve the code!

 

Authentication and Authorisation for Microsoft Graph API

In order to call the Microsoft Graph API, the PCF control must acquire an access token from the Microsoft identity platform. To do this, we need to create an app registration using the Azure Portal and configure the permissions to authorise calls to Microsoft Graph resources.

In the https://portal.azure.com, type App registrations in the global search and select App Registrations. Select New Registration and set the values as follows:

Once the app has been registered, copy the values for Application (client) ID and Directory (tenant) ID as we will need these later to configure our PCF control:

Make sure to enable the implicit grant flow by setting these checkboxes under Authentication:

The app needs to be configured to allow access to the resources exposed by Microsoft Graph. From our PCF control, we need the ability to retrieve information about the current user and the ability to retrieve presence information for multiple users. Hence these permissions must be given:

  • User.Read
  • Presence.Read.All

We will be using delegated permissions which allows a user or an administrator to consent to the permissions so the app can act as the signed-in user when requesting the resources.

By default, User.Read permission is already configured when app registration is created.

Select Microsoft Graph from the list:

Select Delegated Permissions, and start searching for the word, presence. Check the box for Presence.Read.All and click Add Permission:

The configured permissions list should look like the following:

Once this is configured we can use the information we’ve copied earlier to acquire the OAuth access token from our control.

 

Configuring the Control for Authentication

We will be integrating Microsoft Authentication Library (MSAL) library to obtain the access token to call the Microsoft Graph API.

In index.ts, we will retrieve the Azure AD app settings via the properties set up in the control manifest:

private _container: HTMLDivElement;
private _props: IAppProps;

public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement) {
	this._container = container;

	context.mode.trackContainerResize(true);
	context.parameters.dataSet.paging.setPageSize(5000);

	this._props = {
		componentContext: context,
		authProvider: new AuthProvider({
			appId: context.parameters.azureADAppId.raw!,
			appRedirectUrl: context.parameters.azureADAppRedirectUrl.raw!,
			appAuthority: context.parameters.azureADAppAuthority.raw!,
			appScopes: [
				'user.read',
				'Presence.Read.All'
			]
		} as IConfig)
	};
}

public updateView(context: ComponentFramework.Context<IInputs>): void {
	if (context.parameters.dataSet.loading) return;
	
	this._props.componentContext = context;

	ReactDOM.render(
		React.createElement(App, this._props),
		this._container
	);
}

The AuthProvider.ts implements all the standard methods required for the authentication:

import { UserAgentApplication } from 'msal';
import { IConfig } from "./interfaces/IConfig";
import { getUserDetails } from './GraphService';

export interface IIdentity {
    displayName: string,
    email: string
}

export class AuthProvider {
    private _userAgentApplication: UserAgentApplication;
    
    constructor(private _config: IConfig) {
        this._userAgentApplication = new UserAgentApplication({
            auth: {
                clientId: _config.appId,
                redirectUri: _config.appRedirectUrl,
                authority: _config.appAuthority,
                postLogoutRedirectUri: window.location.href
            },
            cache: {
                cacheLocation: "sessionStorage",
                storeAuthStateInCookie: true
            }
        })
    }

    async login() {
        await this._userAgentApplication.loginPopup({
            scopes: this._config.appScopes,
            prompt: "select_account"
        });
    }

    logout() {
        this._userAgentApplication.logout();
    }

    getAccount() {
        return this._userAgentApplication.getAccount();
    }

    async getIdentity(): Promise<IIdentity | undefined> {
        const accessToken = await this.getAccessToken();
        if (accessToken) {
            const user = await getUserDetails(accessToken);
            return {
                displayName: user.displayName,
                email: user.mail || user.userPrincipalName
            };
        }
    }

    async getAccessToken(): Promise<string> {
        try {
            const silentResponse = await this._userAgentApplication.acquireTokenSilent({
                scopes: this._config.appScopes
            });
            return silentResponse.accessToken;
        } catch (err) {
            if (this.isInteractionRequired(err)) {
                const interactiveResponse = await this._userAgentApplication.acquireTokenPopup({
                    scopes: this._config.appScopes
                });
                return interactiveResponse.accessToken;
            } else {
                throw err;
            }
        }
    }

    private isInteractionRequired(error: Error): boolean {
        if (error.message?.length <= 0) {
            return false;
        }

        return (
            error.message.indexOf('consent_required') > -1 ||
            error.message.indexOf('interaction_required') > -1 ||
            error.message.indexOf('login_required') > -1
        );
    }
}

The methods to call the Microsoft Graph API are defined in GraphService.ts:

import graph = require("@microsoft/microsoft-graph-client");

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

export const getUserDetails = async(accessToken: string) => {
    const client = getAuthenticatedClient(accessToken);
    const user = await client.api("/me")
                        .select(["department","displayName","id","mail","userPrincipalName"])
                        .get();
    return user;
}

export const getPresencesByUserId = async(accessToken: string, ids: string[]) => {
    const client = getAuthenticatedClient(accessToken);
    const res = await client.api("/communications/getPresencesByUserId")
                             .version("beta")
                             .post({ids});
    return res;
}

Please note, retrieving presence information is currently in beta. More information here.

 

Managing States with React Context API and Hooks

When you start building complex PCF controls there will often be times when multiple components are needed. In these cases the data is usually passed between layers of components using props, this is often referred to as “threading” or prop drilling. As soon as the same data is needed by many components, this can quickly become very messy. To solve this, the React Context API can be used to share data without having to explicitly pass props through every component in the React component tree. Combining this with React Hooks, we can start managing global states within the control. This is particularly useful when you have an authentication state that needs to be shared. We will be using useState, useContext, useReducer and custom hooks. As this is not a React post, I’m not going to go into details about how-tos of these React features.

There are a few high-level terminologies you should be aware of:

  • Store – an object that holds the control’s state tree
  • Action – payloads of information that send data from your control to your store. You send them to the store using dispatch
  • Reducer – a function that accepts payload from actions and accumulates state value to process and return new accumulated state value

These are very familiar concepts if you have used Redux, a state container for JavaScript apps.

First, let’s lay out the components in App.tsx:

import * as React from "react";
import { IInputs } from "./generated/ManifestTypes";
import { AppProvider } from "./AppContext";
import { AuthProvider } from "./AuthProvider";
import { Login } from "./components/Login";
import { Users } from "./components/Users";
import { Message } from "./components/Message";

export interface IAppProps {
    componentContext: ComponentFramework.Context<IInputs>
    authProvider: AuthProvider
}

export const App: React.FC<IAppProps> = (props: IAppProps) => {
    return (
        <AppProvider {...props}>
            <Message></Message>
            <Login></Login>
            <Users></Users>
        </AppProvider>
    );
}

In AppContext.tsx, we create the Context:

import * as React from "react";
import { createContext, useReducer, useContext, Dispatch } from "react";
import { IInputs } from "./generated/ManifestTypes";
import { rootReducer, IRootState } from "./store";
import { UserlistActions } from "./store/userlist/actions";
import { AuthActions, AuthActionTypes } from "./store/auth/actions";
import { AuthProvider, IIdentity } from "./AuthProvider";
import { IAppProps } from "./App";

const initialState: IRootState = {
    auth: {
        authenticated: false
    },
    userlist: {
        users: []
    }
};

export interface IAppContext {
    authProvider: AuthProvider,
    componentContext: ComponentFramework.Context<IInputs>,
    state: IRootState,
    dispatch: Dispatch<UserlistActions | AuthActions>
}

export const AppContext = createContext<IAppContext>({} as IAppContext);

export const AppProvider: React.FC<IAppProps> = (props) => {
    const [state, dispatch] = useReducer(rootReducer, initialState);
    const { authProvider, componentContext } = props;
    return (
        <AppContext.Provider value={{ authProvider, componentContext, state, dispatch }} >
            {props.children}
        </AppContext.Provider>
    );
};

The AppContext will provide the authentication, PCF component context, global states and dispatch function to all child components consuming it.

We’ve also created a custom hook, useAppContext, to streamline context access from child functional components:

export const useAppContext = () => useContext(AppContext);

We then create a custom hook, useAuthContext, to abstract the access to the authentication provider and its states:

export const useAuthContext = () => {
    const { authProvider, state, dispatch } = useAppContext();
    const { auth } = state;

    const login = () => {
        authProvider.login().then(() => {
            dispatch({
                type: AuthActionTypes.LOGIN_SUCCESS
            });
        }, (err) => {
            dispatch({
                type: AuthActionTypes.LOGIN_FAILURE,
                payload: err
            });
        });
    };

    const logout = () => {
        authProvider.logout();
        dispatch({
            type: AuthActionTypes.LOGOUT
        });
    };

    const getAccessToken = async (): Promise<string> => {
        return authProvider.getAccessToken();
    };

    const handleAuthentication = async () => {
        const account = authProvider.getAccount();
        if (authProvider.getAccount()) {
            await authProvider.getIdentity().then((identity?: IIdentity) => {
                if (identity) {
                    dispatch({
                        type: AuthActionTypes.SET_IDENTITY,
                        payload: identity
                    });
                }
            }, (err) => {
                dispatch({
                    type: AuthActionTypes.AUTH_FAILURE,
                    payload: err
                });
            });
        }
        return account;
    };

    return {
        authenticated: auth.authenticated,
        identity: auth.identity,
        login,
        logout,
        getAccessToken,
        handleAuthentication
    };
}

In our control we have multiple stores to hold the states, one for authentication and one for users. Hence, rootReducer is used to combine the multiple stores in one place in store/index.ts.

import { UserlistActions } from "./userlist/actions";
import { AuthActions } from "./auth/actions";
import { authReducer, AuthState } from "./auth/reducers";
import { userlistReducer, UserlistState } from "./userlist/reducers";

export interface IRootState {
    auth: AuthState,
    userlist: UserlistState
}

export const rootReducer = (state: IRootState, action: UserlistActions | AuthActions) => ({
    auth: authReducer(state.auth, action as AuthActions),
    userlist: userlistReducer(state.userlist, action as UserlistActions)
});

In store/auth/actions.ts:

import { IIdentity } from "../../AuthProvider"

export enum AuthActionTypes {
    LOGIN = 'LOGIN',
    LOGIN_SUCCESS = 'LOGIN_SUCCESS',
    LOGIN_FAILURE = 'LOGIN_FAILURE',
    LOGOUT = 'LOGOUT',
    SET_IDENTITY = 'SET_IDENTITY',
    AUTH_FAILURE = 'AUTH_FAILURE'
}

type LoginAction = {
    type: typeof AuthActionTypes.LOGIN
}

type LogoutAction = {
    type: typeof AuthActionTypes.LOGOUT
}

type LoginFailure = {
    type: typeof AuthActionTypes.LOGIN_FAILURE,
    payload: any
}

type LoginSucess = {
    type: typeof AuthActionTypes.LOGIN_SUCCESS
}

type SetAuthenticated = {
    type: typeof AuthActionTypes.SET_IDENTITY,
    payload: IIdentity
}

type AuthFailure = {
    type: typeof AuthActionTypes.AUTH_FAILURE,
    payload: any
}

export type AuthActions = 
    | LoginAction
    | LoginSucess
    | LoginFailure
    | LogoutAction
    | SetAuthenticated
    | AuthFailure;

In store/auth/reducers.ts:

import { AuthActions, AuthActionTypes } from "./actions";
import { IIdentity } from "../../AuthProvider";
import { INormalisedError, getNormalisedError } from "../../utils/errors";

export interface AuthState {
    authenticated: boolean;
    identity?: IIdentity;
    error?: INormalisedError | null;
}

export function authReducer(state: AuthState, action: AuthActions) {
    switch (action.type) {
        case AuthActionTypes.LOGIN:
            return {
                ...state,
                authenticated: false
            };

        case AuthActionTypes.LOGIN_SUCCESS:
            return {
                ...state,
                authenticated: true,
                error: null
            };

        case AuthActionTypes.LOGIN_FAILURE:
            return {
                ...state,
                authenticated: false,
                error: getNormalisedError(action.payload)
            };

        case AuthActionTypes.LOGOUT:
            return {
                ...state,
                authenticated: false
            };

        case AuthActionTypes.SET_IDENTITY:
            return {
                ...state,
                authenticated: true,
                identity: action.payload,
                error: null
            };

        case AuthActionTypes.AUTH_FAILURE:
            return {
                ...state,
                error: getNormalisedError(action.payload)
            };

        default:
            return state;
    }
}
Userlist store is managed in a similar way as auth store, I won't go into details here. 

 

Login Component

All the authentication actions are encapsulated in the Login.tsx component:

import * as React from "react";
import { useEffect } from "react";
import { ActionButton, Stack } from "office-ui-fabric-react";
import { useAuthContext } from "../AppContext";

export const Login: React.FC = () => {
    const { authenticated, identity, login, logout, handleAuthentication } = useAuthContext();

    useEffect(() => {
        handleAuthentication();
    },[authenticated])

    return (
        <Stack verticalAlign={"center"} horizontalAlign={"end"} horizontal tokens={{ childrenGap: 4 }}>
            {authenticated && (
                <ActionButton iconProps={{ iconName: "Signout" }} onClick={logout}>{identity?.displayName || ""}</ActionButton>
            )}
            {!authenticated && (
                <ActionButton iconProps={{ iconName: "Signin" }} onClick={login}>Sign in</ActionButton>
            )}
        </Stack>
    );
}

It uses the custom hook useAuthContext to retrieve the states and the methods required to log in and logout in a decoupled manner.

Using useEffect we handle the authenticated state for any users returning to the control after they have logged in once. This will silently refresh the tokens if required on the component mount.

 

Users Component

Finally, to tie everything together, here is the Users.tsx:

import * as React from "react";
import { Stack, IStackStyles, Persona, PersonaPresence, PersonaSize } from "office-ui-fabric-react";
import { IInputs } from "../generated/ManifestTypes";
import { useAppContext, useAuthContext } from "../AppContext";
import { IUser } from "../interfaces/IUser";
import { UserlistActionTypes } from "../store/userlist/actions";
import { getPresencesByUserId } from "../GraphService";
import { AuthActionTypes } from "../store/auth/actions";

const personaWrapper: IStackStyles = {
    root: {
        backgroundColor: "#efefef", 
        padding: 8, 
        flex: "1 1 0" 
    }
}

export const Users: React.FC = () => {
    const { state, dispatch, componentContext } = useAppContext();
    const { authenticated, getAccessToken } = useAuthContext();
    const { userlist } = state;

    React.useEffect(() => {
        dispatch({ type: UserlistActionTypes.LOAD_USERS });
        try {
            const users = getUsers(componentContext);
            if (authenticated) {
                getAccessToken().then((token) => {
                    getPresences(token, users).then((updatedUsers) => {
                        dispatch({ 
                            type: UserlistActionTypes.LOAD_USERS_SUCCESS, 
                            payload: updatedUsers 
                        });
                    }, (err) => {
                        dispatch({ 
                            type: UserlistActionTypes.LOAD_USERS_FAILURE, 
                            payload: err 
                        });
                    })
                }, (err) => {
                    dispatch({ 
                        type: AuthActionTypes.AUTH_FAILURE, 
                        payload: err 
                    });
                });
            }
            else {
                dispatch({ 
                    type: UserlistActionTypes.LOAD_USERS_SUCCESS, 
                    payload: users 
                });
            }
        } catch (err) {
            dispatch({ 
                type: UserlistActionTypes.LOAD_USERS_FAILURE, 
                payload: err 
            });
        }
    }, [componentContext.parameters.dataSet, authenticated]);

    return (
        <Stack horizontal wrap tokens={{childrenGap: 8}}>
            {userlist.users.map(user => (
                <Stack key={user.key} styles={personaWrapper}>
                    <Persona
                        size={PersonaSize.size72}
                        text={user.fullname}
                        secondaryText={user.jobTitle}
                        tertiaryText={mapPresenceActivity(user.presenceActivity)}
                        presence={mapPresenceAvailability(user.presenceAvailability)}
                        onClick={()=>openForm(componentContext, user)}
                    />
                </Stack>
            ))}
        </Stack>);
}

const getUsers = (componentContext: ComponentFramework.Context<IInputs>): IUser[] => {
    const dataSet = componentContext.parameters.dataSet;
    const users = dataSet.sortedRecordIds.map<IUser>(recordId => ({
        key: recordId,
        azureADObjectId: dataSet.records[recordId].getFormattedValue("azureactivedirectoryobjectid"),
        fullname: dataSet.records[recordId].getFormattedValue("fullname"),
        jobTitle: dataSet.records[recordId].getFormattedValue("jobTitle")
    }));
    return users;
}

const getPresences = async (accessToken: string, users: IUser[]): Promise<IUser[]> => {
    const ids = users.map(user => user.azureADObjectId)
    const res = await getPresencesByUserId(accessToken, ids);
    if (res && res.value) {
        users.map(user => {
            const presence = res.value.find((r: any) => r.id?.toLocaleLowerCase() == user.azureADObjectId.toLocaleLowerCase());
            if (presence) {
                user.presenceAvailability = presence.availability;
                user.presenceActivity = presence.activity
            }
        })
    }
    return users;
}

const mapPresenceAvailability = (availability?: string): PersonaPresence => {
    switch (availability) {
        case "Available":
        case "AvailableIdle":
            return PersonaPresence.online;
        case "Away":
        case "BeRightBack":
            return PersonaPresence.away;
        case "Busy":
        case "BusyIdle":
            return PersonaPresence.busy;
        case "DoNotDisturb":
            return PersonaPresence.dnd;
        case "Offline":
            return PersonaPresence.offline;
    }
    return PersonaPresence.none;
}

const mapPresenceActivity = (activity?: string): string => {
    switch(activity) {
        case "BeRightBack": return "Be Right Back";
        case "DoNotDisturb": return "Do Not Disturb";
        case "InACall": return "In A Call";
        case "InAConferenceCall": return "In A Conference Call";
        case "InAMeeting": return "In A Meeting";
        case "OffWork": return "Off Work";
        case "OutOfOffice": return "Out Of Office";
        case "PresenceUnknown": return "Presence Unknown";
        case "UrgentInterruptionsOnly": return "Urgent Interruptions Only";
        default:
            return activity || "";
    }
}

const openForm = (componentContext: ComponentFramework.Context<IInputs>, user: IUser): void => {
    componentContext.navigation.openForm({ entityId: user.key, entityName: "systemuser", openInNewWindow: true } as ComponentFramework.NavigationApi.EntityFormOptions)
};

Using useEffect, we update the userlist store with the latest accumulation of users when either the PCF context dataset or authentication status changes.

What makes this control possible is the Azure Active Directory Object ID stored in the system user entity. This is the unique ID of the user in the Azure AD, hence no special query needs to be executed to map the users before calling the Microsoft Graph API.

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 😊