TL;DR
Authenticating with Microsoft Graph API inside a Single-Page Application is straightforward when you have full control of the application. This is a bit more challenging when you’re working inside Microsoft Dynamics 365 Customer Experience (i.e. Microsoft Dynamics CRM, or just “CRM”), but it can still be done. This article contains an explanation along with examples of my solution to this problem.
Background
Microsoft Graph API is a fantastic interface that exposes large amounts of functionality and data from Microsoft’s services, including Office 365, OneDrive, SharePoint, and more. This empowers developers to build custom applications that tie together features from a variety of these systems into a single user interface. See my previous post for a bit more of a high-level introduction to Microsoft Graph API (or just “Graph API”).
Many companies using the CRM platform may find value in adding custom functionality that leverages Graph API to interact with Email, Contacts, Calendar, SharePoint, OneNote, and more. In theory, this should be simple enough: Use the JavaScript SDK and follow the rich Getting Started examples.
Not so fast.
The Problem
Before getting to the essence of the problem, let’s review the workflow of the Authentication process that is required to interact with Graph API:
- An unauthenticated User navigates to part of your application that uses Graph API functionality. Your application responds by showing the Microsoft Login site, either by redirecting the entire browser there, or by showing a popup.
- After Microsoft’s login process determines that the user is authenticated by Microsoft, the browser or popup are redirected to the Redirect URL, provided by the application developer. A Login Token is passed as a parameter at the end of the URL.
- The page living at the Redirect URL uses the browser’s lifecycle events (i.e. load) – or your framework’s equivalent – to extract the Login Token, typically storing it inside the JavaScript as a variable or even caching it for later use.
- The Login Token is used to make a second tall to an Authorization service. Upon success, this service returns an Access Token (which is a JWT). The Access Token is then used for each subsequent interaction with Graph API.
Some details in the list above have been simplified for brevity. For those interested in learning more about authentication, read the Graph API Documentation on how to get auth tokens. Also, for what it’s worth, this authentication approach is called Implicit Flow.
I should also add some more background about the CRM configuration that this solution addresses. The custom Single-Page Application (or “SPA”) components that will be making calls to the Graph API are configured in CRM as Web Resources. That is, the HTML and minified JavaScript files have been added to CRM as Web Resources and can be added to a form using the forms designer.
Finally, I want to mention that this solution targets the Unified User Interface (i.e. the UUI or “new UI”). I have not attempted this solution for the Classic UI, but I think it will work there also. YMMV.
Back to the problem at hand. Step #2 above mentions a Redirect URL – this is the essence of our challenge. A custom Single-Page Application hosted as a Web Resource inside CRM is rendered inside an iFrame – it is not actually the top-level page. In other words, that the URL of the page in CRM will depend on the record you’re visiting. For example:
https://[YourOrganization].crm.dynamics.com/main.aspx?appid=&pagetype=entityrecord&etn=quote&id=
Whereas, the Web Resource URL will be something like:
https://[YourOrganization].crm.dynamics.com/{[UniqueId]}/webresources/[YourWebResourceName].html
As mentioned above, the page hosted at the Redirect URL is expected to contain custom functionality for extracting the Login Token (and later the Access Token). Since CRM does not allow full customization of top-level pages, the authentication process requires a bit of tweaking to be succesful.
To summarize the problem:
- Authentication with Graph API requires specifying a Redirect URL
- The page hosted at the Redirect URL is expected to have access to the page’s lifecycle so it can extract the Login Token and Access Token
- CRM presents custom SPAs inside an iFrame, rather than as a top-level page
A Solution
Notice, this is not *the* solution. There are certainly other ways (and probably better ones) to solve this problem. But this solution has worked successfully in my situation. Caveat emptor.
Here is the general overview of my solution:
- In addition to the Web Resource where your main application’s functionality lives, create a second Web Resource that will serve as the Redirect URL.
- From the main Web Resource, perform the login step using the MSAL’s UserAgentApplication.loginPopup() method.
- From the second Web Resource, capture the AccessToken after login, storing it in the browser’s Session or Local Storage (depending on configuration).
- From the main Web Resource, use MSAL’s UserAgentApplication.acquireTokenPopup() method to retrieve the token from the browser’s Session or Local Storage.
- Once the Access Token is in hand, use the Graph API JavaScript SDK methods to make the desired Graph API requests.
The Details
First, the prerequisites:
- In CRM, create a Web Resource (html) that you will use as the Redirect URL. You can consult the CRM documentation on how to locate the full URL of this Web Resource – you’ll need it for the next step.
- As instructed in the Microsoft Graph Quick Start, you will need to register an application on Microsoft’s Application Registration Portal.
- Under the heading for Platforms, you’ll want to add a “Web” platform (NOT “Web API”).
- You will be prompted for a Redirect URL – use the URL from the Web Resource you created above.
- Be sure that “Implicit Flow” is checked.
- Make note of the Application Id (also called “Client Id”) – you will need this later.
- You will need to specify the Delegated and Application permissions that will be used by your application (such as Mail.Read, Mail.Send, User.Read, etc).
- Install the libraries for MSAL and Graph API into your SPA. In my case, I’m working with a React application that uses npm, so I can use the following npm commands:
- npm install msal
- npm install @microsoft/microsoft-graph-client
In your main Web Resource (i.e. the component where the Graph API interactions will be hosted), you can use the following code to initiate the process (the code is written in TypeScript, but you can use a similar approach in JavaScript):
import { Client } from '@microsoft/microsoft-graph-client'; import { UserAgentApplication } from 'msal'; // //Inside an Async method or arrow function // const clientId = '[YourApplicationId]'; //ApplicationId is the same as ClientId const authority = 'https://login.microsoftonline.com/[YourOrganization]'; //See https://docs.microsoft.com/en-us/azure/active-directory/develop/azure-ad-endpoint-comparison for details const redirectUri = '[YourRedirectUrl]'; //RedirectUri is the same as RedirectUrl and this MUST match the value you provided when you configured your Application!! const cacheLocation = 'localStorage'; //localStorage or sessionStorage const scopes = ['Array', 'of', 'your', 'Application', 'Scopes']; //Such as ['Mail.Read', 'User.Read'], etc... const application = new UserAgentApplication(clientId, authority, null, { redirectUri: redirectUri, cacheLocation: cacheLocation }); const loginToken = await application.loginPopup(); //This will open a popup window for authentication and then redirect to your Redirect URL const accessToken = await application.acquireTokenPopup(scopes); //This will open another popup and then redirect to the same Redirect URL //Now that you have the Access Token, you can make Graph API calls. //TODO: You can cache the LoginToken and AccessToken to avoid making additional login/auth calls during the session... //TODO: You'll still need to handle getting a new Access Token after this one expires... //At this point, everything follows the sample applications provided by Graph API const getClient(accessToken: string): Client => { return Client.init({ authProvider: (done) => { done(null, accessToken); } }); } //Get mail folders const myMailFolders = getClient(accessToken) .api('me/mailFolders') .expand('childFolders') .select('id, displayName') .get();
For the second Web Resource (the one you’ll use as your Redirect URL), you’ll want something like this (using React here, but this applies to any framework):
import { UserAgentApplication } from 'msal'; export class AuthRedirect extends React.Component { async componentDidMount() { //This is required behavior so the token will be written to local storage. //This allows the other components to access it. const clientId = '[YourApplicationId]'; //ApplicationId is the same as ClientId const authority = 'https://login.microsoftonline.com/[YourOrganization]'; //See https://docs.microsoft.com/en-us/azure/active-directory/develop/azure-ad-endpoint-comparison for details const redirectUri = '[YourRedirectUrl]'; //RedirectUri is the same as RedirectUrl and this MUST match the value you provided when you configured your Application!! const cacheLocation = 'localStorage'; //localStorage or sessionStorage const scopes = ['Array', 'of', 'your', 'Application', 'Scopes']; //Such as ['Mail.Read', 'User.Read'], etc... const application = new UserAgentApplication(clientId, authority, null, { redirectUri: redirectUri, cacheLocation: cacheLocation }); const accessToken = await application.acquireTokenSilent(scopes); //This will store the token in the specified browser storage (local or session) } public render() { return This is the Auth Redirect Page. This window should close automatically...; } } ReactDOM.render(, document.getElementById('root'));
This component will be seen only briefly, as the popup window will close quickly. Note that you will see it TWO TIMES – once for Login and a second for acquiring the AccessToken. You can minimize this by caching the tokens during the session.
Please forgive any typos or mistakes. This sample includes code from several helper classes and is simplified for illustrative purposes. There are no guarantees, explicit or implied.
I hope this is helpful for anyone who needs to authenticate for using Microsoft Graph API inside Microsoft Dynamics 365 / CRM. Getting this working will make you feel like you’ve scaled Mt. Everest!