Erreur SSO Teams : « App resource defined in manifest and iframe origin do not match » (cause, diagnostic, correctif)

Erreur SSO Teams « App resource defined in manifest and iframe origin do not match » : diagnostic complet, cause racine et correctif pas à pas pour les onglets Teams (React/Fluent UI, TeamsFx).

Sommaire

Contexte et symptômes

Vous implémentez le SSO (Single Sign-On) dans un onglet Microsoft Teams construit avec React + Fluent UI, en utilisant le SDK @microsoft/teamsfx. Votre manifeste et votre application Azure AD (Entra ID) sont configurés, mais l’appel TeamsUserCredential.getSSOToken() échoue systématiquement avec l’erreur :

ErrorWithCode.InternalError: Get SSO token failed with error: 
App resource defined in manifest and iframe origin do not match

Typiquement, votre configuration ressemble à ceci :

  • webApplicationInfo.id = client-id AAD
  • webApplicationInfo.resource = api://my-aad-app-client-id
  • Application ID URI (Expose an API) = api://my-aad-app-client-id
  • URI de redirection = https://localhost:53000 (tests locaux)

Malgré cela, Teams refuse le SSO car le resource déclaré et l’origin (domaine) réel du contenu iframe ne correspondent pas.

Explication claire de la cause racine

Le SSO dans un onglet Teams repose sur un triptyque qui doit être parfaitement aligné :

  1. L’origin de l’iframe qui affiche votre onglet (schéma + domaine + éventuellement port), par ex. https://xxxxx.ngrok-free.app ou https://localhost:53000.
  2. L’Application ID URI (Expose an API) de votre application AAD, qui sert de ressource (audience) pour le jeton SSO : format recommandé api://<fqdn>/<APPID>.
  3. webApplicationInfo.resource dans le manifeste Teams, qui doit être identique à l’Application ID URI.

Si le domaine de l’Application ID URI ne reflète pas exactement le domaine depuis lequel votre onglet est chargé, l’authentification intégrée de Teams considère que l’application demandant le jeton n’est pas la bonne « ressource ». D’où l’erreur : « App resource defined in manifest and iframe origin do not match ».

En particulier, la forme api://{client-id} ne porte aucune information de domaine. Elle est valide dans certains scénarios OAuth, mais pas pour le SSO des onglets Teams hébergés sur un domaine précis, car Teams doit pouvoir vérifier que la page affichée dans l’iframe et la ressource ciblée pointent bien vers le même origin.

Correctif express (la solution qui marche)

  1. Changez l’Application ID URI (Expose an API) pour inclure un FQDN correspondant à l’origin réel de l’onglet : api://fully-qualified-domain-name.com/<APPID> En local, utilisez un tunnel type ngrok (ex. https://xxxxx.ngrok-free.app) et réutilisez ce FQDN.
  2. Mettez exactement la même valeur dans le manifeste Teams sous webApplicationInfo.resource.
  3. Réinstallez / re-sideload l’application dans Teams (supprimez la version précédente) pour forcer la prise en compte du nouveau manifeste.

Après cette correction, getSSOToken() retourne un jeton valide et votre onglet peut poursuivre (par exemple échanger ce jeton côté serveur contre un jeton Microsoft Graph via un flux On-Behalf-Of).

Comprendre l’origin et la ressource (pour éviter la récidive)

Origin (au sens navigateur) = schéma + domaine (+ port si non standard). Exemples :

URL de l’ongletOrigin calculéCompatible si Application ID URI =
https://xxxxx.ngrok-free.app/index.htmlhttps://xxxxx.ngrok-free.appapi://xxxxx.ngrok-free.app/<APPID>
https://localhost:53000/https://localhost:53000api://<FQDN DEV>/<APPID> (recommandé : tunnel)
https://portal.exemple.com/tabs/homehttps://portal.exemple.comapi://portal.exemple.com/<APPID>

Pour l’Application ID URI, privilégiez toujours api://<fqdn>/<APPID>. Évitez api://{client-id} pour les onglets Teams, car Teams ne peut pas corréler cette forme dépourvue de domaine avec l’origin réel de l’iframe.

Configuration détaillée pas à pas

1) Choisir un FQDN fiable (dev et prod)

  • Développement : utilisez un tunnel (ngrok, dev tunnels, etc.) ; ex. https://xxxxx.ngrok-free.app. Ce FQDN doit rester stable durant votre session.
  • Production : utilisez votre domaine public (ex. portal.exemple.com).

