Create a Node.js Office Add-in that uses single sign-on

Users can sign in to Office, and your Office Web Add-in can take advantage of this sign-in process to authorize users to your add-in and to Microsoft Graph without requiring users to sign in a second time. For an overview, see Enable SSO in an Office Add-in.

This article walks you through the process of enabling single sign-on (SSO) in an add-in. The sample add-in you create has two parts; a task pane that loads in Microsoft Excel, and a middle-tier server that handles calls to Microsoft Graph for the task pane. The middle-tier server is built with Node.js and Express and exposes a single REST API, /getuserfilenames, that returns a list of the first 10 file names in the user's OneDrive folder. The task pane uses the getAccessToken() method to get an access token for the signed in user to the middle-tier server. The middle-tier server uses the On-Behalf-Of flow (OBO) to exchange the access token for a new one with access to Microsoft Graph. You can extend this pattern to access any Microsoft Graph data. The task pane always calls a middle-tier REST API (passing the access token) when it needs Microsoft Graph services. The middle-tier uses the token obtained via OBO to call Microsoft Graph services and return the results to the task pane.

This article works with an add-in that uses Node.js and Express. For a similar article about an ASP.NET-based add-in, see Create an ASP.NET Office Add-in that uses single sign-on.


  • Node.js (the latest LTS version)

  • Git Bash (or another git client)

  • A code editor - we recommend Visual Studio Code

  • At least a few files and folders stored on OneDrive for Business in your Microsoft 365 subscription

  • A build of Microsoft 365 that supports the IdentityAPI 1.3 requirement set. You might qualify for a Microsoft 365 E5 developer subscription, which includes a developer sandbox, through the Microsoft 365 Developer Program; for details, see the FAQ. The developer sandbox includes a Microsoft Azure subscription that you can use for app registrations in later steps in this article. If you prefer, you can use a separate Microsoft Azure subscription for app registrations. Get a trial subscription at Microsoft Azure.

Set up the starter project

  1. Clone or download the repo at Office Add-in NodeJS SSO.


    There are two versions of the sample:

    • The Begin folder is a starter project. The UI and other aspects of the add-in that are not directly connected to SSO or authorization are already done. Later sections of this article walk you through the process of completing it.
    • The Complete folder contains the same sample with all coding steps from this article completed. To use the completed version, just follow the instructions in this article, but replace "Begin" with "Complete" and skip the sections Code the client side and Code the middle-tier server side.
  2. Open a command prompt in the Begin folder.

  3. Enter npm install in the console to install all of the dependencies itemized in the package.json file.

  4. Run the command npm run install-dev-certs. Select Yes to the prompt to install the certificate.

Use the following values for placeholders for the subsequent app registration steps.

Placeholder Value
<add-in-name> Office-Add-in-NodeJS-SSO
<fully-qualified-domain-name> localhost:3000
Microsoft Graph permissions profile, openid, Files.Read

Register the add-in with Microsoft identity platform

You need to create an app registration in Azure that represents your web server. This enables authentication support so that proper access tokens can be issued to the client code in JavaScript. This registration supports both SSO in the client, and fallback authentication using the Microsoft Authentication Library (MSAL).

  1. Sign in to the Azure portal with the admin credentials to your Microsoft 365 tenancy. For example,

  2. Select App registrations. If you don't see the icon, search for "app registration" in the search bar.

    The Azure portal home page.

    The App registrations page appears.

  3. Select New registration.

    New registration on the App registrations pane.

    The Register an application page appears.

  4. On the Register an application page, set the values as follows.

    • Set Name to <add-in-name>.
    • Set Supported account types to Accounts in any organizational directory (any Azure AD directory - multitenant) and personal Microsoft accounts (e.g. Skype, Xbox).
    • Set Redirect URI to use the platform Single-page application (SPA) and the URI to https://<fully-qualified-domain-name>/dialog.html.

    Register an application pane with name and supported account completed.

  5. Select Register. A message is displayed stating that the application registration was created.

    Message stating that the application registration was created.

  6. Copy and save the values for the Application (client) ID and the Directory (tenant) ID. You'll use both of them in later procedures.

    App registration pane for Contoso displaying the client ID and directory ID.

