Word with TypeScript SSO

You can use the Yeoman Generator to create a Word Add-in project using VS Code with TypeScript SSO
The link above displays the contents of these three files:

package.json 
webpack.config.js
manifest.xml

This page displays all the other important files in this project:

.ENV 
src/helpers/fallbackauthdialog.html
src/helpers/fallbackauthdialog.ts
src/helpers/fallbackauthhelper.ts
src/helpers/ssoauthhelper.ts
src/taskpane/taskpane.html
src/taskpane/taskpane.ts
src/commands/commands.html
src/commands/commands.ts
babel.config.json
tsconfig.json

.ENV

CLIENT_ID={Application GUID} 
GRAPH_URL_SEGMENT=/me
NODE_ENV=development
PORT={PORT}
QUERY_PARAM_SEGMENT=
SCOPE=User.Read

fallbackauthdialog.html

<!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>

fallbackauthdialog.ts

/* global console, localStorage, Office */ 
import { Configuration, PublicClientApplication, RedirectRequest } from "@azure/msal-browser";

const msalConfig: Configuration = {
  auth: {
    clientId: "{application GUID here}", //This is your client ID
    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.
  },
};

const loginRequest: RedirectRequest = {
  scopes: [`https://graph.microsoft.com/User.Read`],
};

const publicClientApp: PublicClientApplication = new PublicClientApplication(msalConfig);

Office.onReady(() => {
  if (Office.context.ui.messageParent) {
    publicClientApp
      .handleRedirectPromise()
      .then(handleResponse)
      .catch((error) => {
        console.log(error);
        Office.context.ui.messageParent(JSON.stringify({ status: "failure", result: error }));
      });

    // 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 hap-
    // pening 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.
    if (localStorage.getItem("loggedIn") === "yes") {
      publicClientApp.acquireTokenRedirect(loginRequest);
    } else {
      // 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.
      publicClientApp.loginRedirect(loginRequest);
    }
  }
});

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

fallbackauthhelper.ts

/* global console, location, Office */ 
import * as sso from "office-addin-sso";

let loginDialog: Office.Dialog = null;
let callbackFunction = null;

export function dialogFallback(callback) {
  callbackFunction = callback;

  // We fall back to Dialog API for any error.
  const url = "/fallbackauthdialog.html";
  showLoginPopup(url);
}

// This handler responds to the success or failure message that the pop-up dialog receives from the identity provider
// and access token provider.
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);
    callbackFunction(response);
  } else if (messageFromDialog.error === undefined && messageFromDialog.result.errorCode === undefined) {
    // Need to pick the user to use to auth
  } else {
    // Something went wrong with authentication or the authorization of the web application.
    loginDialog.close();
    if (messageFromDialog.error) {
      sso.showMessage(JSON.stringify(messageFromDialog.error.toString()));
    }
  }
}

// Use the Office dialog API to open a pop-up and display the sign-in page for the identity provider.
function showLoginPopup(url) {
  var fullUrl = location.protocol + "//" + location.hostname + (location.port ? ":" + location.port : "") + url;

  // height and width are percentages of the size of the parent Office application, e.g., PowerPoint, Excel, Word, etc.
  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);
  });
}

ssoauthhelper.ts

import { dialogFallback } from "./fallbackauthhelper"; 
import { handleClientSideErrors, makeGraphApiCall, showMessage } from "office-addin-sso";

/* global OfficeRuntime */

let retryGetAccessToken = 0;

export async function getGraphData(callback): Promise<void> {
  try {
    let bootstrapToken: string = await OfficeRuntime.auth.getAccessToken({ allowSignInPrompt: true });
    let response: any = await makeGraphApiCall(bootstrapToken);
    if (response.claims) {
      // 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.
      let mfaBootstrapToken: string = await OfficeRuntime.auth.getAccessToken({
        authChallenge: response.claims,
      });
      response = makeGraphApiCall(mfaBootstrapToken);
    }

    // AAD errors are returned to the client with HTTP code 200, so they do not trigger
    // the catch block below.
    if (response.error) {
      handleAADErrors(response, callback);
    } else {
      // makeGraphApiCall makes an AJAX call to the MS Graph endpoint. Errors are caught
      // in the .fail callback of that call
      callback(response);
      Promise.resolve();
    }
  } catch (exception) {
    // if handleClientSideErrors returns true then we will try to authenticate via the fallback
    // dialog rather than simply throw and error
    if (exception.code) {
      if (handleClientSideErrors(exception)) {
        dialogFallback(callback);
      }
    } else {
      showMessage("EXCEPTION: " + JSON.stringify(exception));
      Promise.reject();
    }
  }
}

function handleAADErrors(response: any, callback: any): void {
  // 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.

  if (response.error_description.indexOf("AADSTS500133") !== -1 && retryGetAccessToken <= 0) {
    retryGetAccessToken++;
    getGraphData(callback);
  } else {
    dialogFallback(callback);
  }
}

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 type="text/javascript" src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.2.1.min.js"></script>
<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="getProfileButton" 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.ts

/* global $, document, Excel, Office */ 
import { getGraphData } from "./../helpers/ssoauthhelper";

Office.onReady((info) => {
  if (info.host === Office.HostType.Word) {
    document.getElementById("getProfileButton").onclick = run;
  }
});

export async function run() {
  getGraphData(writeDataToOfficeDocument);
}

export function writeDataToOfficeDocument(result: Object): Promise<any> {
  return Word.run(function (context) {
    let data: string[] = [];
    let userProfileInfo: string[] = [];
    userProfileInfo.push(result["displayName"]);
    userProfileInfo.push(result["jobTitle"]);
    userProfileInfo.push(result["mail"]);
    userProfileInfo.push(result["mobilePhone"]);
    userProfileInfo.push(result["officeLocation"]);

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

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

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.js"></script>
</head>
<body>
</body>
</html>

commands.ts

/* global global, Office, self, window */ 

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

export function action(event: Office.AddinCommands.Event) {
const message: Office.NotificationMessageDetails = {
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() as any;
g.action;

babel.config.json

{ 
"presets": ["@babel/preset-typescript"]
}

tsconfig.json

{ 
"compilerOptions": {
"allowJs": true,
"baseUrl": ".",
"esModuleInterop": true,
"experimentalDecorators": true,
"jsx": "react",
"noEmitOnError": true,
"outDir": "lib",
"sourceMap": true,
"target": "es5",
"lib": [
"es2015",
"dom"
]
},
"exclude": [
"node_modules",
"dist",
"lib",
"lib-amd",
],
"ts-node": {
"files": true
}
}

Previous

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();
  });
}

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