Yeoman Generator

Uses MSAL 2.0 with Implicit Flow using msal (1.3.2)
Uses office-addin-sso (1.0.36)
Uses OfficeRuntime.auth.getAccessToken
link - docs.microsoft.com/en-us/office/dev/add-ins/quickstarts/sso-quickstart
link - docs.microsoft.com/en-us/office/dev/add-ins/quickstarts/sso-quickstart-customize
The source code used to create this project can be found here:
link - github.com/OfficeDev/Office-Addin-Taskpane-SSO

npm run configure-sso 
npm start

manifest.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?> 
<OfficeApp
  xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
  xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides"
  xsi:type="TaskPaneApp">
  <Id>1cc6cbec-df0d-421e-8bde-af08e5930cf2</Id>
  <Version>1.0.0.0</Version>
  <ProviderName>Contoso</ProviderName>
  <DefaultLocale>en-US</DefaultLocale>
  <DisplayName DefaultValue="MyWordSsoAddin"/>
  <Description DefaultValue="An add-in that shows how to use SSO, and to fallback to interactive login when SSO is not available."/>
  <IconUrl DefaultValue="https://localhost:8080/assets/icon-32.png"/>
  <HighResolutionIconUrl DefaultValue="https://localhost:8080/assets/icon-64.png"/>
  <SupportUrl DefaultValue="https://www.contoso.com/help"/>
  <AppDomains>
    <AppDomain>https://www.contoso.com</AppDomain>
  </AppDomains>
  <Hosts>
    <Host Name="Document"/>
  </Hosts>
  <DefaultSettings>
    <SourceLocation DefaultValue="https://localhost:8080/taskpane.html"/>
  </DefaultSettings>
  <Permissions>ReadWriteDocument</Permissions>
  <VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0">
    <Hosts>
      <Host xsi:type="Document">
        <DesktopFormFactor>
          <GetStarted>
            <Title resid="GetStarted.Title"/>
            <Description resid="GetStarted.Description"/>
            <LearnMoreUrl resid="GetStarted.LearnMoreUrl"/>
          </GetStarted>
          <ExtensionPoint xsi:type="PrimaryCommandSurface">
            <OfficeTab id="TabHome">
              <Group id="CommandsGroup">
                <Label resid="CommandsGroup.Label"/>
                <Icon>
                  <bt:Image size="16" resid="Icon.16x16"/>
                  <bt:Image size="32" resid="Icon.32x32"/>
                  <bt:Image size="80" resid="Icon.80x80"/>
                </Icon>
                <Control xsi:type="Button" id="TaskpaneButton">
                  <Label resid="TaskpaneButton.Label"/>
                  <Supertip>
                    <Title resid="TaskpaneButton.Label"/>
                    <Description resid="TaskpaneButton.Tooltip"/>
                  </Supertip>
                  <Icon>
                    <bt:Image size="16" resid="Icon.16x16"/>
                    <bt:Image size="32" resid="Icon.32x32"/>
                    <bt:Image size="80" resid="Icon.80x80"/>
                  </Icon>
                  <Action xsi:type="ShowTaskpane">
                    <TaskpaneId>ButtonId1</TaskpaneId>
                    <SourceLocation resid="Taskpane.Url"/>
                  </Action>
                </Control>
              </Group>
            </OfficeTab>
          </ExtensionPoint>
        </DesktopFormFactor>
      </Host>
    </Hosts>
    <Resources>
      <bt:Images>
        <bt:Image id="Icon.16x16" DefaultValue="https://localhost:8080/assets/icon-16.png"/>
        <bt:Image id="Icon.32x32" DefaultValue="https://localhost:8080/assets/icon-32.png"/>
        <bt:Image id="Icon.80x80" DefaultValue="https://localhost:8080/assets/icon-80.png"/>
      </bt:Images>
      <bt:Urls>
        <bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://go.microsoft.com/fwlink/?LinkId=276812"/>
        <bt:Url id="Commands.Url" DefaultValue="https://localhost:8080/commands.html"/>
        <bt:Url id="Taskpane.Url" DefaultValue="https://localhost:8080/taskpane.html"/>
      </bt:Urls>
      <bt:ShortStrings>
        <bt:String id="GetStarted.Title" DefaultValue="Get started with your sample add-in!"/>
        <bt:String id="CommandsGroup.Label" DefaultValue="Commands Group"/>
        <bt:String id="TaskpaneButton.Label" DefaultValue="Show Taskpane"/>
      </bt:ShortStrings>
      <bt:LongStrings>
        <bt:String id="GetStarted.Description" DefaultValue="Your sample add-in loaded succesfully. Go to the HOME tab and click the 'Show Taskpane' button to get started."/>
        <bt:String id="TaskpaneButton.Tooltip" DefaultValue="Click to Show a Taskpane"/>
      </bt:LongStrings>
    </Resources>
    <WebApplicationInfo>
      <Id>{application GUID here}</Id>
      <Resource>api://localhost:8080/Client GUID</Resource>
      <Scopes>
        <Scope>User.Read</Scope>
        <Scope>profile</Scope>
      </Scopes>
    </WebApplicationInfo>
  </VersionOverrides>
