I have an angular SPA that runs in an office add-in (word) that I need to authenticate against Azure AD using Oauth2, consume resources from multiple apis and make graph calls. I have been successful doing this in a standard web app but not with the introduction of office add-ins and using the office dialog api. Currently, I have implemented my own authentication service to get auth code with PKCE, get an access token & refresh token, and manage the lifetimes of them. I am stuck trying to take an existing access token and exchange it for use on a different resource without starting the process entirely over. I have read about the on-behalf-of flow to do this task, but it seems to require a client secret or a client assertion and typically is designed for a "middle-man" api. I do not believe that is safe on a SPA. Reading through the MSAL.js source code it appears it can be done, but it also looks like they may be building the new token, encoding it and signing it within the package that runs in the browser. Is there a safe way to use on-behalf-of flow for exchanging tokens? Is there a different option to do this that I don't know about? Thanks!
1 Answers
**Update: With the help of Microsoft (surprise, surprise...) I was able to accomplish this authentication scenario. With one user interaction I can successfully login to Azure AD with an active account and connect to any resource protected by Azure with separate JWT access tokens without subsequent user interactions. Doing this in an office add-in required resolving 3 basic issues: login/logout, login from an http interceptor and login from a route guard. The trick was to override the authRequest 'onRedirectNavigate' and send the url request to the office.context.ui.displayDialogAsync() method. You must also send the resulting authentication redirect back to the taskpane and call the MsalService handleRedirectPromise method with the redirect as the parameter.
This example code shows how to use the onRedirectNavigate of the authRequest to use the office dialog for authentication:
authRequest: {
scopes: [],
onRedirectNavigate: (url: string) => {
Office.context.ui.displayDialogAsync(
url,
{ height: 50, width: 40 },
(result) => {
if (result.status === Office.AsyncResultStatus.Failed) {
console.log(`${result.error.code} ${result.error.message}`);
} else {
dialog = result.value;
dialog.addEventHandler(
Office.EventType.DialogMessageReceived,
// officeDialogHelper.processLoginMessage
async (arg: any) => {
let messageFromDialog = JSON.parse(arg.message);
if (messageFromDialog.status === 'success') {
and then take the resulting redirect:
async handleRedirectCallback(authCode: string) {
await this.msalService.instance.handleRedirectPromise(authCode);
}
The places to implement this are exactly...
- In your login & logout methods
async login() {
this.msalService.loginRedirect({
onRedirectNavigate: (url: string) => {
Office.context.ui.displayDialogAsync(
url,
{ height: 50, width: 40 },
(result) => {
if (result.status === Office.AsyncResultStatus.Failed) {
console.log(`${result.error.code} ${result.error.message}`);
} else {
this.loginDialog = result.value;
this.loginDialog.addEventHandler(
Office.EventType.DialogMessageReceived,
this.processLoginMessage
);
...
logout() {
this.msalService.logoutRedirect({
onRedirectNavigate: (url: string) => {
Office.context.ui.displayDialogAsync(
url,
{ height: 50, width: 40 },
(result) => {
- In the MsalGuard configuration seen in your module you set the guard up as a provider
export function MSALGuardConfigFactory(officeDialogHelper: officeDialogHelper): MsalGuardConfiguration {
let dialog: Office.Dialog;
return {
interactionType: InteractionType.Redirect,
authRequest: {
scopes: [],
onRedirectNavigate: (url: string) => {
Office.context.ui.displayDialogAsync(
url,
{ height: 50, width: 40 },
(result) => {
- In the MsalInterceptor config factory
return {
interactionType: InteractionType.Redirect,
protectedResourceMap,
authRequest: {
onRedirectNavigate: (url: string) => {
Office.context.ui.displayDialogAsync(
url,
{ height: 50, width: 40 },
(result) => {
if (result.status === Office.AsyncResultStatus.Failed) {
console.log(`${result.error.code} ${result.error.message}`);
} else {
dialog = result.value;
dialog.addEventHandler(
Office.EventType.DialogMessageReceived,
async (arg: any) => {
Here is my issue I created on the Microsoft GitHub page: https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/6177
Hope this helps someone! - Josh