2) Azure AD (Entra ID) — Expose an API

  1. Ouvrez votre application AAD, section « Expose an API ».
  2. Définissez Application ID URI avec le FQDN choisi : api://xxxxx.ngrok-free.app/<APPID>
  3. Créez (ou vérifiez) la portée access_as_user si vous suivez le modèle TeamsFx côté serveur (OBO).
  4. Vérifiez les URI de redirection : le même origin doit y figurer pour les flux d’auth front (par ex. https://xxxxx.ngrok-free.app/auth-end si vous en avez un).

3) Manifeste Teams — webApplicationInfo et validDomains

Dans votre manifeste (version 1.16+), mettez à jour les champs suivants :

{
  "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.16/MicrosoftTeams.schema.json",
  "manifestVersion": "1.16",
  "version": "1.0.0",
  "id": "00000000-0000-0000-0000-000000000000",
  "packageName": "com.exemple.teams.tab",
  "developer": {
    "name": "Exemple",
    "websiteUrl": "https://xxxxx.ngrok-free.app",
    "privacyUrl": "https://xxxxx.ngrok-free.app/privacy",
    "termsOfUseUrl": "https://xxxxx.ngrok-free.app/terms"
  },
  "name": { "short": "Mon Onglet", "full": "Mon Onglet Teams" },
  "description": {
    "short": "Onglet avec SSO Teams",
    "full": "Démonstration de SSO pour un onglet Teams en React"
  },
  "staticTabs": [
    {
      "entityId": "home",
      "name": "Accueil",
      "contentUrl": "https://xxxxx.ngrok-free.app/index.html",
      "websiteUrl": "https://xxxxx.ngrok-free.app/",
      "scopes": ["personal"]
    }
  ],
  "webApplicationInfo": {
    "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
    "resource": "api://xxxxx.ngrok-free.app/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
  },
  "validDomains": [
    "xxxxx.ngrok-free.app"
  ]
}

4) Code client — obtenir le jeton SSO

Exemple minimal pour récupérer le jeton SSO avec TeamsFx côté client (React) :

import { TeamsUserCredential } from "@microsoft/teamsfx";

async function getSsoToken() {
const credential = new TeamsUserCredential();
// Selon la version de TeamsFx, la méthode peut s'appeler getSSOToken() ou getToken()
const token = await credential.getSSOToken();
return token;
}

getSsoToken()
.then(token => console.log("SSO token OK, longueur:", token.length))
.catch(err => console.error("SSO token error:", err)); 

Bonnes pratiques : n’exposez pas le jeton dans la console en production ; préférez l’envoyer en HTTPS à votre API pour échange OBO (voir ci‑dessous) lorsque vous devez appeler Microsoft Graph ou une API downstream.

5) Côté serveur — échange OBO vers Microsoft Graph (recommandé)

Envoyez le jeton SSO au backend, qui le convertit en jeton Graph via l’On‑Behalf‑Of. Exemple Node/Express schématique :

import express from "express";
import fetch from "node-fetch";

const app = express();
app.use(express.json());

app.post("/api/me", async (req, res) => {
const ssoToken = (req.headers\["authorization"] || "").replace("Bearer ", "");
try {
// 1) Échanger ssoToken contre un jeton Graph (OBO) via le point d'extrémité AAD
const oboToken = await exchangeForGraph(ssoToken, \["User.Read"]);
// 2) Appeler Graph au nom de l'utilisateur
const me = await callGraphMe(oboToken);
res.json(me);
} catch (e) {
res.status(401).json({ error: String(e) });
}
});

app.listen(53001, () => console.log("API prête"));

async function exchangeForGraph(ssoToken: string, scopes: string\[]): Promise\ {
// Implémentation OBO à base de client secret ou certificat (dépend de votre environnement)
// ...
return "eyJ..."; // jeton Graph
}

async function callGraphMe(oboToken: string) {
const r = await fetch("[https://graph.microsoft.com/v1.0/me](https://graph.microsoft.com/v1.0/me)", {
headers: { "Authorization": `Bearer ${oboToken}` }
});
return await r.json();
} 

Pourquoi ce modèle ? Il centralise les autorisations Graph, évite les CORS et limite l’exposition des secrets côté client. Vous pouvez bien sûr appeler votre propre API en direct si le SSO ne sert qu’à elle.

Tableau de contrôle — ce qu’il faut vérifier

