Corrigir erro “App resource defined in manifest and iframe origin do not match” no SSO do Microsoft Teams (React, TeamsFx)

Se a sua guia React no Microsoft Teams falha ao chamar getSSOToken com “App resource defined in manifest and iframe origin do not match”, este guia explica a causa real e mostra a correção: alinhar o Application ID URI/manifest ao domínio FQDN que hospeda a tab.

Índice

Visão geral do problema

Durante o desenvolvimento de uma guia (tab) para o Microsoft Teams com React e Fluent UI, é comum implementar Single Sign‑On (SSO) usando @microsoft/teamsfx (ou o SDK do Teams diretamente). Em muitos projetos, ao chamar TeamsUserCredential.getSSOToken() surge a exceção:

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

O cenário típico é este:

  • Manifest do Teams com o bloco webApplicationInfo parecido com: { "webApplicationInfo": { "id": "my-aad-app-client-id", "resource": "api://my-aad-app-client-id" } }
  • Microsoft Entra ID (antigo Azure AD):
    • Application ID URI: api://my-aad-app-client-id
    • Redirect URI para desenvolvimento local: https://localhost:53000

Mesmo com os valores “alinhados”, o erro persiste. A raiz está no domínio que o Teams usa para hospedar a sua tab dentro de um iframe e no domínio embutido no seu Application ID URI.

Por que a mensagem aparece

Quando o Teams renderiza uma guia, ele o faz dentro de um iframe cuja origem (origin) corresponde ao domínio do seu site/túnel (localhost, ngrok, Azure Static Web Apps, Azure App Service etc.). Para o fluxo de SSO, o cliente do Teams precisa ter certeza de que o recurso (resource/Audience) do token solicitado é “propriedade” da mesma origem que está hospedando o conteúdo.

Em termos simples:

[Teams Desktop/Web]
       |
       |  (carrega sua guia em um iframe)
       v
[iframe origin]  = https://<SEU_DOMÍNIO>[/caminho]
       |
       |  (getSSOToken para o recurso "api://<ALGO>")
       v
[Application ID URI]  = api://<DOMÍNIONOURI>/<AppID>

Se  !=  → ERRO:
“App resource defined in manifest and iframe origin do not match” 

Quando se usa o formato antigo api://<clientId> como Application ID URI, não há domínio explícito — e, portanto, em um cenário de tab (conteúdo hospedado em uma origem de site), a validação falha. É por isso que o erro costuma surgir ao migrar de exemplos mínimos para um app real hospedado na web.

Solução comprovada

A solução efetiva é alterar o Application ID URI (e o campo webApplicationInfo.resource no manifest) para o formato:

api://<domínio-totalmente-qualificado>/<AppID>

Exemplo:

api://contoso.com/11111111-2222-3333-4444-555555555555

Ou, para desenvolvimento local:

api://localhost/11111111-2222-3333-4444-555555555555

Esse formato torna explícita a associação entre o recurso do token e a origem do iframe no Teams. O domínio do Application ID URI deve corresponder ao domínio que hospeda a sua guia.

Passo a passo no Microsoft Entra ID (Azure AD)

  1. Abra o registro da sua aplicação (App Registration).
  2. Vá em Expose an API e ajuste o Application ID URI:
    • Defina como api://<seu-dominio>/<seu-app-guid> (por exemplo, api://contoso.com/<AppID>).
    • Para desenvolvimento local, use api://localhost/<AppID>.
  3. Confirme que Redirect URIs (em Authentication) incluem https://localhost:<porta> e toda URL pública que você usa (ngrok, Azure etc.).
  4. Garanta que o tipo de plataforma para SSO em tab esteja como Web e que “ID tokens” esteja habilitado.

Passo a passo no Manifest do Teams

No arquivo manifest.json do aplicativo do Teams, ajuste o bloco webApplicationInfo para refletir o mesmo Application ID URI:

{
  "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.16/MicrosoftTeams.schema.json",
  "manifestVersion": "1.16",
  ...
  "webApplicationInfo": {
    "id": "11111111-2222-3333-4444-555555555555",
    "resource": "api://contoso.com/11111111-2222-3333-4444-555555555555"
  },
  ...
}

Para desenvolvimento local:

{
  "webApplicationInfo": {
    "id": "11111111-2222-3333-4444-555555555555",
    "resource": "api://localhost/11111111-2222-3333-4444-555555555555"
  }
}

Limpe cache e faça sideload novamente

  • Windows: feche o Teams e remova %appdata%\Microsoft\Teams.
  • macOS: feche o Teams e remova ~/Library/Application Support/Microsoft/Teams.

Depois, faça o sideload novamente do pacote do app (zip do manifest e ícones). Sem isso, o cliente pode continuar usando um manifest antigo e replicar o erro.

