Skip to content
CloudOps
Newsletter
All guides
AI for Microsoft Teams By James Joyner IV · · 9 min read

SSO for Teams Apps: On-Behalf-Of Flow Without the Pain

Teams SSO lets your tab or bot get a token silently and call Graph or your own APIs as the user. Here's the on-behalf-of flow, set up so it actually works.

  • #microsoft-teams
  • #sso
  • #azure-ad
  • #on-behalf-of
  • #graph-api
  • #security

The fastest way to kill adoption of a Teams app is to make people log in again. They’re already signed into Teams with their work identity — asking them to authenticate a second time inside the tab feels broken, because it is. Teams SSO fixes this: your tab or bot can silently obtain a token for the signed-in user and exchange it, via the on-behalf-of (OBO) flow, for a token to call Microsoft Graph or your own backend. It’s powerful and it’s fiddly, and almost every failure traces back to one of three setup mistakes. Here’s the whole path.

What SSO actually gives you

When SSO works, your client-side code calls one SDK method and gets a token — no popup, no redirect:

import { authentication } from "@microsoft/teams-js";

const ssoToken = await authentication.getAuthToken();
// send ssoToken to your backend in the Authorization header

But that ssoToken is scoped to your app’s API, not to Graph. You can’t call Graph with it directly. That’s where OBO comes in: your backend swaps the incoming token for a downstream token.

The three setup steps everyone gets wrong

SSO failing silently is almost always one of these:

1. Expose an API and set the Application ID URI. In your Azure AD app registration, under “Expose an API,” set the Application ID URI to the Teams-required format: api://<your-domain>/<app-client-id> (for tabs hosted on a domain) or the bot-specific format for bots. Add a scope named access_as_user. Get the URI format wrong and getAuthToken() returns a consent error.

2. Pre-authorize the Teams client IDs. Under the same “Expose an API,” add the two Teams client application IDs as authorized client applications for your access_as_user scope. Skip this and Teams won’t issue the token silently — the user gets an interactive consent prompt every time, defeating the point.

3. Mirror it in the manifest. The Teams app manifest needs webApplicationInfo with your app’s client ID and resource (the Application ID URI):

{
  "webApplicationInfo": {
    "id": "<app-client-id>",
    "resource": "api://devops.example.com/<app-client-id>"
  }
}

All three must agree. A mismatch between the manifest resource and the Azure AD Application ID URI is the single most common reason SSO “just doesn’t work.”

The on-behalf-of exchange

Your backend receives the SSO token, validates it, then exchanges it for a Graph token using the OBO grant. With MSAL on the server:

const { ConfidentialClientApplication } = require("@azure/msal-node");

const cca = new ConfidentialClientApplication({
  auth: {
    clientId: process.env.AAD_CLIENT_ID,
    clientSecret: process.env.AAD_CLIENT_SECRET,
    authority: `https://login.microsoftonline.com/${tenantId}`,
  },
});

async function getGraphToken(ssoToken) {
  const result = await cca.acquireTokenOnBehalfOf({
    oboAssertion: ssoToken,
    scopes: ["https://graph.microsoft.com/User.Read"],
  });
  return result.accessToken;
}

Now you call Graph as the user, with only the permissions they’ve consented to — not an all-powerful application token.

OBO can fail with an interaction_required error when the user hasn’t consented to a downstream scope yet. This is expected the first time. Catch it, tell the client to run the interactive consent flow once, and from then on the silent path works:

try {
  return await getGraphToken(ssoToken);
} catch (err) {
  if (err.errorCode === "invalid_grant" || err.suberror === "consent_required") {
    return { needsConsent: true, scopes: ["User.Read"] };
  }
  throw err;
}

On the client, when you get needsConsent, call authentication.authenticate() to pop a consent window pointed at your AAD consent URL. After that one-time grant, the experience is silent forever.

Bots use SSO too

The same flow works for bots, but the trigger is different: you configure an OAuth connection on the Azure Bot resource and the bot SDK surfaces a token via the token service. For message extensions and adaptive card actions that need the user’s identity, this is how you avoid a sign-in card on every interaction. The principle is identical — get a user-scoped token, OBO it for what you actually need to call.

Least privilege still applies

SSO makes it easy to request broad scopes, which is exactly why you should resist. Two habits:

  • Request the narrowest Graph scopes that do the job. User.Read to greet someone by name; don’t request Directory.Read.All because it might be handy. Every scope you add is a consent the user (or an admin) has to approve, and broad scopes get apps blocked by tenant admins.
  • Validate the incoming token. Check the audience (aud) matches your Application ID URI and the issuer is your tenant before you trust it. The SSO token is a bearer token; treat it like one.

Where this fits

SSO is the difference between a Teams app that feels native and one that feels bolted on. The flow itself is just “get token, OBO it” — the work is the three-way agreement between Azure AD, the manifest, and your scopes. Get the Application ID URI right, pre-authorize the Teams client IDs, mirror it in webApplicationInfo, and handle the one-time consent. For the message-extension and tab handlers that consume these tokens, see the related Teams guides, plus token-validation snippets in the prompt library and more in the Microsoft Teams category.

Azure AD app registration UI and MSAL APIs change; verify the Application ID URI format and OBO scopes against current Microsoft identity docs before shipping.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.