OAuth Identity Passthrough no Foundry Agent Service com MCP Server customizado em .NET - Parte 1

OAuth Identity Passthrough no Foundry Agent Service com MCP Server customizado em .NET - Parte 1

2 de julho de 2026

O OAuth Identity Passthrough permite que o Foundry Agent Service chame um MCP Server em nome do usuário logado, preservando a identidade e os scopes que aquele usuário consentiu. A diferença em relação a uma integração comum é conceitualmente simples: o agente não chama o MCP Server usando uma API Key ou uma identidade genérica de aplicação. Ele chama o MCP Server usando um access token delegado do próprio usuário, emitido pelo Microsoft Entra ID. Isso significa que cada tool do MCP roda com as permissões reais da pessoa que está conversando com o agente, e não com um crachá compartilhado.

O fluxo de identidade que vamos montar percorre a seguinte cadeia, começando no usuário autenticado, passando pelo Foundry Agent Service, depois pelo App Registration client que representa o Foundry como OAuth client, seguindo para o Microsoft Entra ID que emite os tokens e terminando no MCP Server protegido, que aqui chamaremos de custom-mcp-cars. Essa cadeia é o coração do artigo, então vale ter ela em mente enquanto avançamos.

Antes de abrir o portal, é importante entender que o cenário usa dois App Registrations distintos, e confundir os dois é a causa mais comum de erro nesse tipo de integração. O primeiro deles é o custom-mcp-cars, que representa o MCP Server como uma API protegida no Microsoft Entra ID. Ele é usado pela aplicação .NET Web API publicada no Azure App Service e é responsável por expor a API, definir o Application ID URI, declarar os scopes e determinar o audience esperado nos tokens que o MCP Server recebe. O segundo é o custom-mcp-cars-client, que representa o OAuth client usado pelo Foundry. Ele é usado pelo Foundry Agent Service e concentra o Client ID, o Client Secret, o Redirect URI, as API permissions e a solicitação de tokens em nome do usuário. Resumindo em uma linha, o custom-mcp-cars é o server e o custom-mcp-cars-client é o client. A diferença entre App Registration, application object e service principal está bem detalhada na documentação sobre apps and service principals, caso você queira aprofundar

Começamos criando o App Registration do server, que vamos chamar de custom-mcp-cars. Esse registro representa o MCP Server como recurso protegido no Microsoft Entra ID, e o passo a passo oficial para criar um App Registration está no quickstart de registro de aplicações.

Com o registro criado, o próximo passo é ir até a seção Expose an API do custom-mcp-cars e configurar o Application ID URI no formato api://<server-app-client-id>. Esse valor é fundamental, porque será exatamente o audience esperado nos tokens recebidos pelo MCP Server. O guia oficial para expor uma Web API e criar scopes cobre esse ponto em detalhe.


Nessa mesma tela vamos criar apenas três scopes próprios da API, que são Cars.Read, Cars.Write e Cars.Delete. O Cars.Read permite consultar carros, o Cars.Write permite criar e atualizar carros e o Cars.Delete permite excluir carros. Ao criar cada scope, deixamos o campo de consentimento como Admins and users, para que o usuário comum também consiga consentir sem depender de um administrador.

Depois de criar os três, a tela de scopes deve listar exatamente Cars.Read, Cars.Write e Cars.Delete. Vale reforçar aqui um detalhe que costuma confundir: a API custom-mcp-cars possui somente esses três scopes próprios, e o offline_access que aparecerá mais adiante não é um scope da API, e sim um scope OIDC padrão usado para habilitar refresh token.

Com a API exposta e os scopes definidos no portal, faz sentido olhar para o outro lado dessa relação, que é o código do MCP Server que vai validar esses tokens. A aplicação .NET já está pronta no repositório, então a ideia não é criar nada do zero, e sim entender como esse código existente funciona como um MCP Server HTTP protegido pelo Microsoft Entra ID. Ela expõe um endpoint /mcp, valida tokens emitidos para o custom-mcp-cars, confere o audience, trabalha com os scopes Cars.Read, Cars.Write e Cars.Delete e aplica autorização específica em cada tool. As tools formam um CRUD de carros e são cars_list, cars_get, cars_create, cars_update e cars_delete, mapeadas respectivamente para Cars.Read nas leituras, Cars.Write nas escritas e Cars.Delete na exclusão.

O ponto de partida da aplicação é o Program.cs, onde a autenticação, a autorização e o próprio MCP Server são configurados.

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization(options => options.AddCarsPolicies());

builder.Services.AddSingleton<CarStore>();

