Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[All hosts](security) Update NAA article to support a walkthrough #4519

Merged
merged 19 commits into from
May 10, 2024
Merged
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
272 changes: 172 additions & 100 deletions docs/develop/enable-nested-app-authentication-in-your-add-in.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
---
title: Enable SSO in an Office Add-in using nested app authentication
description: Learn how to enable SSO in an Office Add-in using nested app authentication.
ms.date: 04/09/2024
ms.date: 04/12/2024
ms.topic: how-to
ms.localizationpriority: high

---

# Enable SSO in an Office Add-in using nested app authentication (preview)
Expand All @@ -18,6 +17,12 @@ You can use the MSAL.js library (version 3.11 and later) with nested app authent

> [!IMPORTANT]
> Nested app authentication (NAA) is currently in preview. To try this feature, join the Microsoft 365 Insider Program (https://insider.microsoft365.com/join) and choose the Beta Channel. Don't use NAA in production add-ins. We invite you to try out NAA in test or development environments and welcome feedback on your experience through GitHub (see the **Feedback** section at the end of this page).
> NAA is supported in the following builds.
>
> - Word, Excel, and PowerPoint on Windows build 16.0.17531.20000 or later.
> - Word, Excel, and PowerPoint on Mac build 16.85.24040319 or later.
> - Outlook on Windows build 16.0.17531.20000 or later.
> - Outlook on Mac build 16.85.24040319 or later.
davidchesnut marked this conversation as resolved.
Show resolved Hide resolved

## Register your single-page application

Expand All @@ -41,30 +46,52 @@ Trusted broker groups are dynamic by design and can be updated in the future to

Configure your add-in to use NAA by setting the `supportsNestedAppAuth` property to true in your MSAL configuration. This enables MSAL to use APIs on its native application host (for example, Outlook) to acquire tokens for your application. If you don't set this property, MSAL uses the default JavaScript-based implementation to acquire tokens for your application, which may lead to unexpected auth prompts and unsatisfiable conditional access policies when running inside of a webview.

```JavaScript
// Configuration for NAA.
The following steps show how to enable NAA in the `taskpane.js` or `taskpane.ts` file in a project built with `yo office`. The code can be adapted to any project.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it really be adapted to any project? Would it apply to the SSO project? To the React project? The latter doesn't have a taskpane.js or taskpane.ts file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll work to reword that better.


1. Add the `@azure/msal-browser` package to the `dependencies` section of the `package.json` file for your project.

```json
"dependencies": {
"@azure/msal-browser": "^3.11.1",
...
```

1. Save and run `npm install` to install `@azure/msal-browser`.

const msalConfig = {
auth: {
clientId: "Enter_the_Application_Id_Here",
authority: "https://login.microsoftonline.com/common",
supportsNestedAppAuth: true
}
}
```
1. Add the following code to the top of the `taskpane.js` or `taskpane.ts` file. Replace the `Enter_the_Application_Id_Here` placeholder with the Azure app ID you saved previously.

```JavaScript
import { PublicClientNext } from "@azure/msal-browser";

// Configuration for NAA.
const msalConfig = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The msalConfig const is only called in the onReady method, so why not declare it there too?

auth: {
clientId: "Enter_the_Application_Id_Here",
authority: "https://login.microsoftonline.com/common",
supportsNestedAppAuth: true
}
}
let pca = undefined; // public client application to be initialized later.
```

## Initialize the public client application

Next, you need to initialize MSAL and get an instance of the public client application. This is used to get access tokens when needed. It's recommended to create the public client application in the `Office.onReady` method.
davidchesnut marked this conversation as resolved.
Show resolved Hide resolved

```javascript
let pca = undefined;
- In your `Office.onReady` function, add a call to `createPublicClientApplication` as shown below to initilize the `pca` variable.

// Initialize the publice client application
Office.onReady(async (info) => {
pca = await msalBrowser.PublicClientNext.createPublicClientApplication(msalConfig);
```javascript
Office.onReady(async (info) => {
if (info.host === Office.HostType.Excel) {
document.getElementById("sideload-msg").style.display = "none";
document.getElementById("app-body").style.display = "flex";
document.getElementById("run").onclick = run;

// Initialize the publice client application
davidchesnut marked this conversation as resolved.
Show resolved Hide resolved
pca = await PublicClientNext.createPublicClientApplication(msalConfig);
}
});
```
```

## Acquire your first token

Expand All @@ -76,114 +103,159 @@ The following steps show the pattern to use for acquiring a token.
1. Call `acquireTokenSilent`. This will get the token without requiring user interaction.
1. If `acquireTokenSilent` fails, call `acquireTokenPopup` to display an interactive dialog for the user. `acquireTokenSilent` can fail if the token expired, or the user has not yet consented to all of the requested scopes.

```JavaScript
async function run() {
// Specify minimum scopes needed for the access token.
const tokenRequest = {
scopes: ["User.Read", "openid", "profile"],
loginHint: myloginHint
}

try {
const userAccount = await pca.acquireTokenSilent(tokenRequest);
// Call your API with the token.
makeMSGraphCall(userAccount.accessToken);
} catch (error) {
// Acquire token silent failure. Send an interactive request via popup.
try {
const userAccount = pca.acquireTokenPopup(tokenRequest);
// Call your API with the token.
makeMSGraphCall(userAccount.accessToken);
} catch (popupError) {
// Acquire token interactive failure.
console.log(popupError);
}
}
}
```
The following code shows how to implement this authentication pattern in your own project.

1. Replace the `run` function in `taskpane.js` or `taskpane.ts` with the following code. The code specifies the minimum scopes needed to read the user's files.

```javascript
async function run() {
// Specify minimum scopes needed for the access token.
const tokenRequest = {
scopes: ["Files.Read", "User.Read", "openid", "profile"],
};
let accessToken = null;

// TODO 2: Call acquireTokenSilent.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put all the TODOs here, so they provide a kind of overview of what is going to be done. That's how we do it in other walkthroughs.

Also, there was no TODO 1, so it looks like the numbering is off.

}
```

1. Replace `TODO 2` with the following code. This code calls `acquireTokenSilent` to get the access token.

```JavaScript
try {
console.log("Trying to acquire token silently...");
const userAccount = await pca.acquireTokenSilent(tokenRequest);
console.log("Acquired token silently.");
accessToken = userAccount.accessToken;
} catch (error) {
console.log(`Unable to acquire token silently: ${error}`);
}

// TODO 3: Call acquireTokenPopup.
```

1. Replace `TODO 3` with the following code. This code checks if the access token is acquired. If not it attemps to interactively get the access token by calling `acquireTokenPopup`.

```javascript
if (accessToken === null) {
// Acquire token silent failure. Send an interactive request via popup.
try {
console.log("Trying to acquire token interactively...");
const userAccount = await pca.acquireTokenPopup(tokenRequest);
console.log("Acquired token interactively.");
accessToken = userAccount.accessToken;
} catch (popupError) {
// Acquire token interactive failure.
console.log(`Unable to acquire token interactively: ${popupError}`);
}
}

// TODO 4: Log error if token still null.
```

1. Replace `TODO 4` with the following code. If both silent and interactive sign-in failed, log the error and return.

```javascript
// Log error if both silent and popup requests failed.
if (accessToken === null) {
console.error(`Unable to acquire access token.`);
return;
}

// TODO 5: Call the Microsoft Graph API.
```

## Call an API

After acquiring the token, use it to call an API. The following example shows how to call the Microsoft Graph API by calling `fetch` with the token attached in the *Authorization* header.

```javascript
async function makeMSGraphCall(accessToken) {
const requestString = "https://graph.microsoft.com/v1.0/me";
const headersInit = { 'Authorization': accessToken };
const requestInit = { 'headers': headersInit }

// Make REST call to MS Graph.
const result = await fetch(requestString, requestInit);
if (result.ok) {
const data = await result.text();
console.log(data);
document.getElementById("userInfo").innerText = data;
} else {
// Likely an MS Graph error if result was not ok.
// Error details are in the body.
const response = await result.text();
console.log(response);
}
}
```
After acquiring the token, use it to call an API. The following example shows how to call the Microsoft Graph API by calling `fetch` with the token attached in the _Authorization_ header.

- Replace `TODO 5` with the following code.

```javascript
// Call the Microsoft Graph API with the access token.
const response = await fetch(
`https://graph.microsoft.com/v1.0/me/drive/root/children?$select=name&$top=10`,
{
headers: { Authorization: accessToken },
}
);

if (response.ok) {
// Write file names to the debug console.
const data = await response.json();
const names = data.value.map((item) => item.name);
names.forEach((name) => {
console.log(name);
});
} else {
const errorText = await response.text();
console.error("Microsoft Graph call failed - error text: " + errorText);
}
```

Once all the previous code is added to the `run` function, be sure a button on the task pane calls the `run` function. Then you can sideload the add-in and try out the code.

## What is nested app authentication

Nested app authentication enables SSO for applications that are nested inside of supported first-party applications. For example, Excel on Windows runs your add-in inside a webview. In this scenario, your add-in is a nested application running inside Excel, which is the host. NAA also supports nested apps in Teams. For example, if a Teams tab is hosting Excel, and your add-in is loaded, it is nested inside Excel, which is also nested inside Teams. Again, NAA supports this nested scenario and you can access SSO to get user identity and access tokens of the signed in user.
Nested app authentication enables SSO for applications that are nested inside of supported Microsoft applications. For example, Excel on Windows runs your add-in inside a webview. In this scenario, your add-in is a nested application running inside Excel, which is the host. NAA also supports nested apps in Teams. For example, if a Teams tab is hosting Excel, and your add-in is loaded, it is nested inside Excel, which is also nested inside Teams. Again, NAA supports this nested scenario and you can access SSO to get user identity and access tokens of the signed in user.

## NAA supported accounts and hosts

NAA supports both Microsoft Accounts and Microsoft Entra ID (work/school) identities. It doesn’t support B2C scenarios. For preview, NAA is supported in Office on Windows and Mac. For GA, NAA will also support Office on the web, iOS, and Outlook Mobile on Android and iOS.

## Best practices

The following are some best practices when using MSAL.js with NAA.
We recommend the following best practices when using MSAL.js with NAA.

### Use silent authentication whenever possible

MSAL.js provides the `acquireTokenSilent` method that handles token renewal by making silent token requests without prompting the user. The method first looks for a valid cached token. If it doesn't find one, the library makes the silent request to Microsoft Entra ID and if there's an active user session, a fresh token is returned.
MSAL.js provides the `acquireTokenSilent` method that handles token renewal by making silent token requests without prompting the user. The method first looks for a valid cached token. If it doesn't find one, the library makes the silent request to Microsoft Entra ID and if there's an active user session, a fresh token is returned.

In certain cases, the `acquireTokenSilent` method's attempt to get the token fails. Some examples of this are when there's an expired user session with Microsoft Entra ID or a password change by the user, which requires user interaction. When the acquireTokenSilent fails, you need to call the interactive `acquireTokenPopup` token method.

### Have a fallback when NAA isn't supported

While we strive to provide a high-degree of compatibility with these flows across the Microsoft ecosystem, your application may appear in downlevel/legacy clients that haven't been updated to support NAA. In these cases, your application won't support seamless SSO and you may need to invoke special APIs for interacting with the user to open authentication dialogs. For more information, see [Authenticate and authorize with the Office dialog API](/office/dev/add-ins/develop/auth-with-office-dialog-api).
While we strive to provide a high-degree of compatibility with these flows across the Microsoft ecosystem, your add-in may be loaded in an older Office host that does not support NAA. In these cases, your add-in won't support seamless SSO and you may need to fall back to an alternate method of authenticating the user. In generaly you'll want to use the MSAL SPA authentication pattern with the Office JS dialog API. For more information, see the following resources.
davidchesnut marked this conversation as resolved.
Show resolved Hide resolved

- [Authenticate and authorize with the Office dialog API](/office/dev/add-ins/develop/auth-with-office-dialog-api).
- [Microsoft identity sample for SPA and JavaScript](https://github.com/Azure-Samples/ms-identity-javascript-tutorial/blob/main/2-Authorization-I/1-call-graph/README.md)
- [Microsoft identity samples for various app types and frameworks](/entra/identity-platform/sample-v2-code?tabs=apptype)

## MSAL.js APIs supported by NAA

The following table shows which APIs are supported when NAA is enabled in the MSAL config.

| Method | Supported by NAA |
|-------------------------------|------------------|
| *acquireTokenByCode* | NO (throws exception) |
| *acquireTokenPopup* | YES |
| *acquireTokenRedirect* | NO (throws exception) |
| *acquireTokenSilent* | YES |
| *addEventCallback* | YES |
| *addPerformanceCallback* | YES |
| *disableAccountStorageEvents* | NO (throws exception) |
| *enableAccountStorageEvents* | NO (throws exception) |
| *getAccountByHomeId* | YES |
| *getAccountByLocalId* | YES |
| *getAccountByUsername* | YES |
| *getActiveAccount* | YES |
| *getAllAccounts* | YES |
| *getConfiguration* | YES |
| *getLogger* | YES |
| *getTokenCache* | NO (throws exception) |
| *handleRedirectPromise* | NO |
| *initialize* | YES |
| *initializeWrapperLibrary* | YES |
| *loginPopup* | YES |
| *loginRedirect* | NO (throws exception) |
| *logout* | NO (throws exception) |
| *logoutPopup* | NO (throws exception) |
| *logoutRedirect* | NO (throws exception) |
| *removeEventCallback* | YES |
| *removePerformanceCallback* | YES |
| *setActiveAccount* | NO |
| *setLogger* | YES |
| *ssoSilent* | YES |
| Method | Supported by NAA |
| ----------------------------- | --------------------- |
| _acquireTokenByCode_ | NO (throws exception) |
| _acquireTokenPopup_ | YES |
| _acquireTokenRedirect_ | NO (throws exception) |
| _acquireTokenSilent_ | YES |
| _addEventCallback_ | YES |
| _addPerformanceCallback_ | NO (throws exception) |
| _disableAccountStorageEvents_ | NO (throws exception) |
| _enableAccountStorageEvents_ | NO (throws exception) |
| _getAccountByHomeId_ | YES |
| _getAccountByLocalId_ | YES |
| _getAccountByUsername_ | YES |
| _getActiveAccount_ | YES |
| _getAllAccounts_ | YES |
| _getConfiguration_ | YES |
| _getLogger_ | YES |
| _getTokenCache_ | NO (throws exception) |
| _handleRedirectPromise_ | NO |
| _initialize_ | YES |
| _initializeWrapperLibrary_ | YES |
| _loginPopup_ | YES |
| _loginRedirect_ | NO (throws exception) |
| _logout_ | NO (throws exception) |
| _logoutPopup_ | NO (throws exception) |
| _logoutRedirect_ | NO (throws exception) |
| _removeEventCallback_ | YES |
| _removePerformanceCallback_ | NO (throws exception) |
| _setActiveAccount_ | NO |
| _setLogger_ | YES |
| _ssoSilent_ | YES |

## Security reporting

Expand Down