Do you use MSGraphClient or the AadHttpClient in your SPFx solutions to access Microsoft Graph API or third-party /custom APIs ? These solutions wouldn’t work in incognito mode or in browsers where third party cookies are blocked (like safari’s Intelligent Tracking Prevention feature). The solution, use msal-browser in SPFx solutions.
I recommend that you read the articles below about authorization code flow with PKCE and msal-browser library to fully understand the solution I will outline in this post.
Let’s look at a sample SPFx webpart that will get email messages using MS Graph API.
Register an app in Azure AD for Authorization code grant flow with PKCE
In the Azure portal, under App Registration click on “New Registration”
- Enter the display name of the app and for this demo we will choose the ‘Supported Account Types’ as single tenant. Click on “Register”
- Under Redirect URI, select “Single Page Application” and enter the redirect URI as /_layouts/workbench.aspx
- After the application is registered, you can navigate to the Authentication section and verify that your app is setup for Auth flow with PKCE
- Go to the API Permissions section and add a “Mail.Read” permission from Microsoft Graph
- Grant Admin consent for these permissions. If you skip this step then the users will be asked to consent when the webpart is accessed for the first time
Scaffold a new SPFx webpart, for this implementation we will use vanilla JavaScript. Let’s call the webpart as Testwebpartmsal. Install the msal-browser npm package.
npm install @azure/msal-browser --save
Update the TestwebpartmsalWebPart.ts file under src->webparts\testwebpartmsal with the following.
Initialize msal.Configuration , msal.PublicClientApplication , msal.AccountInfo and msal.SilentRequest objects
const msalConfig: msal.Configuration = {
auth: {
clientId: "<clientId from app registration step above>",
authority: "https://login.microsoftonline.com/<tenantid>",
redirectUri:"<redirectUri from app registration above",
},
system: {
iframeHashTimeout: 10000,
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (containsPii) {
return;
}
switch (level) {
case msal.LogLevel.Error:
console.error(message);
return;
case msal.LogLevel.Info:
console.info(message);
return;
case msal.LogLevel.Verbose:
console.debug(message);
return;
case msal.LogLevel.Warning:
console.warn(message);
return;
}
},
},
},
};
const msalInstance: msal.PublicClientApplication = new msal.PublicClientApplication(
msalConfig
);
let currentAccount: msal.AccountInfo = null;
const tokenrequest: msal.SilentRequest = {
scopes: ["Mail.Read"],
account: currentAccount,
};
Inside the class TestwebpartmsalWebPart define a method that will set the currently logged in account as a msal account object.
protected setCurrentAccount = (): void => {
const currentAccounts: msal.AccountInfo[] = msalInstance.getAllAccounts();
if (currentAccounts === null || currentAccounts.length == 0) {
currentAccount = msalInstance.getAccountByUsername(
this.context.pageContext.user.loginName
);
} else if (currentAccounts.length > 1) {
console.warn("Multiple accounts detected.");
currentAccount = msalInstance.getAccountByUsername(
this.context.pageContext.user.loginName
);
} else if (currentAccounts.length === 1) {
currentAccount = currentAccounts[0];
}
tokenrequest.account = currentAccount;
};
To call the graph API we need an access token. We first try to get the access token silently using acquireTokenSilent. Since, we don’t have a valid msal account object in our first pass, we will have to fall back to ssoSilent. If by any chance, a valid logged in user isn’t found in the cache then we might have to even fall back to an interactive way of login either using acquireTokenPopup or acquireTokenRedirect.
I will use the acquireTokenPopup for interactive login but you could also easily implement the redirect method if you need to.
protected getAccessToken = async (): Promise<string> => {
let accessToken: string = null;
this.setCurrentAccount();
console.log(currentAccount);
return msalInstance
.acquireTokenSilent(tokenrequest)
.then((tokenResponse) => {
console.log("Inside Silent");
console.log(tokenResponse.accessToken);
return tokenResponse.accessToken;
})
.catch((err) => {
console.log(err);
console.log("Silent Failed");
if (err instanceof msal.InteractionRequiredAuthError) {
return this.interactionRequired();
} else {
console.log("Some other error. Inside SSO.");
const loginPopupRequest: msal.AuthorizationUrlRequest = tokenrequest;
loginPopupRequest.loginHint = this.context.pageContext.user.loginName;
return msalInstance
.ssoSilent(loginPopupRequest)
.then((tokenResponse) => {
return tokenResponse.accessToken;
})
.catch((ssoerror) => {
console.error(ssoerror);
console.error("SSO Failed");
if (ssoerror) {
return this.interactionRequired();
}
return null;
});
}
});
};
protected interactionRequired = (): Promise<string> => {
console.log("Inside Interaction");
const loginPopupRequest: msal.AuthorizationUrlRequest = tokenrequest;
loginPopupRequest.loginHint = this.context.pageContext.user.loginName;
return msalInstance
.acquireTokenPopup(loginPopupRequest)
.then((tokenResponse) => {
return tokenResponse.accessToken;
})
.catch((error) => {
console.error(error);
// I haven't implemented redirect but it is fairly easy
console.error("Maybe it is a popup blocked error. Implement Redirect");
return null;
});
};
Now that we have implemented methods to get the access token. Let’s call the Graph API to get email messages.
protected _searchWithGraph = (): void => {
// Log the current operation
console.log("Using _searchWithGraph() method");
this.getAccessToken().then((accessToken) => {
if (accessToken != null) {
const headers = new Headers();
const bearer = `Bearer ${accessToken}`;
headers.append("Authorization", bearer);
const options = {
method: "GET",
headers: headers,
};
console.log("request made to Graph API at: " + new Date().toString());
fetch("https://graph.microsoft.com/v1.0/me/messages", options)
.then((response) => response.json())
.then((data) => {
console.log(data);
document.getElementById("email").innerText = data.value
.map((o) => o.subject)
.join(" || ");
})
.catch((error) => console.log(error));
} else {
document.getElementById("error").innerText =
"Error! Check browser console";
}
});
};
Modify the render method to show the subject of our latest email messages (top 10)
public render(): void {
this.domElement.innerHTML = `
<div class="${styles.testwebpartmsal}">
<div class="${styles.container}">
<div class="${styles.row}">
<div class="${styles.column}">
<p>Email Subject:<span id="email"></span> </p>
<p>Error, if any : <span id="error"></span></p>
</p>
</div>
</div>
</div>
</div>`;
this._searchWithGraph();
}
I had also encountered a timeout issue while implementing this solution. Details of the issue and its resolution can be found here.
You can use this msal-browser in SPFx solutions until there is a native implementation of auth code flow with PKCE using MSGraphClient and AadHttpClient
Great stuff. Was actually looking for someone who has implemented the msal 2.0. WIll it work if I add multiple web part instances on same page?
Thanks Yogesh. I think this should be possible, although I haven’t tried this yet. If you are sure that the multiple webparts won’t be used in isolation ever, then I would probably use the same app registration and implement a single login mechanism for both the apps.
Ok. My requirement is to use same web part (with different configurations) multiple times on same page . So in this case SSOSilent works fine but if the third party cookies are blocked then redirectLogin causes an issue. My page get redirects multiple times( for ex. 3 times for 3 web parts ) as they all are in isolation.
That’s a bit tricky. The implementation would probably be to use one webpart (apart from the other three that you already have) which is responsible for login and then the other webparts would then be able to use the token from this webpart. I don’t have a definite answer at this point. I haven’t been able to try this as well.
Hi there great work, I have a doubt here I see Redirect URL as the mandatory part in this authentication, so how will we manage this if I am gonna use same SPFX solutions in dynamic provisioned sp pages who url’s are not known in advance
Thanks Arun. You will be able to consume this webpart in any site even with the redirect URI pointing to the workbench.
These lines:
const loginPopupRequest: msal.AuthorizationUrlRequest = tokenrequest;
Will error though or not?
tokenrequest is a different type of object (SilentRequest) than an AuthorizationUrlRequest in this sample. Do you need to do a cast here?
You just need to set msal.PopupRequest instead of AuthorizationUrlRequest
Nice article