office-addin-sso

First released in December 2019.
This package provides the ability to register an application in Azure Active Directory and for implementing single sign on task pane add-ins.
link - npmjs.com/package/office-addin-sso
link - github.com/OfficeDev/Office-Addin-Scripts/tree/master/packages/office-addin-sso


msgraph-helper.ts

The "/auth" and "/getuserdata" are server-side Express JS routes that exchanges the bootstrap token with Azure AD for an access token with permissions to Microsoft Graph.

import { ODataHelper } from "./odata-helper"; 
import { showMessage } from "./message-helper";
import * as $ from "jquery";

const domain: string = "graph.microsoft.com";
const versionURLsegment: string = "/v1.0";

export async function getGraphData(
  accessToken: string,
  apiURLsegment: string,
  queryParamsSegment?: string
): Promise<any> {
  try {
    const oData = await ODataHelper.getData(accessToken, domain, apiURLsegment, versionURLsegment, queryParamsSegment);
    return Promise.resolve(oData);
  } catch (err) {
    return Promise.reject(`Error get Graph data. \n${err}`);
  }
}

export async function getGraphToken(bootstrapToken): Promise<any> {
  try {
    let response = await $.ajax({
      type: "GET",
      url: "/auth",
      headers: { Authorization: "Bearer " + bootstrapToken },
      cache: false,
    });
    return response;
  } catch (err) {
    throw new Error(`Error getting Graph token. \n${err}`);
  }
}

export async function makeGraphApiCall(accessToken: string): Promise<any> {
  try {
    const response = await $.ajax({
      type: "GET",
      url: "/getuserdata",
      headers: { access_token: accessToken },
      cache: false,
    });
    return response;
  } catch (err) {
    showMessage(`Error from Microsoft Graph. \n${err}`);
  }
}

odata-helper.ts

import * as https from "https"; 

export class ODataHelper {
  static getData(
    accessToken: string,
    domain: string,
    apiURLsegment: string,
    apiVersion?: string,
    queryParamsSegment?: string
  ) {
    return new Promise<any>((resolve, reject) => {
      const options = {
        host: domain,
        path: apiVersion + apiURLsegment + queryParamsSegment,
        method: "GET",
        headers: {
          "Content-Type": "application/json",
          Accept: "application/json",
          Authorization: "Bearer " + accessToken,
          "Cache-Control": "private, no-cache, no-store, must-revalidate",
          Expires: "-1",
          Pragma: "no-cache",
        },
      };

      https
        .get(options, (response) => {
          let body = "";
          response.on("data", (d) => {
            body += d;
          });
          response.on("end", () => {
            let error;
            if (response.statusCode === 200) {
              let parsedBody = JSON.parse(body);
              resolve(parsedBody);
            } else {
              error = new Error();
              error.code = response.statusCode;
              error.message = response.statusMessage;

              body = body.trim();
              error.bodyCode = JSON.parse(body).error.code;
              error.bodyMessage = JSON.parse(body).error.message;
              resolve(error);
            }
          });
        })
        .on("error", reject);
    });
  }
}

message-helper.ts

export function showMessage(text: string): void { 
  $(".welcome-body").hide();
  $("#message-area").show();
  $("#message-area").text(text);
}

commands.ts

import * as chalk from "chalk"; 
import { parseNumber } from "office-addin-cli";
import { ManifestInfo, OfficeAddinManifest } from "office-addin-manifest";
import { usageDataObject } from "./defaults";
import * as configure from "./configure";
import { SSOService } from "./server";
import { addSecretToCredentialStore, writeApplicationData, applicationDataConfigured } from "./ssoDataSettings";
import { ExpectedError } from "office-addin-usage-data";
import inquirer = require("inquirer");

/* global process, console */