</OfficeApp>

taskpane.html

<!DOCTYPE html> 
<html>
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Contoso Task Pane Add-in</title>
  <link rel="stylesheet" href="https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/9.6.1/css/fabric.min.css" />
  <script src="https://appsforoffice.microsoft.com/lib/beta/hosted/office.js" type="text/javascript"></script>
  <link href="taskpane.css" rel="stylesheet" type="text/css" />
</head>

<body class="ms-font-m ms-welcome ms-Fabric">
    <header class="ms-welcome__header ms-bgColor-neutralLighter">
        <img width="90" height="90" src="../../assets/logo-filled.png" alt="Contoso" title="Contoso" />
        <h1 class="ms-font-su">Welcome</h1>
    </header>
    <main class="ms-firstrun-instructionstep">
        <ul class="ms-List ms-welcome__features">
            <li class="ms-ListItem">
                <i class="ms-Icon ms-Icon--Ribbon ms-font-xl"></i>
                <span class="ms-font-m">Achieve more with Office integration</span>
            </li>
            <li class="ms-ListItem">
                <i class="ms-Icon ms-Icon--Unlock ms-font-xl"></i>
                <span class="ms-font-m">Unlock features and functionality</span>
            </li>
            <li class="ms-ListItem">
                <i class="ms-Icon ms-Icon--Design ms-font-xl"></i>
                <span class="ms-font-m">Create and visualize like a pro</span>
            </li>
        </ul>
        <section class="ms-firstrun-instructionstep__header">
            <h2 class="ms-font-m"> This add-in demonstrates how to use single sign-on by making a call to Microsoft
                Graph to get user profile data.</h2>
            <div class="ms-firstrun-instructionstep__header--image"></div>
        </section>
        <div class="ms-firstrun-instructionstep__welcome-body">
            <p class="ms-font-m ms-firstrun-instructionstep__welcome-intro"><b>To use this add-in:</b></p>
            <ul class="ms-List ms-firstrun-instructionstep__list">
                <li class="ms-ListItem">
                    <span class="ms-ListItem-primaryText">Click the <b>Get My User Profile Information</b>
                        button.</span>
                    <div class="clearfix"></div>
                </li>
                <li class="ms-ListItem">
                    <span class="ms-ListItem-secondaryText">If you are not signed into Office, you are prompted to sign
                        in.</span>
                    <div class="clearfix"></div>
                </li>
                <li class="ms-ListItem">
                    <span class="ms-ListItem-primaryText">You may also be prompted to accept the app's permissions request.</span>
                    <div class="clearfix"></div>
                </li>
                <li class="ms-ListItem">
                    <span class="ms-ListItem-primaryText">Your user profile information will be displayed in the document.</span>
                    <div class="clearfix"></div>
                </li>
            </ul>
            <br>
            <p align="center">
                <button id="getGraphDataButton" class="popupButton ms-Button ms-Button--primary"><span
                        class="ms-Button-label">Get My User Profile Information </span></button>
            </p>
        </div>
    </main>