builder.Services
    .AddMcpServer()
    .WithHttpTransport(options =>
    {
        options.Stateless = true;
    })
    .AddAuthorizationFilters()
    .WithToolsFromAssembly();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/healthz", () => Results.Ok(new
{
    status = "healthy",
    service = "cars-mcp"
}))
.AllowAnonymous();

app.MapMcp("/mcp")
   .RequireAuthorization("McpAccess");

app.Run();
  • AddMicrosoftIdentityWebApi() protege a Web API validando os tokens do Microsoft Entra ID com base na seção AzureAd da configuração, conforme a referência oficial do método;
  • AddCarsPolicies() registra as policies de autorização baseadas em scope que controlam o acesso ao MCP e a cada tool;
  • AddMcpServer() com WithHttpTransport() configura o MCP Server usando o transport HTTP do SDK C# do MCP;
  • AddAuthorizationFilters() liga os filtros de autorização do MCP para que os atributos [Authorize] das tools sejam respeitados, conforme documentado nos filters do SDK;
  • WithToolsFromAssembly() descobre automaticamente todas as tools anotadas no assembly;
  • MapMcp("/mcp") publica o endpoint do MCP Server exigindo a policy McpAccess, enquanto o /healthz permanece anônimo para health checks.

A configuração da seção AzureAd usada por esse código vive no appsettings.json e é o que amarra a aplicação ao App Registration server.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "<Your_Tenant_Id>",
    "ClientId": "<Your_Client_Id>",
    "Audience": "<Your_Audience>"
  },
  "AllowedHosts": "*"
}

Aqui vale destacar que o TenantId, o ClientId e o Audience se referem ao custom-mcp-cars, e não ao custom-mcp-cars-client. O MCP Server não valida tokens pelo Client ID do Foundry: ele valida tokens emitidos para o App Registration server, com audience igual ao Application ID URI do custom-mcp-cars. Essa é a essência da configuração de uma Web API protegida em ASP.NET Core.

As policies mencionadas no Program.cs ficam concentradas em um método de extensão, e é nele que a validação de scope realmente acontece.

public static void AddCarsPolicies(this AuthorizationOptions options)
{
    options.AddPolicy("McpAccess", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireAssertion(context =>
            context.User.HasAnyScope(
                CarsScopes.Read,
                CarsScopes.Write,
                CarsScopes.Delete));
    });

    options.AddPolicy("CarsRead", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireAssertion(context =>
            context.User.HasScope(CarsScopes.Read));
    });

    options.AddPolicy("CarsWrite", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireAssertion(context =>
            context.User.HasScope(CarsScopes.Write));
    });

    options.AddPolicy("CarsDelete", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireAssertion(context =>
            context.User.HasScope(CarsScopes.Delete));
    });
  • McpAccess exige um usuário autenticado que tenha ao menos um dos três scopes, servindo como porta de entrada geral do endpoint /mcp.
  • CarsRead exige especificamente o scope Cars.Read para operações de leitura.
  • CarsWrite exige o scope Cars.Write para criação e atualização.
  • CarsDelete exige o scope Cars.Delete para exclusão.

Esse desenho segue a authorization baseada em policies do ASP.NET Core e a recomendação de validar scopes em uma API protegida. A leitura do scope em si é feita por um par de extensões sobre o ClaimsPrincipal, que inspecionam a claim scp presente no token.

public static bool HasScope(this ClaimsPrincipal user, string requiredScope)
{
    var scopeClaim =
        user.FindFirst("scp")?.Value ??
        user.FindFirst("http://schemas.microsoft.com/identity/claims/scope")?.Value;

    if (string.IsNullOrWhiteSpace(scopeClaim))
        return false;

    var scopes = scopeClaim.Split(' ', StringSplitOptions.RemoveEmptyEntries);

    return scopes.Contains(requiredScope, StringComparer.OrdinalIgnoreCase);
}

public static bool HasAnyScope(this ClaimsPrincipal user, params string[] requiredScopes)
{
    return requiredScopes.Any(user.HasScope);
}
  • HasScope() lê a claim scp do token, separa os scopes por espaço e verifica se o scope exigido está presente, ignorando diferença de maiúsculas e minúsculas.
  • HasAnyScope() retorna verdadeiro se qualquer um dos scopes informados estiver presente, e é usado pela policy McpAccess.

Com as policies prontas, cada tool declara qual delas precisa. A tool de listagem é o exemplo mais direto, porque é somente leitura e depende apenas do Cars.Read.

[McpServerTool(Name = "cars_list", ReadOnly = true, Destructive = false)]
[Authorize(Policy = "CarsRead")]
[Description("Lists all cars. Requires the Cars.Read scope.")]
public static IReadOnlyCollection<Car> ListCars(CarStore store)
{
    return store.List();
}