Add a client secret

Sometimes called an application password, a client secret is a string value your app can use in place of a certificate to identity itself.

  1. From the left pane, select Certificates & secrets. Then on the Client secrets tab, select New client secret.

    The Certificates & secrets pane.

    The Add a client secret pane appears.

  2. Add a description for your client secret.

  3. Select an expiration for the secret or specify a custom lifetime.

    • Client secret lifetime is limited to two years (24 months) or less. You can't specify a custom lifetime longer than 24 months.
    • Microsoft recommends that you set an expiration value of less than 12 months.

    Add a client secret pane with description and expires completed.

  4. Select Add. The new secret is created and the value is temporarily displayed.


Record the secret's value for use in your client application code. This secret value is never displayed again after you leave this pane.

Expose a web API

  1. From the left pane, select Expose an API.

    The Expose an API pane appears.

    An app registration's Expose an API pane.

  2. Select Set to generate an application ID URI.

    Set button in the app registration's Expose an API pane.

    The section for setting the application ID URI appears with a generated Application ID URI in the form api://<app-id>.

  3. Update the application ID URI to api://<fully-qualified-domain-name>/<app-id>.

    Edit the App ID URI pane with localhost port set to 44355.

    • The Application ID URI is pre-filled with app ID (GUID) in the format api://<app-id>.
    • The application ID URI format should be: api://<fully-qualified-domain-name>/<app-id>
    • Insert the fully-qualified-domain-name between api:// and <app-id> (which is a GUID). For example, api://<app-id>.
    • If you're using localhost, then the format should be api://localhost:<port>/<app-id>. For example, api://localhost:3000/c6c1f32b-5e55-4997-881a-753cc1d563b7.

    For additional application ID URI details, see Application manifest identifierUris attribute.


    If you get an error saying that the domain is already owned but you own it, follow the procedure at Quickstart: Add a custom domain name to Azure Active Directory to register it, and then repeat this step. (This error can also occur if you are not signed in with credentials of an admin in the Microsoft 365 tenancy. See step 2. Sign out and sign in again with admin credentials and repeat the process from step 3.)

Add a scope

  1. On the Expose an API page, select Add a scope.

    Select Add a scope button.

    The Add a scope pane opens.

  2. In the Add a scope pane, specify the scope's attributes. The following table shows example values for and Outlook add-in requiring the profile, openid, Files.ReadWrite, and Mail.Read permissions. Modify the text to match the permissions your add-in needs.

    Field Description Values
    Scope name The name of your scope. A common scope naming convention is resource.operation.constraint. For SSO this must be set to access_as_user.
    Who can consent Determines if admin consent is required or if users can consent without an admin approval. For learning SSO and samples, we recommend you set this to Admins and users.

    Select Admins only for higher-privileged permissions.
    Admin consent display name A short description of the scope's purpose visible to admins only. Read/write permissions to user files. Read permissions to user mail and profiles.
    Admin consent description A more detailed description of the permission granted by the scope that only admins see. Allow Office to have read/write permissions to all user files and read permissions to all user mail. Office can call the app's web APIs as the current user.
    User consent display name A short description of the scope's purpose. Shown to users only if you set Who can consent to Admins and users. Read/write permissions to your files. Read permissions to your mail and profile.
    User consent description A more detailed description of the permission granted by the scope. Shown to users only if you set Who can consent to Admins and users. Allow Office to have read/write permissions to your files, and read permissions to your mail and profile.
  3. Set the State to Enabled, and then select Add scope.

    Set state to enabled and select the add scope button.

    The new scope you defined displays on the pane.

    The new scope displayed on the Expose an API pane.


    The domain part of the Scope name displayed just below the text field should automatically match the Application ID URI set in the previous step, with /access_as_user appended to the end; for example, api://localhost:6789/c6c1f32b-5e55-4997-881a-753cc1d563b7/access_as_user.

  4. Select Add a client application.

    Select add a client application.

    The Add a client application pane appears.

  5. In the Client ID enter ea5a67f6-b6f3-4338-b240-c655ddc3cc8e. This value pre-authorizes all Microsoft Office application endpoints. If you also want to pre-authorize Office when used inside of Microsoft Teams, add 1fec8e78-bce4-4aaf-ab1b-5451cc387264 (Microsoft Teams desktop and Teams mobile) and 5e3ce6c0-2b1f-4285-8d4b-75ee78787346 (Teams on the web).


    The ea5a67f6-b6f3-4338-b240-c655ddc3cc8e ID pre-authorizes Office on all the following platforms. Alternatively, you can enter a proper subset of the following IDs if, for any reason, you want to deny authorization to Office on some platforms. If you do so, leave out the IDs of the platforms from which you want to withhold authorization. Users of your add-in on those platforms will not be able to call your Web APIs, but other functionality in your add-in will still work.

    • d3590ed6-52b3-4102-aeff-aad2292ab01c (Microsoft Office)
    • 93d53678-613d-4013-afc1-62e9e444a0a5 (Office on the web)
    • bc59ab01-8403-45c6-8796-ac3ef710b3e3 (Outlook on the web)
  6. In Authorized scopes, select the api://<fully-qualified-domain-name>/<app-id>/access_as_user checkbox.

  7. Select Add application.

    The Add a client application pane.

