Microsoft SSO in an Office Add-in with Azure AD

5 min read
Microsoft SSO in an Office Add-in with Azure AD

A Microsoft SSO Office add-in lets a user sign in once with their Microsoft 365 account and have the add-in just work, with no second login dialog. When someone is already signed in to Office, the add-in requests an access token for the current identity and uses it to call Microsoft Graph or your own API. Done right, it removes a login screen, keeps tokens out of your own storage, and ties the add-in to your Entra ID policies. This guide walks through the full flow: enabling SSO in the manifest, getting a token, exchanging it for Graph access, handling the fallback when SSO is not available, and the scope mistakes that break most first builds.

How Microsoft SSO works in an Office add-in

Office SSO is built on the OAuth 2.0 on-behalf-of flow. The sequence is short once you see it end to end:

  1. Your add-in calls OfficeRuntime.auth.getAccessToken(), which returns a bootstrap token for the signed-in user — issued by Microsoft Entra ID without a prompt.
  2. The add-in sends that token to your server.
  3. Your server exchanges it, on behalf of the user, for an access token scoped to Microsoft Graph or your downstream API.
  4. The server calls the API with that token and returns the result.

The bootstrap token only identifies the user and grants access to your own app; it is not a Graph token by itself. That distinction is where most early confusion starts.

Enable SSO in your manifest

For the XML manifest, SSO requires a WebApplicationInfo block that points at your Entra app registration and declares the scopes the add-in needs:

Code
<WebApplicationInfo>
  <Id>00000000-0000-0000-0000-000000000000</Id>
  <Resource>api://localhost:3000/00000000-0000-0000-0000-000000000000</Resource>
  <Scopes>
    <Scope>openid</Scope>
    <Scope>profile</Scope>
    <Scope>User.Read</Scope>
  </Scopes>
</WebApplicationInfo>

The Resource value must match the Application ID URI you set on the app registration, in the api:// format. In the unified JSON manifest the same information lives under webApplicationInfo, but the rules are identical: the resource URI and the registered scopes have to line up exactly.

Get a token with getAccessToken

From the add-in, request the bootstrap token and pass it to your backend. Wrapping the call in MSAL on the server keeps the on-behalf-of exchange clean:

Code
async function callApi() {
  try {
    const bootstrapToken = await OfficeRuntime.auth.getAccessToken({
      allowSignInPrompt: true,
    });
    const res = await fetch("/api/graph/me", {
      headers: { Authorization: `Bearer ${bootstrapToken}` },
    });
    return await res.json();
  } catch (err) {
    // 13xxx errors mean SSO could not complete — fall back to a dialog.
    return signInWithDialog();
  }
}

On the server, MSAL's acquireTokenOnBehalfOf takes the bootstrap token and returns a Graph token, which you then use to call https://graph.microsoft.com/v1.0/me.

Token lifetime

SSO access tokens are short-lived — roughly one hour. Never cache them long term or store them client-side. Request a fresh token when you need one and let MSAL handle silent renewal on the server.

The token scope mistakes that break most first builds

Why does getAccessToken return a token that Graph rejects?

Because the bootstrap token is scoped to your app, not to Graph. You have to exchange it server-side with the on-behalf-of flow before calling Graph. Sending the bootstrap token straight to graph.microsoft.com returns a 401 every time.

Why am I getting error 13003 or 13007?

These mean SSO is not available in the current context — an unsupported Office version, a guest account, or a configuration mismatch. They are expected in some setups, which is exactly why you need a fallback path rather than treating SSO as guaranteed.

Usually a missing scope or missing admin consent. Confirm that openid, profile, and the Graph permissions you call (for example User.Read) are all granted on the app registration, and that an admin has consented for tenant-wide scopes. Getting the Azure AD app registration right up front avoids most consent loops.

Handle the fallback when SSO is not available

When getAccessToken throws a 13xxx error, switch to the dialog API with MSAL so the user can still sign in interactively:

Code
function signInWithDialog() {
  Office.context.ui.displayDialogAsync(
    "https://localhost:3000/auth/login.html",
    { height: 60, width: 30 },
    (result) => {
      const dialog = result.value;
      dialog.addEventHandler(Office.EventType.DialogMessageReceived, (arg) => {
        const token = JSON.parse(arg.message).accessToken;
        dialog.close();
        // proceed with the token from the interactive flow
      });
    }
  );
}

This keeps the add-in usable everywhere, with SSO as the fast path and the dialog as the safety net.

Frequently asked questions

Do I need a backend server for Office SSO?

For anything calling Microsoft Graph, yes. The on-behalf-of exchange has to happen server-side because it uses your app's client secret or certificate, which can never live in the browser.

Can one Office SSO add-in work across multiple tenants?

Yes. Register the app as multi-tenant in Entra ID and handle admin consent per tenant. Our single sign-on and identity services cover multi-tenant setup end to end.

Is MSAL.js required for Office add-in SSO?

MSAL is the supported and far simpler path. It handles token acquisition, the on-behalf-of exchange, caching, and renewal, so you are not hand-rolling OAuth flows that are easy to get subtly wrong.

Need this built for your team?

We build Microsoft SSO and Azure AD implementation for enterprise teams like yours. Get a free 24-hour scoping call.

If you want SSO wired up correctly the first time — app registration, scopes, on-behalf-of, and fallback — a discovery call is the place to start.