Entendendo por que funciona

  • Correspondência de origem: o Teams carrega a sua guia em um iframe cuja origem é o domínio do seu site. Ao pedir um token SSO para um resource, o domínio no api://<domínio>/<AppID> deve corresponder ao domínio do iframe.
  • Padrão recomendado: o formato api://{FQDN}/{appGUID} evita conflitos ao alternar entre localhost, túneis temporários (ngrok) e produção, mantendo a semântica de “um recurso por domínio”.

Boas práticas que evitam regressões

URLs consistentes por ambiente

AmbienteDomínio do siteApplication ID URI (resource)Redirect URIs (Web)
Locallocalhostapi://localhost/<AppID>https://localhost:53000 (e portas usadas)
Homologaçãosubdominio.contoso.comapi://contoso.com/<AppID> (ou api://subdominio.contoso.com/<AppID> se o host for realmente o subdomínio)URL pública completa do site
Produçãoapp.contoso.comapi://contoso.com/<AppID> (ou api://app.contoso.com/<AppID> conforme sua estratégia)URL pública completa do site

Dica: mantenha um mesmo domínio base entre HML e PRD (por exemplo, ambos sob contoso.com), reduzindo mudanças no Application ID URI. Se isso não for possível, considere separar os registros de app (um por ambiente).

Redirect URIs

  • Adicione explicitamente todas as URLs usadas pelo seu front-end (localhost, domínio público, caminhos de retorno como /auth-end caso utilize).
  • Garanta que a plataforma é Web e que “ID tokens” está habilitado.
  • Evite http://: use https:// em todos os ambientes (exceto localhost, quando o certificado local não está disponível).

Developer Portal do Teams (App Studio)

  • Marcar a tab como “Requires SSO” cria/valida o bloco webApplicationInfo.
  • Confirme se o id corresponde ao Application (client) ID e se o resource replica o seu Application ID URI exato.

Cache do cliente Teams

Sempre que alterar o manifest, limpe o cache do cliente antes de um novo sideload. Essa etapa evita leituras do manifest anterior e elimina falsos negativos de configuração.

Diagnóstico rápido

  • DevTools (F12) no Teams: filtre por authStart, authError ou getAuthToken para ver a chamada que falhou e qual resource estava sendo pedido.
  • Logs de Sign-in no Entra ID (Azure AD): mensagens como invalid_audience (código 700016) indicam que o resource no token não bate com o que a aplicação espera.
SintomaCausa provávelCorreção
“manifest e iframe origin não coincidem”resource no manifest usa api://<clientId> ou domínio diferenteMudar para api://<FQDN>/<AppID> e fazer sideload novamente
invalid_audience 700016Application ID URI não confere com o token esperado pelo backend/SPAAlinhar Application ID URI e webApplicationInfo.resource
Funciona local, falha no PRDDomínio mudou (ngrok → domínio corporativo)Atualizar Application ID URI para o novo FQDN

Exemplos práticos de configuração

Manifest: configuração por ambiente

Use variáveis de ambiente e substituição em build/pipeline para gerar o manifest.json certo para cada destino.

// manifest.base.json
{
  "manifestVersion": "1.16",
  "version": "1.0.0",
  "id": "${TEAMSAPPID}",
  "packageName": "com.contoso.teams.tab",
  "developer": { "name": "Contoso", "websiteUrl": "https://contoso.com", "privacyUrl": "https://contoso.com/privacy", "termsOfUseUrl": "https://contoso.com/terms" },
  "name": { "short": "Contoso Tab", "full": "Contoso Tab SSO" },
  "description": { "short": "Tab com SSO", "full": "Exemplo de tab com SSO via TeamsFx" },
  "accentColor": "#6264A7",
  "staticTabs": [
    {
      "entityId": "contosoTab",
      "name": "Contoso",
      "contentUrl": "${CONTENT_URL}",
      "scopes": [ "personal" ]
    }
  ],
  "webApplicationInfo": {
    "id": "${AADAPPCLIENT_ID}",
    "resource": "${AADAPPRESOURCE}" // api://&lt;domínio&gt;/&lt;AppID&gt;
  }
}

Variáveis por ambiente:

ChaveLocalHomologaçãoProdução
AADAPPRESOURCEapi://localhost/<AppID>api://contoso.com/<AppID>api://contoso.com/<AppID>
CONTENT_URLhttps://localhost:53000https://hml.contoso.comhttps://app.contoso.com

Script simples para gerar o manifest

// scripts/build-manifest.js
const fs = require('fs');

function must(name) {
if (!process.env[name]) throw new Error(`Env ${name} ausente`);
return process.env[name];
}

