DEV Community

MS Office Addin
MS Office Addin

Posted on

Building an Office.js Add-in with Azure AD Multi-Tenant Auth

Padlock icon overlaid on a glowing network of connected nodes, representing secure multi-tenant authentication

Your add-in works great in your tenant. Then a customer in another company installs it, and sign-in just breaks.

If you have ever shipped an Office Add-in to AppSource, you already know this moment. Local testing was smooth. Your own org's accounts worked fine. Then a customer with their own Microsoft 365 tenant installs the add-in, hits "Sign in," and gets stuck on a consent screen or an AADSTS700016 error that means nothing to them and everything to you.

This happens because single-tenant auth only knows about one directory. The moment a second organization shows up, your app registration has no idea who they are. Multi-tenant auth is what fixes this, and it is also the part of Office.js development that trips up the most developers, partly because Microsoft renamed Azure AD to Microsoft Entra ID partway through everyone's learning process and half the internet's tutorials still use the old name.

This post walks through what multi-tenant auth actually means for an Office Add-in, how to set it up in Entra ID, and the specific places this breaks in real-world Office.js code.

What "multi-tenant" actually means here

A tenant is a single organization's instance of Microsoft Entra ID. Your own company is a tenant. Every customer who installs your add-in from AppSource is a different tenant, with its own users, admins, and consent policies.

A single-tenant app registration only accepts sign-ins from accounts inside the tenant where it was registered. That is fine for an internal tool. It is not fine for anything you plan to distribute, because every external customer's sign-in attempt gets rejected before your code ever runs.

A multi-tenant app registration accepts sign-ins from any Entra ID directory (and optionally personal Microsoft accounts too). The trade-off is that you now need to handle per-tenant admin consent, and your token validation logic needs to check the issuer rather than assuming a fixed tenant ID.

Setting up the app registration

In the Entra admin center, under App registrations, the setting that matters is Supported account types. For a distributable Office Add-in you want:

Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant)

If your add-in also needs to support personal Microsoft accounts (Outlook.com, Xbox, etc.), there's a separate option that includes those too, but most B2B-focused Office Add-ins skip this.

Two settings people consistently get wrong:

Redirect URI type. For SSO-based Office Add-ins, this needs to be registered as a Single-page application (SPA) redirect, pointing at your fallback dialog (commonly /dialog.html or similar) rather than a Web redirect. Using the wrong platform type here is the single most common reason dialog.displayDialogAsync silently fails to return a token.

Token version. In the app manifest (not your Office Add-in manifest, the Entra app manifest), set requestedAccessTokenVersion to 2 under the api object. Office SSO expects v2.0 tokens, and a multi-tenant registration without this set will quietly issue v1.0 tokens. If you're getting invalid audience or signature validation errors that make no sense, check this first.

Office SSO vs. MSAL fallback

Office.js gives you two paths for getting a token: OfficeRuntime.auth.getAccessToken() for native Office SSO, and a fallback flow using MSAL.js inside a dialog when SSO isn't available (older Office builds, certain platforms, or when the user needs to consent for the first time).

For multi-tenant apps, both paths need to resolve to the correct tenant, and this is where a lot of implementations quietly break:

// Primary path: native Office SSO
async function getOfficeToken() {
  try {
    const token = await OfficeRuntime.auth.getAccessToken({
      allowSignInPrompt: true,
      allowConsentPrompt: true,
      forMSGraphAccess: true
    });
    return token;
  } catch (err) {
    if (err.code === 13001 || err.code === 13002) {
      // SSO not available or consent required, fall back to MSAL dialog
      return getTokenViaDialog();
    }
    throw err;
  }
}
Enter fullscreen mode Exit fullscreen mode

The fallback dialog flow uses MSAL.js with the /common or /organizations authority endpoint rather than a tenant-specific one:

const msalConfig = {
  auth: {
    clientId: "YOUR_CLIENT_ID",
    authority: "https://login.microsoftonline.com/organizations",
    redirectUri: "https://yourapp.com/dialog.html"
  }
};
Enter fullscreen mode Exit fullscreen mode

Using /organizations instead of a specific tenant GUID is what makes this work across customers. If you hardcode your own tenant ID here, it will work perfectly in testing and fail for every external customer, which is exactly the trap most people fall into.

The cache bug nobody warns you about

There's a subtle MSAL behavior specific to multi-tenant apps: if you request a token using /common or /organizations, get a response, then make a second request also using the generic endpoint, MSAL caches the first token under the tenant it actually came from. The second request misses that cache entry and prompts the user to sign in again, even though they just signed in seconds ago.

The fix is to capture the tenant ID from the first response and use it for subsequent silent token requests within that session, rather than repeatedly hitting the generic endpoint.

Validating tokens on your backend

If your add-in calls a backend API, that backend needs to validate incoming tokens without assuming a single tenant. Two things matter:

  1. Validate the iss (issuer) claim against the multi-tenant issuer pattern, not a single hardcoded tenant.
  2. Store the tid (tenant ID) claim alongside each user record. You will need it later for per-tenant data isolation, admin consent tracking, and support debugging.

Admin consent: the part that generates support tickets

Multi-tenant apps that request anything beyond basic sign-in (Mail.Read, Files.ReadWrite, etc.) typically need tenant admin consent before any user in that organization can use the add-in. This is the step that causes the most confusion for end users, who see a permissions screen, don't recognize it, and either abandon the install or email your support inbox.

Two things help here. First, build a dedicated admin-consent redirect path so an IT admin can grant consent for their whole org in one action rather than every user hitting individual consent prompts. Second, write the in-app messaging for that consent screen assuming the reader is an end user, not an admin. Tell them plainly what to do next: "This requires approval from your IT administrator. Forward this link to them."

Closing thoughts

Multi-tenant auth is one of those things that looks like a checkbox in Entra ID and turns out to be a handful of small decisions, token version, redirect URI type, authority endpoint, cache handling, that each silently break distribution if you get them wrong. Most of them only surface once a real external tenant tries your add-in, which is exactly when you don't want to be debugging auth.

If you're working through this for an Office Add-in or a Google Workspace add-on right now and want a second pair of eyes on your app registration or SSO setup, that's literally what we do daily. You can see how we approach Azure AD / Entra app registration and OAuth2 for Office Add-ins, or just drop a question in the comments below, happy to help debug a specific error code if you're stuck on one.

What's the weirdest auth error you've hit shipping an Office Add-in to AppSource? Drop it below, there's a decent chance someone else here has seen it too.

tags: officejs, azure, oauth, microsoft365

Top comments (0)