Skip to content

Commit

Permalink
Merge pull request #854 from OfficeDev/davech-naa-fb-simple
Browse files Browse the repository at this point in the history
[All hosts] NAA - add fallback support
  • Loading branch information
codexeon authored Sep 28, 2024
2 parents 91a467b + f3b7faa commit a5097bf
Show file tree
Hide file tree
Showing 14 changed files with 522 additions and 186 deletions.
55 changes: 37 additions & 18 deletions Samples/auth/Outlook-Add-in-SSO-NAA/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ This sample shows how to use MSAL.js nested app authentication (NAA) in an Outlo
## Features

- Use MSAL.js NAA to get an access token to call Microsoft Graph APIs.
- Use MSAL.js NAA to get information about the user signed in to Office.
- Fall back to using the Office dialog API for auth when NAA unavailable.

## Applies to

- Outlook (Current Channel (previeiw) for classic Outlook only, new Outlook coming soon).
- Outlook (Current Channel (preview) for classic Outlook only, new Outlook coming soon).
- Outlook on the web.

For more information on supported platforms, see [NAA supported accounts and hosts](https://learn.microsoft.com/office/dev/add-ins/develop/enable-nested-app-authentication-in-your-add-in#naa-supported-accounts-and-hosts).
Expand All @@ -47,15 +47,20 @@ For more information on supported platforms, see [NAA supported accounts and hos
### Create an application registration

1. Go to the [Azure portal - App registrations](https://go.microsoft.com/fwlink/?linkid=2083908) page to register your app.
1. Sign in with the ***admin*** credentials to your Microsoft 365 tenancy. For example, **[email protected]**.
1. Sign in with the **_admin_** credentials to your Microsoft 365 tenancy. For example, **[email protected]**.
1. Select **New registration**. On the **Register an application** page, set the values as follows.

- Set **Name** to `Outlook-Add-in-SSO-NAA`.
- Set **Supported account types** to **Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)**.
- In the **Redirect URI** section, ensure that **Single-page application (SPA)** is selected in the drop down and then set the URI to `brk-multihub://localhost:3000`.
- Select **Register**.
- Set **Name** to `Outlook-Add-in-SSO-NAA`.
- Set **Supported account types** to **Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)**.
- In the **Redirect URI** section, ensure that **Single-page application (SPA)** is selected in the drop down and then set the URI to `brk-multihub://localhost:3000`. This allows Office to broker the auth request.
- Select **Register**.

1. On the **Outlook-Add-in-SSO-NAA** page, copy and save the value for the **Application (client) ID**. You'll use it in the next section.
1. Under **Manage** select **Authentication**.
1. In the **Single-page application** pane, select **Add URI**.
1. Enter the value `https://localhost:3000/auth.html` and select **Save**. This redirect handles the fallback scenario when browser auth is used from add-in.
1. In the **Single-page application** pane, select **Add URI**.
1. Enter the value `https://localhost:3000/dialog.html` and select **Save**. This redirect handles the fallback scenario when the Office dialog API is used.

For more information on how to register your application, see [Register an application with the Microsoft Identity Platform](https://learn.microsoft.com/graph/auth-register-app-v2).

Expand All @@ -71,18 +76,18 @@ For more information on how to register your application, see [Register an appli

1. Run the following commands.

`npm install`
`npm run start`
`npm install`
`npm run start`

This will start the web server and sideload the add-in to Outlook.
This will start the web server and sideload the add-in to Outlook.

1. In Outlook, compose a new email message.
1. On the ribbon for the message, look for the **Show task pane** button and select it.
1. When the task pane opens, there are two buttons: **Get user data** and **Get user files**.
1. To see the signed in user's name and email, select **Get user data**.
1. To insert the first 10 filenames from the signed in user's Microsoft OneDrive, select **Get user files**.

You will be prompted to consent to the scopes the sample needs when you select the buttons.
You will be prompted to consent to the scopes the sample needs when you select the buttons.

## Debugging steps

Expand All @@ -102,24 +107,38 @@ The `src/taskpane/authConfig.ts` file contains the MSAL code for configuring and

- The `initialize` function is called from Office.onReady to configure and intitialize MSAL to use NAA.
- The `ssoGetAccessToken` function gets an access token for the signed in user to call Microsoft Graph APIs.
- The `ssoGetUserAccount` function gets the account information of the signed in user. This can be used to get user details such as name and email.
- The `getTokenWithDialogApi` function uses the Office dialog API to support a fallback option if NAA fails.

The `src/taskpane/taskpane.ts` file contains code that runs when the user chooses buttons in the task pane. They use the AccountManager class to get tokens or user information depending on which button is chosen.
The `src/taskpane/taskpane.ts` file contains code that runs when the user chooses buttons in the task pane. It uses the AccountManager class to get tokens or user information depending on which button is chosen.

The `src/taskpane/msgraph-helper.ts` file contains code to construct and make a REST call to the Microsoft Graph API.

### Fallback code

The `fallback` folder contains files to fall back to an alternate authentication method if NAA is unavailable and fails. When your code calls `acquireTokenSilent`, and NAA is unavailable, an error is thrown. The next step is the code calls `acquireTokenPopup`. MSAL then attempts to sign in the user by opening a dialog box with `window.open` and `about:blank`. Some older Outlook clients don't support the `about:blank` dialog box and cause the `aquireTokenPopup` method to fail. You can catch this error and fall back to using the Office dialog API to open the auth dialog instead.

- the `src/taskpane/authconfig.ts` file contains the following code to detect the error and fall back to using the Office dialog API.

```typescript
// Optional fallback if about:blank popup should not be shown
if (popupError instanceof BrowserAuthError && popupError.errorCode === "popup_window_error") {
const accessToken = await this.getTokenWithDialogApi();
return accessToken;
```
- The `src/taskpane/fallback/fallbackauthdialog.ts` file contains code to initialize MSAL and acquire an access token. It sends the access token back to the task pane.
## Security reporting
If you find a security issue with our libraries or services, report the issue to [[email protected]](mailto:[email protected]) with as much detail as you can provide. Your submission may be eligible for a bounty through the [Microsoft Bounty](https://aka.ms/bugbounty) program. Don't post security issues to [GitHub Issues](https://github.com/AzureAD/microsoft-authentication-library-for-android/issues) or any other public site. We'll contact you shortly after receiving your issue report. We encourage you to get new security incident notifications by visiting [Microsoft technical security notifications](https://technet.microsoft.com/security/dd252948) to subscribe to Security Advisory Alerts.
## More resources
- NAA public preview blog: https://aka.ms/NAApreviewblog
- NAA public preview blog: https://aka.ms/NAApreviewblog
- [Updates on deprecating legacy Exchange Online tokens for Outlook add-ins](https://devblogs.microsoft.com/microsoft365dev/updates-on-deprecating-legacy-exchange-online-tokens-for-outlook-add-ins/?commentid=1131)
- NAA docs to get started: https://aka.ms/NAAdocs
- NAA FAQ: https://aka.ms/NAAFAQ
- NAA Outlook sample: https://aka.ms/NAAsampleOutlook
- NAA Word, Excel, and PowerPoint sample: https://aka.ms/NAAsampleOffice
- NAA docs to get started: https://aka.ms/NAAdocs
- NAA FAQ: https://aka.ms/NAAFAQ
- NAA Word, Excel, and PowerPoint sample: https://aka.ms/NAAsampleOffice
## Questions and feedback
Expand Down
192 changes: 125 additions & 67 deletions Samples/auth/Outlook-Add-in-SSO-NAA/src/taskpane/authConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,62 +3,70 @@

/* This file provides MSAL auth configuration to get access token through nested app authentication. */

/* global console*/
/* global Office, console, document*/

import {
BrowserAuthError,
createNestablePublicClientApplication,
type IPublicClientApplication,
Configuration,
LogLevel,
} from "@azure/msal-browser";
import { msalConfig } from "./msalconfig";
import { createLocalUrl } from "./util";
import { getTokenRequest } from "./msalcommon";

export { AccountManager };

const applicationId = "Enter_the_Application_Id_Here";

function getMsalConfig(enableDebugLogging: boolean) {
const msalConfig: Configuration = {
auth: {
clientId: applicationId,
authority: "https://login.microsoftonline.com/common",
},
system: {},
};
if (enableDebugLogging) {
if (msalConfig.system) {
msalConfig.system.loggerOptions = {
logLevel: LogLevel.Verbose,
loggerCallback: (level: LogLevel, message: string) => {
switch (level) {
case LogLevel.Error:
console.error(message);
return;
case LogLevel.Info:
console.info(message);
return;
case LogLevel.Verbose:
console.debug(message);
return;
case LogLevel.Warning:
console.warn(message);
return;
}
},
piiLoggingEnabled: true,
};
}
}
return msalConfig;
}
export type AuthDialogResult = {
accessToken?: string;
error?: string;
};

type DialogEventMessage = { message: string; origin: string | undefined };
type DialogEventError = { error: number };
type DialogEventArg = DialogEventMessage | DialogEventError;

// Encapsulate functions for getting user account and token information.
class AccountManager {
export class AccountManager {
private pca: IPublicClientApplication | undefined = undefined;
private _dialogApiResult: Promise<string> | null = null;
private _usingFallbackDialog = false;

private getSignOutButton() {
return document.getElementById("signOutButton");
}

private setSignOutButtonVisibility(isVisible: boolean) {
const signOutButton = this.getSignOutButton();
if (signOutButton) {
signOutButton.style.visibility = isVisible ? "visible" : "hidden";
}
}

private isNestedAppAuthSupported() {
return Office.context.requirements.isSetSupported("NestedAppAuth", "1.1");
}

// Initialize MSAL public client application.
async initialize() {
// Make sure office.js is initialized
await Office.onReady();

// If auth is not working, enable debug logging to help diagnose.
this.pca = await createNestablePublicClientApplication(getMsalConfig(false));
this.pca = await createNestablePublicClientApplication(msalConfig);

// If Office does not support Nested App Auth provide a sign-out button since the user selects account
if (!this.isNestedAppAuthSupported() && this.pca.getActiveAccount()) {
this.setSignOutButtonVisibility(true);
}
this.getSignOutButton()?.addEventListener("click", () => this.signOut());
}

private async signOut() {
if (this._usingFallbackDialog) {
await this.signOutWithDialogApi();
} else {
await this.pca?.logoutPopup();
}

this.setSignOutButtonVisibility(false);
}

/**
Expand All @@ -67,46 +75,96 @@ class AccountManager {
* @returns An access token.
*/
async ssoGetAccessToken(scopes: string[]) {
const userAccount = await this.ssoGetUserAccount(scopes);
return userAccount.accessToken;
}
if (this._dialogApiResult) {
return this._dialogApiResult;
}

/**
*
* Uses MSAL and nested app authentication to get the user account from Office SSO.
*
* @param scopes The minimum scopes needed.
* @returns The user account information from MSAL.
*/
async ssoGetUserAccount(scopes: string[]) {
if (this.pca === undefined) {
throw new Error("AccountManager is not initialized!");
}

// Specify minimum scopes needed for the access token.
const tokenRequest = {
scopes: scopes,
};

try {
console.log("Trying to acquire token silently...");
const authResult = await this.pca.acquireTokenSilent(tokenRequest);
const authResult = await this.pca.acquireTokenSilent(getTokenRequest(scopes, false));
console.log("Acquired token silently.");
return authResult;
return authResult.accessToken;
} catch (error) {
console.log(`Unable to acquire token silently: ${error}`);
console.warn(`Unable to acquire token silently: ${error}`);
}

// Acquire token silent failure. Send an interactive request via popup.
try {
console.log("Trying to acquire token interactively...");
const authResult = await this.pca.acquireTokenPopup(tokenRequest);
const selectAccount = this.pca.getActiveAccount() ? false : true;
const authResult = await this.pca.acquireTokenPopup(getTokenRequest(scopes, selectAccount));
console.log("Acquired token interactively.");
return authResult;
if (selectAccount) {
this.pca.setActiveAccount(authResult.account);
}
if (!this.isNestedAppAuthSupported()) {
this.setSignOutButtonVisibility(true);
}
return authResult.accessToken;
} catch (popupError) {
// Acquire token interactive failure.
console.log(`Unable to acquire token interactively: ${popupError}`);
throw new Error(`Unable to acquire access token: ${popupError}`);
// Optional fallback if about:blank popup should not be shown
if (popupError instanceof BrowserAuthError && popupError.errorCode === "popup_window_error") {
const accessToken = await this.getTokenWithDialogApi();
return accessToken;
} else {
// Acquire token interactive failure.
console.error(`Unable to acquire token interactively: ${popupError}`);
throw new Error(`Unable to acquire access token: ${popupError}`);
}
}
}

/**
* Gets an access token by using the Office dialog API to handle authentication. Used for fallback scenario.
* @returns The access token.
*/
async getTokenWithDialogApi(): Promise<string> {
this._dialogApiResult = new Promise((resolve, reject) => {
Office.context.ui.displayDialogAsync(createLocalUrl(`dialog.html`), { height: 60, width: 30 }, (result) => {
result.value.addEventHandler(Office.EventType.DialogEventReceived, (arg: DialogEventArg) => {
const errorArg = arg as DialogEventError;
if (errorArg.error == 12006) {
this._dialogApiResult = null;
reject("Dialog closed");
}
});
result.value.addEventHandler(Office.EventType.DialogMessageReceived, (arg: DialogEventArg) => {
const messageArg = arg as DialogEventMessage;
const parsedMessage = JSON.parse(messageArg.message);
result.value.close();

if (parsedMessage.error) {
reject(parsedMessage.error);
this._dialogApiResult = null;
} else {
resolve(parsedMessage.accessToken);
this.setSignOutButtonVisibility(true);
this._usingFallbackDialog = true;
}
});
});
});
return this._dialogApiResult;
}

signOutWithDialogApi(): Promise<void> {
return new Promise((resolve) => {
Office.context.ui.displayDialogAsync(
createLocalUrl(`dialog.html?logout=1`),
{ height: 60, width: 30 },
(result) => {
result.value.addEventHandler(Office.EventType.DialogMessageReceived, () => {
this.setSignOutButtonVisibility(false);
this._dialogApiResult = null;
resolve();
result.value.close();
});
}
);
});
}
}
Loading

0 comments on commit a5097bf

Please sign in to comment.