const base = fs.readFileSync('manifest.base.json', 'utf8');
const replaced = base
.replace(/${TEAMSAPPID}/g, must('TEAMSAPPID'))
.replace(/${AADAPPCLIENTID}/g, must('AADAPPCLIENTID'))
.replace(/${AADAPPRESOURCE}/g, must('AADAPPRESOURCE'))
.replace(/${CONTENTURL}/g, must('CONTENTURL'));

fs.writeFileSync('manifest.json', replaced);
console.log('manifest.json gerado com sucesso'); 

Código de exemplo: obtendo o SSO token com TeamsFx

Exemplo em React com @microsoft/teams-js e @microsoft/teamsfx para capturar o token e lidar com erros de origem:

import React, { useEffect, useState } from 'react';
import { app } from '@microsoft/teams-js';
import { TeamsUserCredential, ErrorWithCode, ErrorCode } from '@microsoft/teamsfx';

export default function SsoButton() {
  const [token, setToken] = useState(null);
  const [err, setErr] = useState(null);

  useEffect(() =&gt; {
    app.initialize().catch(setErr);
  }, []);

  async function getToken() {
    try {
      const credential = new TeamsUserCredential();
      const sso = await credential.getToken('');
      setToken(sso.token);
    } catch (e) {
      if (e instanceof ErrorWithCode) {
        // Ajuda a identificar falhas de origem/recurso
        console.error('TeamsFx error code:', e.code, 'message:', e.message);
        if (e.code === ErrorCode.InternalError) {
          setErr('Falha ao obter SSO. Verifique se o Application ID URI/manifest usam o mesmo domínio do site.');
        } else {
          setErr(e.message);
        }
      } else {
        setErr(String(e));
      }
    }
  }

  return (
    &lt;div&gt;
      &lt;button onClick={getToken}&gt;Obter SSO&lt;/button&gt;
      {token &amp;&amp; &lt;pre&gt;{token}&lt;/pre&gt;}
      {err &amp;&amp; &lt;p style={{color:'red'}}&gt;{err}&lt;/p&gt;}
    &lt;/div&gt;
  );
}

Observação: o parâmetro '' em getToken('') indica escopos vazios adicionais porque, para SSO em tabs, o Teams emite um token de “logon único” para o seu recurso configurado. Os escopos de API (Graph, serviços próprios) entram mais à frente, quando você troca esse token por um access token no backend via On‑Behalf‑Of (OBO), se aplicável.

Validações úteis antes de publicar

  • Regra do domínio: Se o conteúdo da tab é servido por https://sub.contoso.com, escolha api://sub.contoso.com/<AppID> ou api://contoso.com/<AppID>. Evite usar um domínio completamente diferente (ex.: outro provedor/empresa).
  • Localhost não tem porta no Application ID URI: use api://localhost/<AppID>, não api://localhost:53000/<AppID>.
  • Túneis temporários: domínios que mudam a cada sessão (subdomínios dinâmicos) quebram a validação. Prefira um subdomínio estável.
  • Um Application ID URI por app: o valor é único. Para múltiplos ambientes com domínios radicalmente distintos, avalie um registro de app por ambiente.

Checklist de solução (cópia e cole no seu PR)

  • [ ] Expose an API > Application ID URI usa api://<FQDN>/<AppID> (ou api://localhost/<AppID> local).
  • [ ] manifest.json > webApplicationInfo.resource está idêntico ao Application ID URI.
  • [ ] webApplicationInfo.id == Application (client) ID.
  • [ ] Redirect URIs incluem todas as URLs reais (localhost e públicas).
  • [ ] Plataforma “Web” com “ID tokens” habilitado.
  • [ ] Cache do Teams limpo e pacote re‑instalado (sideload).
  • [ ] DevTools sem erros de authError na aba Network/Console.

Erros comuns e como evitá-los

  1. Confiar em api://<clientId> em produção: esse formato ignora o domínio do site, falhando em tabs. O resultado é o erro de “origin do iframe não coincide”.
  2. Misturar domínios no mesmo app: hospedar a tab em *.contoso.com mas usar api://fabrikam.com/<AppID> como recurso. Resultado: falha de SSO.
  3. Esquecer de limpar o cache do Teams: o cliente mantém o manifest antigo e os testes parecem “inexplicavelmente” inconsistentes.
  4. Redirect URIs incompletas: trocar ngrok por um host público e não adicionar a nova URL gera falhas no fluxo de autenticação.

Automação: garantindo consistência em CI/CD

Inclua um passo no pipeline para “carimbar” o Application ID URI conforme o ambiente, impedindo que um manifest de DEV chegue à PRODUÇÃO.

# Exemplo (YAML genérico)
steps:
  - script: |
      node scripts/build-manifest.js
    env:
      TEAMSAPPID: $(TEAMSAPPID)
      AADAPPCLIENTID: $(AADAPPCLIENTID)
      AADAPPRESOURCE: $(AADAPPRESOURCE) # api://contoso.com/$(AADAPPCLIENT_ID)
      CONTENTURL: $(CONTENTURL)           # https://app.contoso.com
    displayName: 'Gerar manifest.json'