Add Microsoft Graph permissions

  1. From the left pane, select API permissions.

    The API permissions pane.

    The API permissions pane opens.

  2. Select Add a permission.

    Adding a permission on the API permissions pane.

    The Request API permissions pane opens.

  3. Select Microsoft Graph.

    The Request API permissions pane with Microsoft Graph button.

  4. Select Delegated permissions.

    The Request API permissions pane with delegated permissions button.

  5. In the Select permissions search box, search for the permissions your add-in needs. For example, for an Outlook add-in, you might use profile, openid, Files.ReadWrite, and Mail.Read.


    The User.Read permission may already be listed by default. It's a good practice to only request permissions that are needed, so we recommend that you uncheck the box for this permission if your add-in doesn't actually need it.

  6. Select the checkbox for each permission as it appears. Note that the permissions will not remain visible in the list as you select each one. After selecting the permissions that your add-in needs, select Add permissions.

    The Request API permissions pane with some permissions selected.

  7. Select Grant admin consent for [tenant name]. Select Yes for the confirmation that appears.

Configure access token version

You must define the access token version that is acceptable for your app. This configuration is made in the Azure Active Directory application manifest.

Define the access token version

The access token version can change if you chose an account type other than Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox). Use the following steps to ensure the access token version is correct for Office SSO usage.

  1. From the left pane, select Manifest.

    Select Azure manifest.

    The Azure Active Directory application manifest appears.

  2. Enter 2 as the value for the accessTokenAcceptedVersion property.

    Value for accepted access token version.

  3. Select Save.

    A message pops up on the browser stating that the manifest was updated successfully.

    Manifest updated message.

Congratulations! You've completed the app registration to enable SSO for your Office add-in.

Configure the add-in

  1. Open the \Begin folder in the cloned project in your code editor.

  2. Open the .ENV file and use the values that you copied earlier from the Office-Add-in-NodeJS-SSO app registration. Set the values as follows:

    Name Value
    CLIENT_ID Application (client) ID from app registration overview page.
    CLIENT_SECRET Client secret saved from Certificates & Secrets page.

    The values should not be in quotation marks. When you are done, the file should be similar to the following:

  3. Open the add-in manifest file "manifest\manifest_local.xml" and then scroll to the bottom of the file. Just above the </VersionOverrides> end tag, you'll find the following markup.

  4. Replace the placeholder "$app-id-guid$" in both places in the markup with the Application ID that you copied when you created the Office-Add-in-NodeJS-SSO app registration. The "$" symbols are not part of the ID, so don't include them. This is the same ID you used for the CLIENT_ID in the .ENV file.


    The <Resource> value is the Application ID URI you set when you registered the add-in. The <Scopes> section is used only to generate a consent dialog box if the add-in is sold through AppSource.

  5. Open the \public\javascripts\fallback-msal\authConfig.js file. Replace the placeholder "$app-id-guid$" with the application ID that you saved from the Office-Add-in-NodeJS-SSO app registration you created previously.

  6. Save the changes to the file.