ListCars() retorna todos os carros do CarStore e só executa se o token do usuário contiver o scope Cars.Read, graças ao atributo [Authorize(Policy = "CarsRead")]. O atributo [McpServerTool] marca o método como uma tool do MCP e traz metadados como ReadOnly e Destructive, descritos na referência do McpServerTool.

A tool de criação eleva o requisito para o scope Cars.Write e ainda valida os dados de entrada antes de gravar.

[McpServerTool(Name = "cars_create", ReadOnly = false, Destructive = false)]
[Authorize(Policy = "CarsWrite")]
[Description("Creates a new car. Requires the Cars.Write scope.")]
public static object CreateCar(
    CarStore store,
    [Description("The car brand. Example: Toyota.")] string brand,
    [Description("The car model. Example: Corolla.")] string model,
    [Description("The manufacturing year.")] int year)
{
    var validation = ValidateCarInput(brand, model, year);

    if (validation is not null)
        return validation;

    var car = store.Create(brand, model, year);

    return new
    {
        created = true,
        car
    };
}

CreateCar() valida marca, modelo e ano, cria o carro no CarStore e devolve o registro criado, exigindo o scope Cars.Write por meio da policy CarsWrite.

Por fim, a tool de exclusão é a única marcada como Destructive e exige o scope mais restrito, o Cars.Delete.

[McpServerTool(Name = "cars_delete", ReadOnly = false, Destructive = true)]
[Authorize(Policy = "CarsDelete")]
[Description("Deletes a car. Requires the Cars.Delete scope.")]
public static object DeleteCar(
    CarStore store,
    [Description("The car id.")] Guid id)
{
    var deleted = store.Delete(id);

    return new
    {
        deleted
    };
}

DeleteCar() remove o carro pelo identificador e retorna se a exclusão ocorreu, exigindo o scope Cars.Delete por meio da policy CarsDelete.

Os dados ficam em um CarStore em memória que já vem populado com dez carros de exemplo, o que é suficiente para demonstrar o fluxo sem precisar de banco de dados.


Entendido o código, precisamos de um lugar público para hospedá-lo, já que o Foundry acessa o MCP Server por um endpoint HTTP remoto. Por isso criamos um Azure App Service que vai receber a aplicação .NET, e a configuração específica de runtime para ASP.NET Core está descrita na documentação de configuração de apps .NET no App Service. O endpoint final esperado do MCP será algo como https://<app-service-name>.azurewebsites.net/mcp.

Com o App Service criado, o deploy da aplicação é feito por um script que já existe no repositório, o deploy.sh. Ele publica o projeto em modo Release, empacota o resultado em um zip e envia para o App Service já existente, sem tocar em nenhuma configuração de ambiente.

#!/usr/bin/env bash
set -euo pipefail

# ─── Configuration ────────────────────────────────────────────────────────────
RESOURCE_GROUP_NAME="rg-cars-mcp"
APP_SERVICE_NAME="cars-mcp-app"
PROJECT_PATH="."
PUBLISH_DIR="./publish"
ZIP_PATH="./publish.zip"
DOTNET_CONFIGURATION="Release"
# ──────────────────────────────────────────────────────────────────────────────

echo "==> Checking Azure CLI login..."
az account show >/dev/null 2>&1 || az login

echo "==> Cleaning previous publish artifacts..."
rm -rf "$PUBLISH_DIR" "$ZIP_PATH"

echo "==> Publishing Cars.Mcp ($DOTNET_CONFIGURATION)..."
dotnet publish "$PROJECT_PATH" \
  --configuration "$DOTNET_CONFIGURATION" \
  --output "$PUBLISH_DIR"

echo "==> Creating deployment zip..."
(cd "$PUBLISH_DIR" && zip -r -q "../$(basename "$ZIP_PATH")" .)

echo "==> Deploying to App Service: $APP_SERVICE_NAME"
az webapp deploy \
  --resource-group "$RESOURCE_GROUP_NAME" \
  --name "$APP_SERVICE_NAME" \
  --src-path "$ZIP_PATH" \
  --type zip

O script primeiro garante o login no Azure, executando az login apenas se ainda não houver sessão ativa. Em seguida limpa artefatos de publicações anteriores, roda o dotnet publish em Release, gera o pacote zip e finalmente usa az webapp deploy para enviar o pacote ao App Service. Para rodar o deploy, basta abrir o terminal na pasta do projeto e executar o script.

./deploy.sh