</body>
</html>

taskpane.js

const ssoAuthHelper = require("./../helpers/ssoauthhelper"); 

Office.onReady((info) => {
  if (info.host === Office.HostType.Word) {
    document.getElementById("getGraphDataButton").onclick = ssoAuthHelper.getGraphData;
  }
});

commands.html

<!DOCTYPE html> 
<html>
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
  <script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.debug.js"></script>
</head>
<body>
</body>
</html>

commands.js

The add-in command functions need to be available in global scope g.action.

Office.onReady(() => { 
});

export function action(event) {
  const message = {
    type: Office.MailboxEnums.ItemNotificationMessageType.InformationalMessage,
    message: "Performed action.",
    icon: "Icon.80x80",
    persistent: true,
  };

  Office.context.mailbox.item.notificationMessages.replaceAsync("action", message);
  event.completed();
}

function getGlobal() {
  return typeof self !== "undefined"
    ? self
    : typeof window !== "undefined"
    ? window
    : typeof global !== "undefined"
    ? global
    : undefined;
}

const g = getGlobal();

helpers/documentHelper.js

contains code that uses the Office JavaScript API library to add the data from Microsoft Graph to the Office document. There is no such file in a TypeScript project; the code that uses the Office JavaScript API library to add the data from Microsoft Graph to the Office document exists in ./src/taskpane/taskpane.ts instead.

export function writeDataToOfficeDocument(result) { 
  return new Promise(function (resolve, reject) {
    try {
      switch (Office.context.host) {
        case Office.HostType.Excel:
          writeDataToExcel(result);
          break;
        case Office.HostType.Outlook:
          writeDataToOutlook(result);
          break;
        case Office.HostType.PowerPoint:
          writeDataToPowerPoint(result);
          break;
        case Office.HostType.Word:
          writeDataToWord(result);
          break;
        default:
          throw "Unsupported Office host application: This add-in only runs on Excel, Outlook, PowerPoint, or Word.";
      }
      resolve();
    } catch (error) {
      reject(Error("Unable to write data to document. " + error.toString()));
    }
  });
}

function filterUserProfileInfo(result) {
  let userProfileInfo = [];
  userProfileInfo.push(result["displayName"]);
  userProfileInfo.push(result["jobTitle"]);
  userProfileInfo.push(result["mail"]);
  userProfileInfo.push(result["mobilePhone"]);
  userProfileInfo.push(result["officeLocation"]);
  return userProfileInfo;
}

function writeDataToExcel(result) {
  return Excel.run(function (context) {
    const sheet = context.workbook.worksheets.getActiveWorksheet();
    let data = [];
    let userProfileInfo = filterUserProfileInfo(result);

    for (let i = 0; i < userProfileInfo.length; i++) {
      if (userProfileInfo[i] !== null) {
        let innerArray = [];
        innerArray.push(userProfileInfo[i]);
        data.push(innerArray);
      }
    }
    const rangeAddress = `B5:B${5 + (data.length - 1)}`;
    const range = sheet.getRange(rangeAddress);
    range.values = data;
    range.format.autofitColumns();

    return context.sync();
  });
}

function writeDataToOutlook(result) {
  let data = [];
  let userProfileInfo = filterUserProfileInfo(result);

  for (let i = 0; i < userProfileInfo.length; i++) {
    if (userProfileInfo[i] !== null) {
      data.push(userProfileInfo[i]);
    }
  }

  let userInfo = "";
  for (let i = 0; i < data.length; i++) {
    userInfo += data[i] + "\n";
  }

  Office.context.mailbox.item.body.setSelectedDataAsync(userInfo, { coercionType: Office.CoercionType.Html });
}