Point à vérifierPourquoi c’est crucial
webApplicationInfo.resource = api://<FQDN>/<APPID> (identique à l’Application ID URI)Évite le mismatch entre ressource déclarée et origin iframe.
validDomains contient le même FQDN (ex. ngrok) que celui utilisé dans contentUrlTeams interdit le chargement d’origins non listés.
URI de redirection AAD incluent l’origin exact (ex. https://xxxxx.ngrok-free.app/auth-end)Sans cela, le flux de redirection échoue après authentification.
Re-sideload de l’app après modification du manifesteTeams peut mettre en cache une version antérieure du manifeste.
Environnements séparés : dev/test/prod via variables (.env) de Teams ToolkitRéduit les erreurs de copier‑coller et aligne automatiquement les URI.
Utiliser un tunnel en local (ngrok ou équivalent)Garantie que l’origin vu par Teams correspond au domaine déclaré.

Erreurs proches et leur signification

MessageCause la plus probableRemède
App resource defined in manifest and iframe origin do not matchRessource sans domaine (api://{client-id}) ou domaine qui ne correspond pas à l’origin.Adoptez api://<FQDN>/<APPID> et synchronisez manifeste + AAD.
invalid_client / unauthorized_clientIdentifiant AAD incohérent, application non autorisée ou URI de redirection absente.Vérifiez webApplicationInfo.id, redirections et consentements.
origin_mismatchLe navigateur tente d’authentifier depuis un origin non approuvé.Ajoutez le FQDN à validDomains et aux redirections AAD.

Checklist de validation rapide

  • Dans l’onglet chargé dans Teams, ouvrez DevTools et exécutez window.location.origin — notez la valeur exacte.
  • Dans AAD → Expose an API, confirmez : Application ID URI = api://<origin-domaine>/<APPID> (même FQDN, pas nécessairement le port).
  • Dans le manifeste Teams, webApplicationInfo.resource = exactement la même chaîne.
  • validDomains contient le FQDN (ex. xxxxx.ngrok-free.app).
  • Les URI de redirection incluent l’origin effectif (si vous avez des pages d’auth front).
  • Vous avez supprimé l’ancienne appli de Teams et re‑sideload la nouvelle.
  • Vous testez dans le même environnement (évitez mélange web/desktop avec caches obsolètes).
  • Le code appelle getSSOToken() après que Teams a initialisé l’SDK (ex. microsoftTeams.app.initialize() si vous utilisez l’API bas niveau).

Scénarios concrets et solutions

Scénario A — Dev en localhost

Vous chargez l’onglet depuis https://localhost:53000. Le SSO échoue avec l’erreur d’origin.

  • Pourquoi : l’Application ID URI est api://{client-id} ou pointe vers un autre domaine.
  • Solution : exposez votre app via ngrok (ex. https://abcd-1234.ngrok-free.app), définissez api://abcd-1234.ngrok-free.app/<APPID> dans AAD et le manifeste, ajoutez le FQDN à validDomains, re-sideload.

Scénario B — Domaine d’entreprise

Votre onglet est hébergé sous https://portal.exemple.com en prod, mais webApplicationInfo.resource est encore api://dev.exemple.com/<APPID>.

  • Pourquoi : désalignement entre prod et dev dans les fichiers de configuration.
  • Solution : externalisez les valeurs dans les .env générés par Teams Toolkit : TAB_DOMAIN, APP_ID_URI, etc., et automatisez le packaging par environnement.

Scénario C — Plusieurs apps AAD

Vous avez une app front et une app API. Vous mettez le client-id de l’API dans webApplicationInfo.id mais gardez une ressource api://{client-id-front}.

  • Pourquoi : mélange des identifiants front/API.
  • Solution : choisissez l’app AAD cible de votre SSO (souvent l’API), alignez id et resource sur la même app, et utilisez l’OBO côté serveur pour Graph.

Exemples « Avant / Après »

Avant (erreur)

"webApplicationInfo": {
  "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "resource": "api://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
},
"validDomains": ["localhost"]

Après (OK)

"webApplicationInfo": {
  "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "resource": "api://xxxxx.ngrok-free.app/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
},
"validDomains": ["xxxxx.ngrok-free.app"]

Astuce : vérifier le jeton pour confirmer la cible

Sans quitter le navigateur, vous pouvez inspecter le jeton retourné (en dev uniquement) :

const token = await credential.getSSOToken();
const payload = JSON.parse(atob(token.split(".")[1]));
console.log("audience (aud):", payload.aud); // doit être le client-id de l'app ciblée
console.log("tenant (tid):", payload.tid);

Si aud n’est pas l’ID de l’application que vous attendez, c’est un indice fort d’un mauvais resource dans le manifeste ou d’un Application ID URI mal configuré.

Foire aux questions (FAQ)

Dois‑je inclure le port dans l’Application ID URI ?

Évitez de dépendre du port : préférez un FQDN stable (tunnel en dev, domaine en prod). Cela simplifie les correspondances côté Teams et les redirections AAD. En pratique, la forme api://<fqdn>/<APPID> est la plus robuste.

Pourquoi api://{client-id} fonctionne ailleurs mais pas ici ?

Parce que pour un onglet Teams, la page vit dans une iframe. Teams effectue une vérification stricte entre le domaine qui héberge votre contenu et la ressource OAuth déclarée ; la forme « sans domaine » ne passe pas cette vérification.

Peut‑on appeler Microsoft Graph directement avec le jeton SSO ?

Le jeton SSO est émis pour la ressource que vous avez déclarée (votre Application ID URI). Le modèle le plus courant est d’envoyer ce jeton à votre backend qui l’échange via OBO contre un jeton Graph. Cela évite d’amplifier les permissions Graph dans le client.

Teams Desktop vs Teams Web : différence ?

Les deux utilisent la même logique de vérification de l’origin. En revanche, le cache du manifeste peut diverger : si vous modifiez la ressource, désinstallez puis réinstallez l’app dans l’environnement où vous testez.

Comment fiabiliser la bascule dev → prod ?

Paramétrez les valeurs sensibles (FQDN, APP_ID_URI, ID AAD) dans des fichiers .env par environnement et générez automatiquement le manifeste via Teams Toolkit lors du packaging. Moins de copier‑coller, moins d’erreurs.

Bonnes pratiques de sécurité

  • Ne logguez jamais les jetons en clair en production.
  • Effectuez l’échange OBO côté serveur ; stockez les secrets (client secret/certificat) dans un coffre (Key Vault ou équivalent).
  • Limitez les scopes Graph au minimum nécessaire.
  • Activez HTTPS partout (même en dev via tunnel).

Modèle de variables d’environnement (.env)

APP_ID=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
TAB_FQDN=xxxxx.ngrok-free.app
APP_ID_URI=api://xxxxx.ngrok-free.app/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
FRONTEND_URL=https://xxxxx.ngrok-free.app
API_BASE_URL=https://xxxxx.ngrok-free.app/api

Plan de dépannage avancé

  1. Vérifier l’origin réel : dans la console, window.location.origin. Il doit être identique (au domaine près) au FQDN de APP_ID_URI.
  2. Contrôler le manifeste actif : si vous voyez encore l’ancien resource, vous testez une version obsolète → supprimez l’application dans Teams puis réinstallez le package.
  3. Nettoyer les caches : fermez complètement Teams desktop (quittez), relancez, re-sideload.
  4. Regarder les en‑têtes réseau : toute redirection vers un origin non listé dans validDomains est suspecte.
  5. Décoder le JWT : si aud ≠ votre client‑id, vous ciblez la mauvaise ressource.
  6. Comparer dev/prod : un fichier .env mal sélectionné suffit à casser le SSO.

Résumé opérationnel

L’erreur « App resource defined in manifest and iframe origin do not match » signale une discordance entre le domaine qui héberge l’onglet (origin) et la ressource OAuth (Application ID URI) publiée dans AAD et référencée dans le manifeste Teams. La solution universelle consiste à basculer votre ressource vers le format api://<FQDN>/<APPID>, à reporter cette valeur dans webApplicationInfo.resource, à ajouter le FQDN à validDomains, puis à re-sideload l’application. En appliquant ces étapes et en isolant proprement vos environnements via des variables, vous sécurisez un SSO stable et prévisible—en dev comme en production.


Annexe — Rappel des éléments à aligner

  • Origin de l’onglet : domaine réel de l’iframe (ex. https://xxxxx.ngrok-free.app).
  • Application ID URI : api://<FQDN>/<APPID>.
  • Manifeste Teams : webApplicationInfo.resource = même valeur.
  • validDomains : inclut ce même FQDN.
  • URI de redirection AAD : utilisent le même origin.

Avec cet alignement, getSSOToken() réussit et la chaîne d’authentification Teams → AAD devient fiable. Vous pouvez ensuite appeler votre API et/ou utiliser l’OBO pour Microsoft Graph sans friction.

Sommaire