Depois da publicação, dá para validar que a aplicação subiu acessando o endpoint https://<app-service-name>.azurewebsites.net/healthz, que responde de forma anônima, enquanto o endpoint protegido do MCP fica em https://<app-service-name>.azurewebsites.net/mcp.

Antes de conectar o Foundry, a aplicação publicada precisa saber a qual App Registration ela pertence, e isso é feito por variáveis de ambiente no Azure App Service, conforme a documentação de configuração de app settings. As quatro variáveis usadas são AzureAd__Audience, AzureAd__ClientId, AzureAd__Instance e AzureAd__TenantId, que correspondem exatamente à seção AzureAd do appsettings.json.

O AzureAd__Audience recebe o Application ID URI do custom-mcp-cars, no formato api://<server-app-client-id>. O AzureAd__ClientId recebe o Client ID do custom-mcp-cars. O AzureAd__Instance recebe https://login.microsoftonline.com/ e o AzureAd__TenantId recebe o Tenant ID onde o App Registration foi criado.

O ponto crítico aqui é o mesmo de antes: todos esses valores pertencem ao App Registration server custom-mcp-cars, e não ao custom-mcp-cars-client.

Com o server pronto e publicado, entramos na segunda metade da configuração, que é o lado do client. Criamos então o segundo App Registration, o custom-mcp-cars-client, que será usado pelo Foundry como OAuth client responsável por solicitar tokens para acessar o MCP Server em nome do usuário.

Dentro do custom-mcp-cars-client, vamos até API permissions, escolhemos Add a permission, depois My APIs e selecionamos o custom-mcp-cars. Esse é o passo que conecta o client à API que criamos, e o procedimento está documentado no guia de configuração de acesso a Web APIs.

Na sequência, marcamos os três scopes que a API expõe, que são Cars.Read, Cars.Write e Cars.Delete. Essas são permissões delegadas, ou seja, permitem que o custom-mcp-cars-client solicite tokens para acessar o custom-mcp-cars em nome do usuário, e o conceito de delegated permissions e consentimento está detalhado na visão geral de permissions and consent.

Depois de adicionar, a lista de permissões mostra os três scopes delegados, ainda sem admin consent, o que é esperado neste cenário, já que o próprio usuário poderá consentir no momento da primeira chamada.

Como o Foundry precisa se autenticar como esse client, criamos também um Client Secret no custom-mcp-cars-client. O Foundry usará o Client ID em conjunto com esse Client Secret para executar o fluxo OAuth, e as boas práticas de criação e gestão de credenciais estão na documentação de application credentials.

Agora que temos server publicado e client configurado, partimos para o Foundry Agent Service, partindo do princípio de que o Foundry Project, o agent e o model deployment já existem e que o MCP Server já está publicado. Adicionamos uma tool MCP no Foundry apontando para o endpoint remoto e escolhendo OAuth Identity Passthrough como autenticação, seguindo o guia de conexão de MCP tools remotas a agentes.

O nome da tool é custom-mcp-cars e o endpoint é https://<app-service-name>.azurewebsites.net/mcp. O Client ID e o Client Secret são os do custom-mcp-cars-client. A Auth URL fica em https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize, enquanto a Token URL e a Refresh URL apontam ambas para https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token, que são os endpoints padrão do protocolo OAuth 2.0 no Microsoft Entra ID.

Os scopes configurados no Foundry usam o formato completo com o Application ID URI, ficando api://<server-app-client-id>/Cars.Read, api://<server-app-client-id>/Cars.Write, api://<server-app-client-id>/Cars.Delete e, por último, offline_access. Os três primeiros são os scopes próprios da API custom-mcp-cars e o offline_access habilita o refresh token no fluxo OAuth. Um detalhe prático que evita dor de cabeça: os scopes devem ser separados por espaço, e não por vírgula.

Ao salvar essa configuração, o Foundry gera um Redirect URI próprio, normalmente no formato https://global.consent.azure-apim.net/redirect/<id>. Esse Redirect URI é o endereço para onde o fluxo OAuth vai devolver o authorization code que o Foundry troca por tokens, e ele precisa ser cadastrado no App Registration. Voltamos então ao custom-mcp-cars-client, entramos em Authentication, escolhemos Add platform, depois Web e adicionamos essa URL como Redirect URI, seguindo o procedimento oficial de adição de redirect URI e respeitando as boas práticas de reply URLs. Vale reforçar que o Redirect URI é cadastrado no custom-mcp-cars-client, e nunca no custom-mcp-cars.