function writeDataToPowerPoint(result) {
  let data = [];
  let userProfileInfo = filterUserProfileInfo(result);

  for (let i = 0; i < userProfileInfo.length; i++) {
    if (userProfileInfo[i] !== null) {
      data.push(userProfileInfo[i]);
    }
  }

  let userInfo = "";
  for (let i = 0; i < data.length; i++) {
    userInfo += data[i] + "\n";
  }
  Office.context.document.setSelectedDataAsync(userInfo, function (asyncResult) {
    if (asyncResult.status === Office.AsyncResultStatus.Failed) {
      throw asyncResult.error.message;
    }
  });
}

function writeDataToWord(result) {
  return Word.run(function (context) {
    let data = [];
    let userProfileInfo = filterUserProfileInfo(result);

    for (let i = 0; i < userProfileInfo.length; i++) {
      if (userProfileInfo[i] !== null) {
        data.push(userProfileInfo[i]);
      }
    }

    const documentBody = context.document.body;
    for (let i = 0; i < data.length; i++) {
      if (data[i] !== null) {
        documentBody.insertParagraph(data[i], "End");
      }
    }
    return context.sync();
  });
}

helpers/fallbackauthdialog.html

is the UI-less page that loads the JavaScript for the fallback authentication strategy.

<!DOCTYPE html> 
<html>
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
  <script type="text/javascript" src="https://ajax.microsoft.com/ajax/4.0/MicrosoftAjax.js"></script>
  <script type="text/javascript" src="https://alcdn.msauth.net/lib/1.1.3/js/msal.min.js"></script>
  <script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.debug.js"></script>
</head>
<body>
</body>
</html>

helpers/fallbackauthdialog.js

contains the JavaScript for the fallback authentication strategy that signs in the user with msal.js.


This file shows how to get an access token to Microsoft Graph and pass it to the task pane.
The very first time the add-in runs on a developer's computer, msal.js hasn't yet stored login data in localStorage.
So a direct call of acquireTokenRedirect causes the error "User login is required".
Once the user is logged in successfully the first time, msal data in localStorage will prevent this error from ever happening again; but the error must be blocked here, so that the user can login successfully the first time.
To do that, call loginRedirect first instead of acquireTokenRedirect.


This will login the user and then the (response.tokenType === "id_token") path in authCallback below will run, which sets localStorage.loggedIn to "yes"
and then the dialog is redirected back to this script, so the acquireTokenRedirect above runs.


const Msal = require("msal"); 

Office.initialize = function () {
  if (Office.context.ui.messageParent) {
    userAgentApp.handleRedirectCallback(authCallback);

    if (localStorage.getItem("loggedIn") === "yes") {
      userAgentApp.acquireTokenRedirect(requestObj);
    } else {
      userAgentApp.loginRedirect(requestObj);
    }
  }
};

const msalConfig = {
  auth: {
    clientId: "Application GUID",
    authority: "https://login.microsoftonline.com/common",
    redirectUri: "https://localhost:{PORT}/fallbackauthdialog.html",
    navigateToLoginRequestUrl: false,
  },
  cache: {
    cacheLocation: "localStorage", // Needed to avoid "User login is required" error.
    storeAuthStateInCookie: true, // Recommended to avoid certain IE/Edge issues.
  },
};

var requestObj = {
  scopes: [`https://graph.microsoft.com/User.Read`],
};

const userAgentApp = new Msal.UserAgentApplication(msalConfig);

function authCallback(error, response) {
  if (error) {
    console.log(error);
    Office.context.ui.messageParent(JSON.stringify({ status: "failure", result: error }));
  } else {
    if (response.tokenType === "id_token") {
      console.log(response.idToken.rawIdToken);
      localStorage.setItem("loggedIn", "yes");
    } else {
      console.log("token type is:" + response.tokenType);
      Office.context.ui.messageParent(JSON.stringify({ status: "success", result: response.accessToken }));
    }
  }
}

helpers/fallbackauthhelper.js

contains the task pane JavaScript that invokes the fallback authentication strategy in scenarios when SSO authentication is not supported.


