office-addin-sso

First released in December 2019 and has a significant update in August 2022.
This package provides the ability to register an application in Azure Active Directory as well as helper files for implementing single sign on task pane add-ins.
This package should only be used for development purposes and should not be used in a Production environment.

link - npmjs.com/package/office-addin-sso 
link - github.com/OfficeDev/Office-Addin-Scripts/tree/master/packages/office-addin-sso

August 2022 Changes

The original sample was returning a middle tier token back to the client which is clearly a bug.

link - youtube.com/watch?v=UcZ2aUMf8f4 

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

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