Code the client-side

Call our web server REST API

  1. In your code editor, open the file public\javascripts\ssoAuthES6.js. It already has code that ensures that Promises are supported, even in the Trident (Internet Explorer 11) webview control, and an Office.onReady call to assign a handler to the add-in's only button.


    As the name suggests, the ssoAuthES6.js uses JavaScript ES6 syntax because using async and await best shows the essential simplicity of the SSO API. When the localhost server is started, this file is transpiled to ES5 syntax so that the sample will support Trident.

  2. In the getFileNameList function, replace TODO 1 with the following code. About this code, note:

    • The function getFileNameList is called when the user chooses the Get OneDrive File Names button on the task pane.
    • It calls the callWebServerAPI function specifying which REST API to call. This returns JSON containing a list of file names from the user's OneDrive.
    • The JSON is passed to the writeFileNamesToOfficeDocument function to list the file names in the document.
    try {
        const jsonResponse = await callWebServerAPI('GET', '/getuserfilenames');
        if (jsonResponse === null) {
            // Null is returned when a message was displayed to the user
            // regarding an authentication error that cannot be resolved.
        await writeFileNamesToOfficeDocument(jsonResponse);
        showMessage('Your OneDrive filenames are added to the document.');
    } catch (error) {
  3. In the callWebServerAPI function, replace TODO 2 with the following code. About this code, note:

    • The function calls getAccessToken which is our own function that encapsulates using Office SSO or MSAL fallback as necessary to get the token. If it returns a null token, a message was shown for an auth error condition that cannot be resolved, so the function also returns null.
    • The function uses the fetch API to call the web server and if successful, returns the JSON body.
    const accessToken = await getAccessToken(authSSO);
    if (accessToken === null) {
        return null;
    const response = await fetch(path, {
        method: method,
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + accessToken,
    // Check for success condition: HTTP status code 2xx.
    if (response.ok) {
        return response.json();
  4. In the callWebServerAPI function, replace TODO 3 with the following code. About this code, note:

    • This code handles the scenario where the SSO token expired. If so we need to call Office.auth.getAccessToken to get a refreshed token. The simplest way is to make a recursive call which results in a new call to Office.auth.getAccessToken. The retryRequest parameter ensures the recursive call is only attempted once.
    • The TokenExpiredError string is set by our web server whenever it detects an expired token.
     // Check for fail condition: Is SSO token expired? If so, retry the call which will get a refreshed token.
    const jsonBody = await response.json();
    if (
        authSSO === true &&
        jsonBody != null &&
        jsonBody.type === 'TokenExpiredError'
    ) {
        if (!retryRequest) {
            return callWebServerAPI(method, path, true); // Try the call again. The underlying call to Office JS getAccessToken will refresh the token.
        } else {
            // Indicates a second call to retry and refresh the token failed.
            authSSO = false;
            return callWebServerAPI(method, path, true); // Try the call again, but now using MSAL fallback auth.
  5. In the callWebServerAPI function, replace TODO 4 with the following code. About this code, note:

    • The Microsoft Graph string is set by our web server whenever a Microsoft Graph call fails.
    // Check for fail condition: Did we get a Microsoft Graph API error, which is returned as bad request (403)?
    if (response.status === 403 && jsonBody.type === 'Microsoft Graph') {
        throw new Error('Microsoft Graph error: ' + jsonBody.errorDetails);
  6. In the callWebServerAPI function, replace TODO 5 with the following code.

    // Handle other errors.
    throw new Error(
        'Unknown error from web server: ' + JSON.stringify(jsonBody)
  7. In the getAccessToken function, replace TODO 6 with the following code. About this code, note:

    • authSSO tracks if we are using SSO, or using MSAL fallback. If SSO is used, the function calls Office.auth.getAccessToken and returns the token.
    • Errors are handled by the handleSSOErrors function which will return a token if it switches to fallback MSAL authentication.
    • Fallback authentication uses the MSAL library to sign in the user. The add-in itself is an SPA, and uses an SPA app registration to access the web server.
    if (authSSO) {
        try {
            // Get the access token from Office host using SSO.
            // Note that Office.auth.getAccessToken modifies the options parameter. Create a copy of the object
            // to avoid modifying the original object.
            const options = JSON.parse(JSON.stringify(ssoOptions));
            const token = await Office.auth.getAccessToken(options);
            return token;
        } catch (error) {
            return handleSSOErrors(error);
    } else {
        // Get access token through MSAL fallback.
        try {
            const accessToken = await getAccessTokenMSAL();
            return accessToken;
        } catch (error) {
            throw new Error(
                'Cannot get access token. Both SSO and fallback auth failed. ' +
  8. In the handleSSOErrors function, replace TODO 7 with the following code. For more information about these errors, see Troubleshoot SSO in Office Add-ins.

    switch (error.code) {
        case 13001:
            // No one is signed into Office. If the add-in cannot be effectively used when no one
            // is logged into Office, then the first call of getAccessToken should pass the
            // `allowSignInPrompt: true` option. Since this sample does that, you should not see
            // this error.
                'No one is signed into Office. But you can use many of the add-ins functions anyway. If you want to log in, press the Get OneDrive File Names button again.'
        case 13002:
            // The user aborted the consent prompt. If the add-in cannot be effectively used when consent
            // has not been granted, then the first call of getAccessToken should pass the `allowConsentPrompt: true` option.
                'You can use many of the add-ins functions even though you have not granted consent. If you want to grant consent, press the Get OneDrive File Names button again.'
        case 13006:
            // Only seen in Office on the web.
                'Office on the web is experiencing a problem. Please sign out of Office, close the browser, and then start again.'
        case 13008:
            // Only seen in Office on the web.
                'Office is still working on the last operation. When it completes, try this operation again.'
        case 13010:
            // Only seen in Office on the web.
                "Follow the instructions to change your browser's zone configuration."
  9. Replace TODO 8 with the following code. For any errors that can't be handled the code switches to fallback authentication using MSAL.

    default: //recursive call.
            // For all other errors, including 13000, 13003, 13005, 13007, 13012, and 50001, fall back
            // to MSAL sign-in.
            showMessage('SSO failed. Trying fallback auth.');
            authSSO = false;
            return getAccessToken(false);
    return null; // Return null for errors that show a message to the user.

Code the web server REST API

The web server provides REST APIs for the client to call. For example, the REST API /getuserfilenames gets a list of filenames from the user's OneDrive folder. Each REST API call requires an access token by the client to ensure the correct client is accessing their data. The access token is exchanged for a Microsoft Graph token through the On-Behalf-Of flow (OBO). The new Microsoft Graph token is cached by the MSAL library for subsequent API calls. It's never sent outside of the web server. For more information, see Middle-tier access token request

Create the route and implement On-Behalf-Of flow

  1. Open the file routes\getFilesRoute.js and replace TODO 9 with the following code. About this code, note:

    • It calls authHelper.validateJwt. This ensures the access token is valid and hasn't been tampered with.
    • For more information, see Validating tokens.
     async function (req, res) {
       // TODO 10: Exchange the access token for a Microsoft Graph token
       //          by using the OBO flow.
  2. Replace TODO 10 with the following code. About this code, note:

    • It only requests the minimum scopes it needs, such as
    • It uses the MSAL authHelper to perform the OBO flow in the call to acquireTokenOnBehalfOf.
    try {
      const authHeader = req.headers.authorization;
      let oboRequest = {
        oboAssertion: authHeader.split(' ')[1],
        scopes: [""],
      // The Scope claim tells you what permissions the client application has in the service.
      // In this case we look for a scope value of access_as_user, or full access to the service as the user.
      const tokenScopes = jwt.decode(oboRequest.oboAssertion).scp.split(' ');
      const accessAsUserScope = tokenScopes.find(
        (scope) => scope === 'access_as_user'
      if (!accessAsUserScope) {
        res.status(401).send({ type: "Missing access_as_user" });
      const cca = authHelper.getConfidentialClientApplication();
      const response = await cca.acquireTokenOnBehalfOf(oboRequest);
      // TODO 11: Call Microsoft Graph to get list of filenames.
    } catch (err) {
      // TODO 12: Handle any errors.
  3. Replace TODO 11 with the following code. About this code, note:

    • It constructs the URL for the Microsoft Graph API call and then makes the call via the getGraphData function.
    • It returns errors by sending an HTTP 500 response along with details.
    • On success it returns the JSON with the filename list to the client.
    // Minimize the data that must come from MS Graph by specifying only the property we need ("name")
    // and only the top 10 folder or file names.
    const rootUrl = '/me/drive/root/children';
    // Note that the last parameter, for queryParamsSegment, is hardcoded. If you reuse this code in
    // a production add-in and any part of queryParamsSegment comes from user input, be sure that it is
    // sanitized so that it cannot be used in a Response header injection attack.
    const params = '?$select=name&$top=10';
    const graphData = await getGraphData(
    // If Microsoft Graph returns an error, such as invalid or expired token,
    // there will be a code property in the returned object set to a HTTP status (e.g. 401).
    // Return it to the client. On client side it will get handled in the fail callback of `makeWebServerApiCall`.
    if (graphData.code) {
          type: "Microsoft Graph",
            "An error occurred while calling the Microsoft Graph API.\n" +
    } else {
      // MS Graph data includes OData metadata and eTags that we don't need.
      // Send only what is actually needed to the client: the item names.
      const itemNames = [];
      const oneDriveItems = graphData["value"];
      for (let item of oneDriveItems) {
    // TODO 12: Check for expired token.
  4. Replace TODO 12 with the following code. This code specifically checks if the token expired because the client can request a new token and call again.

    } catch (err) {
       // On rare occasions the SSO access token is unexpired when Office validates it,
       // but expires by the time it is used in the OBO flow. Microsoft identity platform will respond
       // with "The provided value for the 'assertion' is not valid. The assertion has expired."
       // Construct an error message to return to the client so it can refresh the SSO token.
       if (err.errorMessage.indexOf('AADSTS500133') !== -1) {
         res.status(401).send({ type: "TokenExpiredError", errorDetails: err });
       } else {
         res.status(403).send({ type: "Unknown", errorDetails: err });

The sample must handle both fallback authentication through MSAL and SSO authentication through Office. The sample will try SSO first, and the authSSO boolean at the top of the file tracks if the sample is using SSO or has switched to fallback auth.

Run the project

  1. Ensure that you have some files in your OneDrive so that you can verify the results.

  2. Open a command prompt in the root of the \Begin folder.

  3. Run the command npm install to install all package dependencies.

  4. Run the command npm start to start the middle-tier server.

  5. You need to sideload the add-in into an Office application (Excel, Word, or PowerPoint) to test it. The instructions depend on your platform. There are links to instructions at Sideload an Office Add-in for Testing.

  6. In the Office application, on the Home ribbon, select the Show Add-in button in the SSO Node.js group to open the task pane add-in.

  7. Click the Get OneDrive File Names button. If you're logged into Office with either a Microsoft 365 Education or work account, or a Microsoft account, and SSO is working as expected the first 10 file and folder names in your OneDrive for Business are inserted into the document. (It may take as much as 15 seconds the first time.) If you're not logged in, or you're in a scenario that doesn't support SSO, or SSO isn't working for any reason, you'll be prompted to sign in. After you sign in, the file and folder names appear.


If you were previously signed into Office with a different ID, and some Office applications that were open at the time are still open, Office may not reliably change your ID even if it appears to have done so. If this happens, the call to Microsoft Graph may fail or data from the previous ID may be returned. To prevent this, be sure to close all other Office applications before you press Get OneDrive File Names.

Security notes

  • The /getuserfilenames route in getFilesroute.js uses a literal string to compose the call for Microsoft Graph. If you change the call so that any part of the string comes from user input, sanitize the input so that it cannot be used in a Response header injection attack.

  • In app.js the following content security policy is in place for scripts. You may want to specify additional restrictions depending on your add-in security needs.

    "Content-Security-Policy": "script-src " + process.env.SERVER_SOURCE,

Always follow security best practices in the Microsoft identity platform documentation.