Com tudo conectado, é hora de testar no Agent Playground do Foundry. Pedimos algo simples que dependa do MCP, como listar todos os carros, e na primeira vez o Foundry responde pedindo consentimento, exibindo um botão de consent no próprio chat. Esse botão aparece porque o usuário ainda não autorizou o custom-mcp-cars-client a acessar o custom-mcp-cars em seu nome.

Ao clicar em Open consent, abre-se o fluxo de consentimento OAuth no navegador. Nesse momento o usuário está autorizando o client usado pelo Foundry a acessar o MCP Server em seu nome, e a experiência de consentimento no Microsoft Entra ID está descrita na documentação de consent experience.

A tela seguinte mostra os scopes solicitados e, dependendo da configuração do tenant, o consentimento pode ser individual ou para toda a organização, um tema aprofundado no guia de requesting permissions and consent.

Depois vem uma confirmação final exigida pelo Foundry. Assim que ela é aceita, o Microsoft Entra ID devolve o authorization code e o Foundry, atuando como Credential Manager, troca esse code por access token e refresh token.

Concluído esse passo, a tela indica que a autenticação foi finalizada, e a partir daí o Foundry consegue obter access tokens para chamar o MCP Server em nome do usuário sempre que precisar.

Voltando ao chat do Foundry, o consentimento já aparece concedido e o agente pode prosseguir com a solicitação original.

Voltando ao chat do Foundry, o consentimento já aparece concedido e o agente pode prosseguir com a solicitação original. Na sequência, o agente executa a chamada ao MCP Server e traz de volta os dados dos carros, mostrando que o fluxo delegado funcionou de ponta a ponta.

Para exercitar uma operação de escrita, pedimos ao agente que crie um novo carro, por exemplo um Fiat Argo Drive 1.0 (hatch) 2021. Essa operação exige o scope Cars.Write, e o Foundry chama o MCP usando o token delegado do usuário, que por sua vez tem seu scope validado pela policy CarsWrite antes de executar a tool cars_create.

O agente coleta os dados informados e monta a chamada correspondente à tool de criação.

Em seguida, o MCP Server responde com o resultado da criação, confirmando que o carro foi persistido no CarStore.

Por fim, pedimos novamente a listagem de todos os carros, agora já com o novo registro incluído. Como o usuário já consentiu anteriormente, o Foundry não precisa pedir consentimento de novo, e a operação, que exige apenas o Cars.Read, é atendida diretamente.

Vale entender o que o MCP Server efetivamente recebe nessas chamadas. O access token entregue à API carrega claims importantes, sendo o aud igual a api://<server-app-client-id>, o scp contendo os scopes delegados concedidos como Cars.Read, Cars.Write e Cars.Delete, o tid identificando o tenant e o oid identificando o usuário. O aud indica para qual API o token foi emitido, o scp indica os scopes delegados presentes, o tid identifica o tenant e o oid identifica a pessoa. O MCP Server valida o audience e os scopes, e se o token não contiver o scope exigido pela tool, a chamada simplesmente não é autorizada.

Olhando o cenário como um todo, cada componente tem um papel bem definido. O Microsoft Entra ID autentica o usuário e emite os tokens. O Foundry Agent Service, atuando também como Credential Manager, executa o fluxo OAuth e gerencia consentimento, access token e refresh token. O custom-mcp-cars-client representa o app usado pelo Foundry para pedir os tokens. O custom-mcp-cars representa o MCP Server como API protegida. O Azure App Service hospeda a aplicação .NET, o MCP Server valida token, audience e scopes, e o código .NET autoriza cada tool conforme o scope necessário.

Chegamos assim ao fim da Parte 1, na qual criamos o Server App Registration custom-mcp-cars, expusemos a API, criamos os três scopes Cars.Read, Cars.Write e Cars.Delete, apresentamos o código existente do MCP Server, criamos o Azure App Service, fizemos o deploy via deploy.sh usando az login, configuramos as variáveis AzureAd__ no App Service, criamos o Client App Registration custom-mcp-cars-client, configuramos as API permissions delegadas, geramos o client secret, configuramos o MCP no Foundry, registramos o Redirect URI no client, passamos pelo consentimento OAuth e executamos chamadas reais ao MCP pelo Foundry Agent Service. Na Parte 2, vamos sair do portal e criar aplicações que consomem esse mesmo fluxo de forma programática, integrando o Foundry Agent Service ao MCP Server com OAuth Identity Passthrough.

Você já pode baixar o projeto por esse link, e não esquece de me seguir no LinkedIn!

Até a próxima, abraços!

Confira mais:

Fique por dentro das novidades

Assine nossa newsletter e receba as últimas atualizações e artigos diretamente em seu email.

Assinar gratuitamente