Adicionalmente, valide o conteúdo do manifest com um pequeno teste:

// test/manifest.resource.test.js
const fs = require('fs');

test('webApplicationInfo.resource bate com o domínio do CONTENT_URL', () => {
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
const contentUrl = process.env.CONTENT_URL;
const resource = manifest.webApplicationInfo.resource; // api://dominio/AppID

const urlHost = new URL(contentUrl).host;           // app.contoso.com
const resourceHost = resource.split('/')[2];        // dominio do api://

expect(resourceHost.endsWith(urlHost) || urlHost.endsWith(resourceHost))
.toBe(true);
}); 

Arquitetura resumida do SSO em tabs

  1. Teams carrega a tab no iframe com origem do seu site.
  2. A tab chama getSSOToken() (via TeamsFx ou SDK do Teams).
  3. O cliente valida que o resource aponta para um Application ID URI cujo domínio corresponde ao da origem do iframe.
  4. Com o token SSO obtido, você pode:
    • Consumir APIs do Microsoft Graph através de um backend que realiza o fluxo On‑Behalf‑Of (OBO),
    • ou trocar por tokens para APIs próprias.

Perguntas frequentes (FAQ)

Preciso usar exatamente o domínio “raiz” (apex) no Application ID URI?
Não necessariamente. Você pode usar o subdomínio exato que hospeda a aplicação (api://app.contoso.com/<AppID>). O importante é que o domínio do resource corresponda ao domínio que carrega a tab.

Posso usar portas no Application ID URI?
Não. O FQDN do Application ID URI não inclui porta. Para localhost, use api://localhost/<AppID>. Portas são configuradas (se necessário) nas Redirect URIs.

Uso túneis que mudam a cada execução. O que fazer?
Domínios que mudam quebram a verificação de origem. Prefira um subdomínio estável (reservado) ou um hostname previsível.

Tenho um único registro de app para DEV/HML/PRD. É aceitável?
É possível, contanto que o Application ID URI e as Redirect URIs suportem o domínio que de fato hospedará a tab em cada ambiente. Caso os domínios sejam muito diferentes, separar registros por ambiente reduz fricção.

Guia de resolução passo a passo (recapitulando)

  1. Identifique o domínio que hospeda sua tab (local, túnel, produção).
  2. No Entra ID: defina Expose an API > Application ID URI para api://<FQDN>/<AppID>.
  3. No manifest.json: alinhe webApplicationInfo.id (clientId) e webApplicationInfo.resource (Application ID URI).
  4. Inclua todas as Redirect URIs necessárias (Web) e habilite “ID tokens”.
  5. Limpe o cache do Teams, gere o pacote e faça sideload novamente.
  6. Verifique no F12 e, se necessário, nos logs de sign‑in do Entra ID.

Resumo em uma linha

Troque o Application ID URI (e o webApplicationInfo.resource) para api://<domínio‑FQDN>/<AppID>, garantindo que o domínio do recurso coincide com a origem do iframe no Teams — e o SSO volta a funcionar.


Anexos práticos

Modelo de Application ID URI por ambiente

# Local
api://localhost/<AppID>

Homologação (mesmo domínio raiz de produção)

api://contoso.com/

Produção

api://contoso.com/ 

Snippets rápidos

Checagem estática do manifest (Node.js)

const manifest = require('./manifest.json');
const { host: contentHost } = new URL(process.env.CONTENT_URL);
const resourceHost = manifest.webApplicationInfo.resource.split('/')[2];

if (!(resourceHost === contentHost || contentHost.endsWith(resourceHost) || resourceHost.endsWith(contentHost))) {
throw new Error(`Domínio do resource (${resourceHost}) não corresponde ao host do conteúdo (${contentHost}).`);
} 

Tratamento de erros no front-end

try {
  const cred = new TeamsUserCredential();
  const token = await cred.getToken('');
  // Use o token...
} catch (e) {
  // Mensagem amigável ao usuário final + log técnico
  console.error(e);
  alert('Não foi possível concluir o logon único. Tente novamente ou contate o suporte com a hora e o time/canal.');
}

Conclusão

O SSO de tabs no Microsoft Teams depende da confiança entre a origem do iframe e o resource solicitado. Ao migrar do formato antigo api://<clientId> para o padrão api://<FQDN>/<AppID>, você elimina conflitos de origem, configura ambientes de forma previsível e destrava a autenticação silenciosa para seus usuários. Combine essa mudança com Redirect URIs completas, automação de manifest e higiene de cache, e o erro “App resource defined in manifest and iframe origin do not match” deixa de ser um obstáculo no seu pipeline.

Índice