Como expor seus agentes de IA no Microsoft 365 Copilot sem reconstruir tudo
TL;DR: Se você tem um serviço agentic funcional (LangChain, Semantic Kernel, etc.) e quer expô-lo no Microsoft 365 Copilot, não precisa reconstruí-lo como um agente declarativo. Coloque um M365 Gateway stateless na frente — ele lida com o protocolo Bot Framework, valida tokens de canal e executa a primeira troca OBO. Seu serviço original permanece intacto, com sua própria lógica de orquestração, sessão e chamadas downstream delegadas. A conclusão principal: você ganha integração com o Copilot sem perder o controle sobre sua arquitetura.
Você construiu um serviço agentic. Ele funciona. Usa o framework que você escolheu — talvez LangChain, Semantic Kernel, Microsoft Agent Framework, ou algo totalmente próprio. Ele conversa com seu LLM, chama suas APIs downstream e gerencia conversas multi-turn do jeito que você projetou.
Agora alguém pergunta: "Dá pra colocar isso no Microsoft 365 Copilot?"
O caminho óbvio é reconstruir o agente usando M365 Copilot Agents (declarative ou custom engine). Mas isso significa abrir mão do controle — sobre sua lógica de orquestração, suas escolhas de framework, seu gerenciamento de sessão e a forma como você chama serviços downstream em nome do usuário logado.
Este guia é para o outro caminho. Aquele onde você mantém seu serviço agentic existente praticamente intacto e coloca um M365 Gateway na frente dele — um serviço stateless que lida com o protocolo Bot Framework, valida tokens de canal, realiza a primeira troca de token On-Behalf-Of (OBO) e traduz as conversas do Copilot para a API nativa do seu serviço. Seu serviço pode continuar agnóstico em relação a Bot e Teams, mas se ele possui acesso delegado a downstreams, deve validar o token de serviço recebido em sua própria fronteira e usar essa validação para sua própria cadeia OBO.
Quando usar este padrão?
- Você tem (ou quer construir) sua própria implementação agentic na tecnologia e framework de sua preferência
- M365 Copilot Agents — declarative ou custom engine — não te dão controle suficiente sobre orquestração, tool calling ou auth downstream
- Você quer propriedade total da lógica agentic: pro-code, seu LLM, seus prompts, suas ferramentas, seu session store
- Você precisa de acesso delegado a serviços downstream (bancos de dados, APIs, servidores MCP) via fluxos OBO encadeados, e quer ser dono da cadeia de tokens de ponta a ponta
O que segue é uma diretriz geral de desenvolvimento — framework-agnostic, linguagem-agnostic — para deploy de qualquer serviço agentic atrás do Azure Container Apps, expondo-o ao M365 Copilot através de um M365 Gateway e usando fluxos OBO encadeados para chamar serviços downstream como o usuário logado.
Qual a arquitetura?
A arquitetura separa responsabilidades em dois serviços independentemente implantáveis:
| Camada | Responsabilidade | Estado |
|---|---|---|
| M365 Copilot | Identidade do usuário, SSO, UX de conversa | Nenhum (plataforma) |
| Gateway | Adaptador de protocolo Bot, auth de canal, troca OBO #1 | Stateless |
| Serviço Agentic | Lógica de negócio, validação de token, OBO downstream, memória de sessão | Stateful |
| Downstream | Dados, APIs, servidores MCP — chamados como o usuário delegado | Externo |
Por que dois serviços?
- Separação de trust boundaries — O gateway lida com protocolo Bot Framework e auth de canal. O serviço é dono do acesso a dados/negócio, valida o bearer que recebe e nunca precisa de credenciais de Bot.
- Escalabilidade independente — O gateway pode escalar para muitas réplicas. O serviço (com sessões em memória) roda como réplica única para MVP.
- Liberdade de framework — O serviço pode usar qualquer framework agentic, provedor de LLM ou lógica customizada. O gateway é apenas um forwarder HTTP.
- Adapte sem modificar — Se você já tem um serviço agentic, pode levá-lo ao M365 Copilot escrevendo apenas o gateway + um adapter de cliente de serviço. O serviço existente fica intocado.
Como funciona o fluxo de tokens?
A identidade do usuário flui de ponta a ponta através de duas trocas OBO encadeadas. Nenhum serviço armazena ou armazena em cache senhas de usuário.
Princípios chave
- O token do usuário nunca sai da cadeia OBO. Cada serviço recebe uma assertion escopada e a troca para o próximo hop. Nenhum serviço vê a senha original.
- A validação JWT é inegociável — e dividida por fronteira. O gateway deve validar o token de canal que recebe do Microsoft 365. O serviço deve validar o bearer scoped antes de usá-lo para OBO, ownership de sessão ou scoping de usuário.
- Ingress interno para o serviço é hardening recomendado, não a única opção segura. No ACA, private ingress é um padrão forte. Se o serviço permanecer acessível externamente, ele deve forçar sua própria validação de bearer e checagens de autorização exatamente da mesma forma.
- ContextVar carrega a assertion validada. O serviço vincula o JWT validado a uma ContextVar async-safe no middleware, para que qualquer chamada OBO downstream dentro da mesma requisição o pegue automaticamente.
Quais registros de app no Entra ID são necessários?
Você precisa de dois registros de app no Entra ID, além de saber o resource app ID da sua API downstream.
Passo a passo do registro
1. App do Serviço Agentic
| Configuração | Valor |
|---|---|
| Display name | your-service-api |
| Identifier URI | api://<service-client-id> |
| Sign-in audience | AzureADMyOrg (single tenant) |
| requestedAccessTokenVersion | 2 |
| Exposed scope | access_as_user (delegated, User consent) |
| Required permissions | Downstream API user_impersonation (delegated) |
| Client secret | Sim — necessário para MSAL ConfidentialClient OBO |
2. App do Gateway / Bot
| Configuração | Valor |
|---|---|
| Display name | your-bot |
| Identifier URI | api://botid-<bot-client-id> (convenção Bot SSO) |
| Sign-in audience | AzureADMyOrg |
| requestedAccessTokenVersion | 2 |
| Exposed scope | access_as_user (delegated) |
| Required permissions | Service app access_as_user (delegated) |
| Redirect URI | https://token.botframework.com/.auth/web/redirect |
| Preauthorized clients | Teams Desktop (1fec8e78-...) e Teams Web (5e3ce6c0-...) |
| Client secret | Sim — necessário para Bot Framework auth |
3. Consentimento do admin (obrigatório)
Ambas as permissões delegadas exigem consentimento do admin do tenant:
Gateway/Bot app → Service app access_as_user ← consentimento admin
Service app → Downstream user_impersonation ← consentimento admin
Sem isso, chamadas OBO retornam AADSTS65001: The user or administrator has not consented to use the application.
4. Conexão OAuth do Azure Bot
Crie uma conexão OAuth no recurso Azure Bot para que o Microsoft Agents SDK possa fazer troca de token silenciosa para o hop gateway-para-serviço:
az bot authsetting create \
-g $RESOURCE_GROUP \
-n $BOT_RESOURCE_NAME \
-c SERVICE_CONNECTION \
--service Aadv2 \
--client-id $BOT_APP_ID \
--client-secret $BOT_APP_PASSWORD \
--provider-scope-string "$SERVICE_API_SCOPE offline_access openid profile" \
--parameters TenantId="$TENANT_ID" TokenExchangeUrl="api://botid-$BOT_APP_ID"
Guia de implementação dos componentes
Componente 1: O M365 Gateway (Stateless, Adaptador de Protocolo)
O trabalho do gateway é ponte entre o protocolo Bot Framework e o contrato HTTP que seu serviço agentic expõe. Ele recebe atividades do Bot, valida o token de canal, troca o token SSO do usuário por um token scoped para o serviço (OBO #1) e traduz a chamada para a forma nativa da sua API.
Insight chave: Se você já tem um serviço agentic stateful — mesmo que não tenha sido projetado para M365 — o gateway pode se adaptar a ele. Você não precisa modificar seu serviço existente para conformar-se a um contrato de API específico. O gateway absorve a tradução do protocolo M365. O serviço ainda deve validar o bearer scoped que chega à sua própria fronteira de API.
Responsabilidades de validação de token no Gateway
Isso é um requisito duro, não opcional. O gateway é a fronteira de confiança pública para a requisição do canal. Se a validação do token de canal for pulada ou incompleta, callers não autenticados podem alcançar seu caminho de forwarding.
O gateway deve validar o token de canal de acordo com os requisitos do SDK/Bot Framework para o canal que atende. Após o OBO #1 produzir um bearer scoped para o serviço, o gateway pode inspecionar esse token para diagnósticos, roteamento ou forwarding opcional de metadados, mas o serviço deve permanecer o validador autoritativo para aquele token de serviço.
Bom comportamento do gateway:
- Validar tráfego de canal de entrada usando o middleware Bot/Agents SDK ou verificações de issuer/signature/audience equivalentes
- Adquirir o token scoped para o serviço com OBO #1
- Forward o bearer do serviço como está em
Authorization: Bearer ... - Opcionalmente extrair claims para logging ou headers de conveniência:
oid(user object ID),tid(tenant ID),upnoupreferred_username - Tratar headers forwardados como metadados não autoritativos. Eles ajudam com telemetria, correlação ou debug, mas o serviço deve derivar identidade do bearer validado que recebe.
Adaptando ao contrato de API do seu serviço
O gateway contém um service client — uma pequena classe adaptadora HTTP que traduz entre o modelo de conversa do Copilot e a API nativa do seu serviço. É aqui que você mapeia:
| Conceito do Copilot | Equivalente no seu serviço | Mapeado no service client |
|---|---|---|
conversation.id |
Session/thread/chat ID | Mapear na criação ou primeira mensagem |
| Texto da mensagem do usuário | Campo do body da requisição (pode ser text, message, prompt, input, etc.) |
Remodelar o payload da requisição |
| Texto de resposta | Campo da resposta (pode ser reply, response, output, content, etc.) |
Extrair do payload de resposta |
| Autenticação | Bearer token, API key ou header customizado | Anexar o token delegado scoped no header/campo correto |
Exemplo: adaptando a diferentes formas de serviço
# Service client para um serviço com POST /chat/{thread_id}
class MyServiceClient:
async def send_turn(self, session_id: str, text: str, token: str) -> str:
resp = await self.http.post(
f"{self.base_url}/chat/{session_id}",
json={"prompt": text, "stream": False},
headers={"Authorization": f"Bearer {token}"},
)
return resp.json()["response"] # extrai da forma de resposta do serviço
# Service client para um serviço com POST /v1/conversations/{id}/messages
class AnotherServiceClient:
async def send_turn(self, session_id: str, text: str, token: str) -> str:
resp = await self.http.post(
f"{self.base_url}/v1/conversations/{session_id}/messages",
json={"content": text, "role": "user"},
headers={"X-Api-Token": token},
)
return resp.json()["choices"][0]["message"]["content"]
O message handler do gateway permanece o mesmo independentemente da forma do service client:
async def handle_message(context):
token = await agent_auth.get_token(context)
session_id = context.activity.conversation.id
reply = await service_client.send_turn(session_id, context.activity.text, token)
await context.send_activity(reply)
O que o gateway precisa de qualquer serviço
O gateway só precisa que o serviço suporte três capacidades — em qualquer forma de API:
- Identidade de sessão/thread — Alguma forma de manter estado de conversa entre turnos (session ID, thread ID, conversation ID, etc.)
- Troca de mensagens — Um endpoint que aceita texto do usuário e retorna uma resposta
- Autenticação — Aceita um bearer token ou outra credencial que o gateway possa fornecer via OBO #1
O serviço não precisa usar um padrão de URL, schema de requisição/resposta ou framework específico. A classe service client do gateway é o único lugar onde você codifica esses mapeamentos. O serviço ainda deve validar o bearer que recebe antes de confiar em qualquer identidade de usuário implicada pela chamada.
Detalhes chave de implementação
Auth handlers — O gateway precisa de dois caminhos de handler de auth:
- Agentic path (
AgenticUserAuthorization): Usado quando a atividade chega através do canal agentic do M365 Copilot. Requer tantoabs_oauth_connection_namequantoobo_connection_name. - Connector path (
UserAuthorization): Usado quando a atividade chega através do conector padrão do Teams/Bot. Requer apenasabs_oauth_connection_name.
auth_handlers = {
"service_agentic": AuthHandler(
auth_type="AgenticUserAuthorization",
abs_oauth_connection_name="SERVICE_CONNECTION",
obo_connection_name="SERVICE_OBO_CONNECTION", # pode ser o mesmo que o abs
scopes=["api://<service-client-id>/access_as_user"],
),
"service_connector": AuthHandler(
auth_type="UserAuthorization",
abs_oauth_connection_name="SERVICE_CONNECTION",
obo_connection_name="",
scopes=["api://<service-client-id>/access_as_user"],
),
}
Tratamento de invoke — O Copilot envia atividades invoke durante o handshake de troca de token SSO. O gateway deve tratá-las graciosamente (retornar sem erro) ou o fluxo de login quebra.
Mapeamento de sessão — Use context.activity.conversation.id como chave de sessão ao forwardar para o serviço. Isso garante que a mesma conversa do Copilot sempre mapeie para a mesma sessão agentic.
Healthz bypass — A health probe do ACA atinge /healthz. Ignore o middleware JWT para este caminho ou a probe falhará.
Escolhas de tecnologia — O gateway usa microsoft-agents-hosting-fastapi para o adapter Bot SDK, microsoft-agents-authentication-msal para troca de token baseada em MSAL e httpx para chamadas HTTP de forwarding ao serviço. Você pode substituir qualquer Bot SDK ou linguagem, desde que lide com o mesmo protocolo.
Componente 2: O Serviço Agentic (Stateful, Framework-Agnostic)
O serviço é uma API HTTP comum. Ele pode não saber nada sobre Bot Framework, Teams ou formas de atividade específicas do Copilot, mas se possui acesso delegado a downstream, deve validar o bearer de serviço recebido em sua própria fronteira. Ele recebe o token scoped, valida, vincula claims do usuário a partir desse token validado, executa sua lógica agentic e retorna uma resposta.
Modelo de confiança do serviço: O serviço é a fronteira de confiança de negócios/dados. Ele deve confiar no token bearer validado que recebe, não apenas em headers de conveniência. Headers forwardados X-User-* são metadados opcionais e nunca devem ser a única base para scoping de usuário ou OBO downstream.
Requisitos mínimos do serviço
| Requisito | O que significa | Por quê |
|---|---|---|
| Validar bearer de entrada | Ler e validar o header Authorization: Bearer |
Necessário antes de usar o token para OBO, ownership ou autorização |
| Extrair claims do token validado | Decodificar oid, tid, upn/preferred_username, scopes, etc. |
Necessário para isolamento de dono de sessão e trilha de auditoria |
| Identidade de sessão/thread | Manter estado da conversa entre turnos | O Copilot espera conversas multi-turn |
| Troca de mensagens | Aceitar texto do usuário, retornar uma resposta | Função central |
| OBO #2 (se acesso delegado for necessário) | acquire_token_on_behalf_of() do MSAL com a assertion forwardada |
Apenas se estiver chamando APIs downstream como o usuário |
| Ingress hardening | Preferir ingress interno ou controles de rede equivalentes quando prático | Reduz superfície de ataque, mas não substitui validação de token no serviço |
Padrão ContextVar para a assertion OBO
A assertion JWT validada (a string de token bruta do gateway) deve estar disponível profundamente na pilha de chamadas quando uma chamada OBO downstream acontece — potencialmente várias camadas abaixo do handler HTTP. Use contextvars.ContextVar em vez de passá-la através de cada assinatura de função:
from contextvars import ContextVar, Token as CtxToken
_USER_ASSERTION: ContextVar[str | None] = ContextVar("user_assertion", default=None)
_USER_CLAIMS: ContextVar[TokenClaims | None] = ContextVar("user_claims", default=None)
def bind_identity(assertion: str, claims: TokenClaims):
return _USER_ASSERTION.set(assertion), _USER_CLAIMS.set(claims)
def reset_identity(a_tok: CtxToken, c_tok: CtxToken):
_USER_ASSERTION.reset(a_tok)
_USER_CLAIMS.reset(c_tok)
# No middleware, após validar o bearer:
a_tok, c_tok = bind_identity(raw_jwt, validated_claims)
try:
response = await call_next(request)
finally:
reset_identity(a_tok, c_tok) # sempre limpar
Por que ContextVar? — É async-safe (cada requisição concorrente ganha seu próprio escopo), framework-agnostic (funciona com FastAPI, Flask, Django, asyncio puro) e evita threadar a assertion por toda a sua pilha agentic.
OBO #2 para serviços downstream
Quando sua lógica agentic precisa chamar uma API downstream como o usuário:
import msal
app = msal.ConfidentialClientApplication(
client_id=SERVICE_CLIENT_ID,
client_credential=SERVICE_CLIENT_SECRET,
authority=f"https://login.microsoftonline.com/{TENANT_ID}",
)
result = app.acquire_token_on_behalf_of(
user_assertion=_USER_ASSERTION.get(), # da ContextVar
scopes=["<downstream-resource-id>/.default"], # ex.: Graph, Databricks, sua API
)
downstream_token = result["access_token"]
Exemplos comuns de escopos downstream:
| Downstream | Scope |
|---|---|
| Microsoft Graph | https://graph.microsoft.com/.default |
| Azure Databricks | 2ff814a6-3304-4ab8-85cb-cd0e6f879c1d/.default |
| Azure SQL | https://database.windows.net/.default |
| API interna customizada | api://<their-app-id>/.default |
| Servidor MCP atrás do Entra | api://<mcp-app-id>/.default |
O padrão é idêntico para qualquer recurso protegido pelo Entra — apenas o escopo muda.
Gerenciamento de sessão
Session store mínimo viável para MVP (in-memory, réplica única):
- Create —
POST /api/chat/sessions→ retornasession_id - Send message —
POST /api/chat/sessions/{id}/messages→ retornareply - Get session —
GET /api/chat/sessions/{id}→ retorna histórico de turnos - Owner isolation — Toda sessão tem um
owner_iddo claimoiddo JWT validado. Rejeitar acesso cross-user com 403. - Turn windowing — Limitar turnos armazenados a
max_turns * 2entradas para evitar crescimento ilimitado de memória. - Concurrency lock —
asyncio.Lock()por sessão previne turnos intercalados.
Sua lógica agentic vai aqui
A API do serviço é uma fronteira limpa. Dentro dela, use o que quiser:
- Microsoft Agent Framework — HandoffBuilder, ConcurrentBuilder, etc.
- LangChain / LangGraph — chains, graphs, tool calling
- Semantic Kernel — planners e plugins
- Código customizado — chamadas diretas ao SDK OpenAI/Azure OpenAI com tool loops
- Qualquer outro framework — CrewAI, AutoGen, etc.
Contrato de API de referência
Se estiver construindo um novo serviço do zero, este contrato funciona bem com o gateway:
POST /api/chat/sessions/{session_id}/messages
Authorization: Bearer <service-scoped-token>
Content-Type: application/json
{"text": "user message"}
→ 200 {"session_id": "...", "reply": "...", "turns": [...]}
→ 401 (bad/missing token)
→ 404 (session not found — gateway auto-creates and retries)
No entanto, se você tem um serviço existente com um contrato diferente, não precisa mudá-lo. Em vez disso, adapte o service client do gateway para falar a API do seu serviço. As três capacidades essenciais são identidade de sessão, troca de mensagens e autenticação — os caminhos de URL e as formas de payload são flexíveis.
Componente 3: O Pacote de App M365
O pacote de app é um ZIP contendo um manifest e ícones que registra o gateway como um Custom Engine Agent no Microsoft 365.
Campos críticos do manifest
{
"bots": [{
"botId": "<BOT_APP_ID>",
"scopes": ["personal"]
}],
"webApplicationInfo": {
"id": "<BOT_SSO_APP_ID>",
"resource": "api://botid-<BOT_APP_ID>"
},
"copilotAgents": {
"customEngineAgents": [{
"type": "bot",
"id": "<BOT_APP_ID>",
"functionsAs": "agentOnly"
}]
},
"validDomains": [
"<gateway-host>.azurecontainerapps.io",
"token.botframework.com"
]
}
Sequência de deploy
Siga esta ordem exata. Cada passo depende dos outputs anteriores.
| Passo | O quê | Depende de |
|---|---|---|
| 1 | Criar service app + bot app no Entra ID | Acesso ao tenant |
| 2 | Conceder consentimento admin para ambas as cadeias de permissão delegada | Passo 1 |
| 3 | Criar recurso Azure Bot, configurar conexão OAuth (SERVICE_CONNECTION) | Passos 1-2 |
| 4 | Provisionar recursos downstream (bancos, APIs, warehouses) | Independente |
| 5 | Construir imagem container, fazer deploy do Serviço Agentic no ACA | Passos 1-4 |
| 6 | Validar /healthz, criação de sessão, primeiro turno, OBO para downstream |
Passo 5 |
| 7 | Construir imagem container, fazer deploy do M365 Gateway no ACA | Passos 1-3, 5 |
| 8 | Validar /healthz, forwarding gateway-para-serviço |
Passos 6-7 |
| 9 | Construir manifest.json + ícones em pacote ZIP | Passos 1, 7 |
| 10 | Fazer upload do ZIP no centro de admin do M365 ou Graph API | Passo 9 |
| 11 | Definir endpoint de mensagens do Azure Bot para https://<gateway>/api/messages |
Passos 3, 7 |
| 12 | Abrir M365 Copilot, enviar mensagem, verificar fluxo de ponta a ponta | Passos 10-11 |
Referência de variáveis de ambiente
Serviço Agentic
| Variável | Exemplo | Propósito |
|---|---|---|
| AZURE_TENANT_ID | b5d67878-... |
Entra tenant (para OBO #2) |
| SERVICE_CLIENT_ID | <service-app-id> |
Service app registration (para OBO #2) |
| SERVICE_CLIENT_SECRET | <secret> |
Credencial cliente OBO do serviço |
| DOWNSTREAM_OBO_SCOPE | <resource-id>/.default |
Escopo do recurso downstream para OBO #2 |
| AZURE_OPENAI_ENDPOINT | https://....cognitiveservices.azure.com |
Endpoint LLM |
| AZURE_OPENAI_DEPLOYMENT | gpt-4o |
Nome do deployment do modelo |
M365 Gateway
| Variável | Exemplo | Propósito |
|---|---|---|
| AZURE_TENANT_ID | b5d67878-... |
Entra tenant |
| BOT_APP_ID | <bot-app-id> |
Bot registration |
| BOT_APP_PASSWORD | <secret> |
Client secret do Bot |
| SERVICE_BASE_URL | https://my-service.azurecontainerapps.io |
Endpoint do serviço |
| SERVICE_API_SCOPE | api://<service-app-id>/access_as_user |
Escopo alvo do OBO #1 |
| SERVICE_EXPECTED_AUDIENCE | api://<service-app-id> |
Validação de audience JWT |
Checklist de segurança
flowchart LR
subgraph "Defense in Depth"
V1["Gateway channel validation<br/>(Bot/channel token path)"]
V2["Service bearer validation<br/>(audience + issuer + signature)"]
V3["ContextVar isolation<br/>(per-request, async-safe)"]
V4["Session owner check<br/>(oid-based)"]
V5["Data access control<br/>(scope-limited queries)"]
end
V1 --> V2 --> V3 --> V4 --> V5
- Gateway channel auth enforced — Tráfego de canal é validado antes do forwarding
- Service JWT audience validated — Serviço rejeita tokens com
auderrado - Service JWT issuer validated — Serviço aceita apenas as URIs STS do tenant pretendido
- Service JWT signature verified — RS256 via endpoint JWKS, não chaves fixas
- Service ingress hardened — Preferir ingress interno ou restrito quando prático
- ContextVar reset in finally block — Previne vazamento de assertion entre requisições
- Session owner isolation —
owner_iddo claimoidvalidado, 403 em mismatch - Downstream data access scoped — Usar queries parametrizadas, operações pré-definidas ou row-level security; evitar expor interfaces de query cruas ao agente
- Secrets stored securely — Client secrets como ACA secrets (
secretref:), não env vars em texto puro - Health endpoint unauthenticated —
/healthzignora middleware JWT - Invoke activities handled — Gateway retorna limpo para invokes do handshake SSO
- Admin consent granted — Ambas as cadeias de permissão OBO têm consentimento do admin do tenant
Problemas comuns e troubleshooting
| Sintoma | Causa | Correção |
|---|---|---|
| AADSTS65001: not consented | Consentimento admin ausente para permissões delegadas | Conceder consentimento admin para ambas as cadeias OBO |
| The provided token is not exchangeable | Conexão OAuth do Bot mal configurada ou cache SSO obsoleto | Recriar conexão OAuth; abrir nova conversa no Copilot |
OBO #2 retorna invalid_grant |
Assertion do usuário expirada ou audience mismatch no service app | Garantir requestedAccessTokenVersion: 2 e audience correto |
| Gateway retorna 401 para o Copilot | Auth de canal do gateway ou aquisição de token wrapper falha | Verificar config de auth do Bot/canal e wiring dos auth handlers do gateway |
| Serviço retorna 401/403 para o gateway | Validação de bearer do serviço rejeita o token forwardado | Verificar SERVICE_EXPECTED_AUDIENCE, issuer, tenant e lógica de refresh de chave de assinatura |
| Session 403 no segundo turno | owner_id da sessão não corresponde ao oid do token validado no turno seguinte |
Verificar se o mesmo usuário logado está presente e o serviço deriva ownership do bearer validado, não de headers mutáveis forwardados |
/healthz retorna 401 |
Endpoint de health não excluído do middleware de auth | Adicionar exclusão de caminho para /healthz |
| Atividades invoke mostram erro ao usuário | Handler de invoke ausente no gateway | Adicionar rota no-op para activity.type == "invoke" com is_invoke=True |
Estendendo o padrão
Como adicionar um novo serviço downstream?
Seja o downstream uma REST API, um banco de dados (SQL, Cosmos DB), um servidor MCP ou uma plataforma SaaS — o padrão OBO é idêntico, desde que o recurso seja protegido pelo Entra ID:
- Adicione uma permissão delegada ao service app registration para o novo recurso
- Conceda consentimento admin
- Adicione uma nova função
acquire_X_access_token()usando a mesma assertion da ContextVar com o escopo do novo recurso (ex.:https://graph.microsoft.com/.default) - Chame-a de sua lógica agentic quando o novo downstream for necessário
Cenários comuns:
- REST APIs — Chamar com token
Bearerno headerAuthorization - Bancos de dados (Azure SQL, Databricks, Cosmos DB) — Usar o token OBO como credencial de auth para o cliente de banco ou connection string
- Servidores MCP — Se o servidor MCP estiver atrás do Entra, passar o token OBO; se usar um esquema de auth diferente, trocar ou pontear a credencial conforme necessário
- Microsoft Graph — OBO para
https://graph.microsoft.com/.defaultpara perfil do usuário, e-mail, calendário, arquivos, etc.
Como funciona com um serviço agentic existente?
Se você já tem um serviço agentic funcional que não foi projetado para M365:
- Mantenha o contrato do serviço estável quando possível. Seu contrato de API, modelo de sessão e runtime agentic geralmente podem permanecer como estão. Se ele já não valida o bearer do serviço, adicione isso na fronteira do serviço antes de confiar em acesso delegado downstream.
- Crie o gateway com uma classe service client que mapeia conversas do Copilot para o modelo de sessão do seu serviço, remodela payloads de mensagens e anexa o token OBO na forma que o serviço espera. O gateway também lida com auth de canal e pode forwardar claims do usuário como headers de metadados opcionais.
- Prefira ingress restrito para o serviço para que não fique amplamente exposto. Isso é hardening recomendado, mas não substitui a validação de bearer no lado do serviço.
- Registre os apps no Entra como descrito acima. O scope
access_as_userdo service app é o alvo do OBO #1 do gateway. Se o serviço já tem um registro de app no Entra, adicione o scopeaccess_as_usere pré-autorize o bot app. - Se o serviço chamar APIs downstream e precisar de acesso delegado, faça-o validar o token Bearer forwardado e use essa assertion validada como
user_assertionpara OBO #2. Se o serviço usar API keys ou managed identity para chamadas downstream (não delegadas), OBO #2 não é necessário.
Esta abordagem permite trazer qualquer serviço agentic — independente de framework, linguagem ou hospedagem — para o M365 Copilot escrevendo apenas a camada de gateway.
E se precisar escalar além de uma réplica?
Substitua o session store in-memory por Redis, Cosmos DB ou outro store distribuído. O padrão ContextVar + OBO não é afetado — é por requisição, não por réplica.
Como adicionar um segundo agente voltado para o Copilot?
Implante um segundo par gateway + serviço. Cada um recebe seus próprios registros de app e recurso Bot. O pacote de app M365 pode declarar múltiplos bots ou você pode publicar pacotes separados.
Para um exemplo prático desta arquitetura, veja a implementação de referência: james-tn/dbx-mcp-copilot. O repositório demonstra como o gateway, o serviço agentic e o fluxo de token OBO delegado funcionam juntos em um deployment real voltado para Microsoft 365 Copilot e Teams.
Perguntas Frequentes
-
Preciso reconstruir meu agente para usar declarative agents no M365 Copilot?
Não. A abordagem apresentada permite manter seu serviço agentic intacto. Basta criar um gateway stateless que traduz o protocolo Bot Framework para a API do seu serviço. Você não precisa modificar a lógica de orquestração, o framework ou o gerenciamento de sessão do seu agente original. -
Como funciona a segurança do token do usuário nesse fluxo?
O token do usuário nunca sai da cadeia OBO. O gateway recebe um token de canal, faz a primeira troca OBO por um token scoped para seu serviço, e repassa esse token. Seu serviço valida o bearer recebido e, se precisar chamar downstreams, faz uma segunda troca OBO. Nenhum serviço armazena senhas ou tokens originais. -
Quantas app registrations no Entra ID são necessárias?
Duas: uma para o serviço agentic (que expõe o scopeaccess_as_user) e outra para o gateway/bot (que consome esse scope). Ambas exigem consentimento do admin do tenant. O gateway também precisa de uma OAuth connection configurada no Azure Bot Resource. -
Meu serviço usa um framework diferente (ex.: LangChain, custom). Funciona?
Sim. A arquitetura é framework-agnostic. O gateway precisa apenas que seu serviço suporte três capacidades: identidade de sessão (session ID), troca de mensagens (entrada/saída de texto) e autenticação via bearer token. O gateway adapta a chamada para qualquer API que seu serviço exponha. -
E se meu serviço precisar escalar além de uma réplica?
Substitua o session store in-memory por Redis, Cosmos DB ou outro store distribuído. O padrão ContextVar + OBO não é afetado, pois é por requisição, não por réplica. O gateway escala horizontalmente sem estado.
Artigo originalmente publicado por JamesN em Azure Updates - Latest from Azure Charts.