We fall back to Dialog API for any error.
This handler responds to the success or failure message that the pop-up dialog receives from the identity provider // and access token provider.
Use the Office dialog API to open a pop-up and display the sign-in page for the identity provider.
height and width are percentages of the size of the parent Office application, e.g., PowerPoint, Excel, Word, etc.

const documentHelper = require("./documentHelper"); 
const sso = require("office-addin-sso");
var loginDialog;

export function dialogFallback() {
  const url = "/fallbackauthdialog.html";
  showLoginPopup(url);
}

async function processMessage(arg) {
  console.log("Message received in processMessage: " + JSON.stringify(arg));
  let messageFromDialog = JSON.parse(arg.message);

  if (messageFromDialog.status === "success") {
    // We now have a valid access token.
    loginDialog.close();
    const response = await sso.makeGraphApiCall(messageFromDialog.result);
    documentHelper.writeDataToOfficeDocument(response);
  } else {
    // Something went wrong with authentication or the authorization of the web application.
    loginDialog.close();
    sso.showMessage(JSON.stringify(messageFromDialog.error.toString()));
  }
}

function showLoginPopup(url) {
  var fullUrl = location.protocol + "//" + location.hostname + (location.port ? ":" + location.port : "") + url;

  Office.context.ui.displayDialogAsync(fullUrl, { height: 60, width: 30 }, function (result) {
    console.log("Dialog has initialized. Wiring up events");
    loginDialog = result.value;
    loginDialog.addEventHandler(Office.EventType.DialogMessageReceived, processMessage);
  });
}

helpers/ssoauthhelper.js

contains the JavaScript call to the SSO API, getAccessToken, receives the bootstrap token, initiates the swap of the bootstrap token for an access token to Microsoft Graph, and calls to Microsoft Graph for the data.


Microsoft Graph requires an additional form of authentication. Have the Office host
get a new token using the Claims string, which tells AAD to prompt the user for all required forms of authentication.


On rare occasions the bootstrap token is unexpired when Office validates it,
but expires by the time it is sent to AAD for exchange. AAD will respond
with "The provided value for the 'assertion' is not valid. The assertion has expired."
Retry the call of getAccessToken (no more than once). This time Office will return a new unexpired bootstrap token.

const documentHelper = require("./documentHelper"); 
const fallbackAuthHelper = require("./fallbackAuthHelper");
const sso = require("office-addin-sso");
let retryGetAccessToken = 0;

export async function getGraphData() {
  try {
    let bootstrapToken = await OfficeRuntime.auth.getAccessToken({ allowSignInPrompt: true });
    let exchangeResponse = await sso.getGraphToken(bootstrapToken);
    if (exchangeResponse.claims) {
      let mfaBootstrapToken = await OfficeRuntime.auth.getAccessToken({ authChallenge: exchangeResponse.claims });
      exchangeResponse = sso.getGraphToken(mfaBootstrapToken);
    }

    if (exchangeResponse.error) {
      // AAD errors are returned to the client with HTTP code 200, so they do not trigger
      // the catch block below.
      handleAADErrors(exchangeResponse);
    } else {
      // makeGraphApiCall makes an AJAX call to the MS Graph endpoint. Errors are caught
      // in the .fail callback of that call
      const response = await sso.makeGraphApiCall(exchangeResponse.access_token);
      documentHelper.writeDataToOfficeDocument(response);
      sso.showMessage("Your data has been added to the document.");
    }
  } catch (exception) {
    if (exception.code) {
      if (sso.handleClientSideErrors(exception)) {
        fallbackAuthHelper.dialogFallback();
      }
    } else {
      sso.showMessage("EXCEPTION: " + JSON.stringify(exception));
    }
  }
}

function handleAADErrors(exchangeResponse) {
  if (exchangeResponse.error_description.indexOf("AADSTS500133") !== -1 && retryGetAccessToken <= 0) {
    retryGetAccessToken++;
    getGraphData();
  } else {
    fallbackAuthHelper.dialogFallback();
  }
}

© 2021 Better Solutions Limited. All Rights Reserved. © 2021 Better Solutions Limited TopPrevNext