export async function configureSSO(manifestPath: string) {
  // Check platform and return if not Windows or Mac
  if (process.platform !== "win32" && process.platform !== "darwin") {
    console.log(chalk.yellow(`${process.platform} is not supported. Only Windows and Mac are supported`));
    return;
  } else if (applicationDataConfigured(manifestPath)) {
    console.log(chalk.yellow("Project was already previously updated."));
    const question = {
      message: `Continue anyway?`,
      name: "didUserConfirm",
      type: "confirm",
    };
    const answer = await inquirer.prompt([question]);
    if (!answer.didUserConfirm) {
      return;
    }
  }

  const port: number = parseDevServerPort(process.env.npm_package_config_dev_server_port) || 3000;

  // Log start time for configuration process
  const ssoConfigStartTime = new Date().getTime();

  // Check to see if Azure CLI is installed. If it isn't installed, then install it
  const cliInstalled = await configure.isAzureCliInstalled();

  if (!cliInstalled) {
    console.log(
      chalk.yellow("Azure CLI is not installed. Installing now before proceeding - this could take a few minutes.")
    );
    await configure.installAzureCli();
    if (process.platform === "win32") {
      console.log(
        chalk.green(
          "Please close your command shell, reopen and run configure-sso again. This is necessary to register the path to the Azure CLI"
        )
      );
    }
    return;
  }

  console.log("Opening browser for authentication to Azure. Enter valid Azure credentials");
  const userJson: Object = await configure.logIntoAzure();
  if (Object.keys(userJson).length >= 1) {
    console.log("Login was successful!");
    const manifestInfo: ManifestInfo = await OfficeAddinManifest.readManifestFile(manifestPath);

    // Register application in Azure
    console.log("Registering new application in Azure");
    const applicationJson: Object = await configure.createNewApplication(
      manifestInfo.displayName,
      port.toString(),
      userJson
    );

    if (applicationJson) {
      console.log("Application was successfully registered with Azure");
      // Set application IdentifierUri
      console.log("Setting identifierUri");
      await configure.setIdentifierUri(applicationJson, port.toString());

      // Set application sign-in audience
      console.log("Setting signin audience");
      await configure.setSignInAudience(applicationJson);

      // Grant admin consent for application if logged-in user is a tenant admin
      if (await configure.isUserTenantAdmin(userJson)) {
        console.log("Granting admin consent");
        await configure.grantAdminConsent(applicationJson);
        // Check to set if SharePoint reply urls are set for tenant. If not, set them
        const setSharePointReplyUrls: boolean = await configure.setSharePointTenantReplyUrls(
          applicationJson["publisherDomain"].substr(0, applicationJson["publisherDomain"].indexOf("."))
        );
        if (setSharePointReplyUrls) {
          console.log("Set SharePoint reply urls for tenant");
        }
        // Check to set if Outlook reply url is set for tenant. If not, set them
        const setOutlookReplyUrl: boolean = await configure.setOutlookTenantReplyUrl();
        if (setOutlookReplyUrl) {
          console.log("Set Outlook reply url for tenant");
        }
      }

      // Create an application secret and add to the credential store
      console.log("Setting application secret");
      const secret: string = await configure.setApplicationSecret(applicationJson);
      console.log(chalk.green(`App secret is ${secret}`));

      // Add secret to Credential Store (Windows) or Keychain(Mac)
      if (process.platform === "win32") {
        console.log(`Adding application secret for ${manifestInfo.displayName} to Windows Credential Store`);
      } else {
        console.log(`Adding application secret for ${manifestInfo.displayName} to Mac OS Keychain.`);
        console.log('You will need to provide an admin password to update the Keychain');
      }
      addSecretToCredentialStore(manifestInfo.displayName, secret);
    } else {
      const errorMessage = "Failed to register application";
      usageDataObject.reportException("createNewApplication", errorMessage);
      console.log(chalk.red(errorMessage));
      return;
    }
    // Write application data to project files (manifest.xml, .env, src/taskpane/fallbacktaskpane.ts)
    console.log(`Updating source files with application ID and port`);
    const projectUpdated = await writeApplicationData(applicationJson["appId"], port.toString(), manifestPath);
    if (!projectUpdated) {
      console.log(
        chalk.yellow(
          `Project was already previously updated. You will need to update the CLIENT_ID and PORT settings manually`
        )
      );
    }

    // Log out of Azure
    console.log("Logging out of Azure now");
    await configure.logoutAzure();
    console.log(
      chalk.green(Application with id ${applicationJson["appId"]} successfully registered in Azure.`)
      chalk.green('Go to https://ms.portal.azure.com/#home and search for 'App Registrations' to see your application');
    )

    // Log end time for configuration process and compute in seconds
    const ssoConfigEndTime = new Date().getTime();
    const ssoConfigDuration = (ssoConfigEndTime - ssoConfigStartTime) / 1000;

    // Send usage data
    usageDataObject.reportSuccess("configureSSO", {
      configDuration: ssoConfigDuration,
    });
  } else {
    const errorMessage: string = "Login to Azure did not succeed";
    usageDataObject.reportException("configureSSO", errorMessage);
    throw new Error(errorMessage);
  }
}

export async function startSSOService(manifestPath: string) {
  try {
    // Check platform and return if not Windows or Mac
    if (process.platform !== "win32" && process.platform !== "darwin") {
      console.log(chalk.yellow(`${process.platform} is not supported. Only Windows and Mac are supported`));
      throw new ExpectedError(`${process.platform} is not supported. Only Windows and Mac are supported`);
    }
    const sso = new SSOService(manifestPath);
    sso.startSsoService();
    usageDataObject.reportSuccess("startSSOService");
  } catch (err) {
    usageDataObject.reportException("startSSOService", err);
  }
}

function parseDevServerPort(optionValue: any): number | undefined {
  const devServerPort = parseNumber(optionValue, "--dev-server-port should specify a number.");

  if (devServerPort !== undefined) {
    if (!Number.isInteger(devServerPort)) {
      throw new ExpectedError("--dev-server-port should be an integer.");
    }
    if (devServerPort < 0 || devServerPort > 65535) {
      throw new ExpectedError("--dev-server-port should be between 0 and 65535.");
    }
  }

  return devServerPort;
}

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