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.
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
- Application ID URI:
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)
- Abra o registro da sua aplicação (App Registration).
- 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>
.
- Defina como
- Confirme que Redirect URIs (em Authentication) incluem
https://localhost:<porta>
e toda URL pública que você usa (ngrok, Azure etc.). - 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 noapi://<domínio>/<AppID>
deve corresponder ao domínio doiframe
. - 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
Ambiente | Domínio do site | Application ID URI (resource) | Redirect URIs (Web) |
---|---|---|---|
Local | localhost | api://localhost/<AppID> | https://localhost:53000 (e portas usadas) |
Homologação | subdominio.contoso.com | api://contoso.com/<AppID> (ou api://subdominio.contoso.com/<AppID> se o host for realmente o subdomínio) | URL pública completa do site |
Produção | app.contoso.com | api://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://
: usehttps://
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 oresource
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
ougetAuthToken
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.
Sintoma | Causa provável | Correção |
---|---|---|
“manifest e iframe origin não coincidem” | resource no manifest usa api://<clientId> ou domínio diferente | Mudar para api://<FQDN>/<AppID> e fazer sideload novamente |
invalid_audience 700016 | Application ID URI não confere com o token esperado pelo backend/SPA | Alinhar Application ID URI e webApplicationInfo.resource |
Funciona local, falha no PRD | Domí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://<domínio>/<AppID>
}
}
Variáveis por ambiente:
Chave | Local | Homologação | Produção |
---|---|---|---|
AADAPPRESOURCE | api://localhost/<AppID> | api://contoso.com/<AppID> | api://contoso.com/<AppID> |
CONTENT_URL | https://localhost:53000 | https://hml.contoso.com | https://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(() => {
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 (
<div>
<button onClick={getToken}>Obter SSO</button>
{token && <pre>{token}</pre>}
{err && <p style={{color:'red'}}>{err}</p>}
</div>
);
}
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
, escolhaapi://sub.contoso.com/<AppID>
ouapi://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ãoapi://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
usaapi://<FQDN>/<AppID>
(ouapi://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
- 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”. - Misturar domínios no mesmo app: hospedar a tab em
*.contoso.com
mas usarapi://fabrikam.com/<AppID>
como recurso. Resultado: falha de SSO. - Esquecer de limpar o cache do Teams: o cliente mantém o manifest antigo e os testes parecem “inexplicavelmente” inconsistentes.
- 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
- Teams carrega a tab no
iframe
com origem do seu site. - A tab chama
getSSOToken()
(via TeamsFx ou SDK do Teams). - O cliente valida que o resource aponta para um Application ID URI cujo domínio corresponde ao da origem do
iframe
. - 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)
- Identifique o domínio que hospeda sua tab (local, túnel, produção).
- No Entra ID: defina
Expose an API > Application ID URI
paraapi://<FQDN>/<AppID>
. - No
manifest.json
: alinhewebApplicationInfo.id
(clientId) ewebApplicationInfo.resource
(Application ID URI). - Inclua todas as Redirect URIs necessárias (Web) e habilite “ID tokens”.
- Limpe o cache do Teams, gere o pacote e faça sideload novamente.
- 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.