Cenário: chamar uma API downstream

Use o SDK do Microsoft Entra para a ID do Agente para lidar com a aquisição de token e a comunicação HTTP em uma única operação. O SDK troca seu token de entrada por um token que possui escopos específicos para a API de destino, depois realiza a chamada HTTP e retorna a resposta. Este guia mostra como configurar APIs downstream, implementar chamadas em TypeScript e Python, lidar com diferentes métodos HTTP e gerenciar erros com novas tentativas.

Pré-requisitos

  • Uma conta Azure com uma assinatura ativa. Crie uma conta gratuitamente.
  • Microsoft Entra SDK para AgentID implantado e em execução em seu ambiente. Consulte o Guia de Instalação para obter instruções de instalação.
  • API downstream configurada no SDK com a URL base e os escopos necessários para as APIs que você deseja chamar.
  • Tokens de acesso de clientes autenticados – seu aplicativo recebe tokens de aplicativos de cliente que você encaminha para o SDK.
  • Permissões adequadas no Microsoft Entra ID - sua conta deve ter permissões para registrar aplicativos e conceder permissões de API.

Configuração

Configure a API downstream em seu SDK de Microsoft Entra para configurações de ambiente de ID do agente:

env:
- name: DownstreamApis__Graph__BaseUrl
  value: "https://graph.microsoft.com/v1.0"
- name: DownstreamApis__Graph__Scopes__0
  value: "User.Read"
- name: DownstreamApis__Graph__Scopes__1
  value: "Mail.Read"

A configuração especifica:

  • BaseUrl: o ponto de extremidade raiz da API downstream
  • Escopos: as permissões necessárias para acessar a API downstream

TypeScript/Node.js

Os exemplos a seguir mostram como chamar APIs intermediárias a partir de aplicativos em TypeScript e Node.js. O código demonstra uma função reutilizável e uma integração com Express.js.

interface DownstreamApiResponse {
  statusCode: number;
  headers: Record<string, string>;
  content: string;
}

async function callDownstreamApi(
  incomingToken: string,
  serviceName: string,
  relativePath: string,
  method: string = 'GET',
  body?: any
): Promise<any> {
  const sdkUrl = process.env.ENTRA_SDK_URL || 'http://localhost:5000';
  
  const url = new URL(`${sdkUrl}/DownstreamApi/${serviceName}`);
  url.searchParams.append('optionsOverride.RelativePath', relativePath);
  if (method !== 'GET') {
    url.searchParams.append('optionsOverride.HttpMethod', method);
  }
  
  const requestOptions: any = {
    method: method,
    headers: {
      'Authorization': incomingToken
    }
  };
  
  if (body) {
    requestOptions.headers['Content-Type'] = 'application/json';
    requestOptions.body = JSON.stringify(body);
  }
  
  const response = await fetch(url.toString(), requestOptions);
  
  if (!response.ok) {
    throw new Error(`SDK error: ${response.statusText}`);
  }
  
  const data = await response.json() as DownstreamApiResponse;
  
  if (data.statusCode >= 400) {
    throw new Error(`API error ${data.statusCode}: ${data.content}`);
  }
  
  return JSON.parse(data.content);
}

// Usage examples
async function getUserProfile(incomingToken: string) {
  return await callDownstreamApi(incomingToken, 'Graph', 'me');
}

async function listEmails(incomingToken: string) {
  return await callDownstreamApi(
    incomingToken,
    'Graph',
    'me/messages?$top=10&$select=subject,from,receivedDateTime'
  );
}

async function sendEmail(incomingToken: string, message: any) {
  return await callDownstreamApi(
    incomingToken,
    'Graph',
    'me/sendMail',
    'POST',
    { message }
  );
}

O exemplo a seguir demonstra como integrar essas funções a um aplicativo Express.js usando manipuladores de middleware e de rota:

// Express.js API example
import express from 'express';

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

app.get('/api/profile', async (req, res) => {
  try {
    const incomingToken = req.headers.authorization;
    if (!incomingToken) {
      return res.status(401).json({ error: 'No authorization token' });
    }
    
    const profile = await getUserProfile(incomingToken);
    res.json(profile);
  } catch (error) {
    console.error('Error:', error);
    res.status(500).json({ error: 'Failed to fetch profile' });
  }
});

app.get('/api/messages', async (req, res) => {
  try {
    const incomingToken = req.headers.authorization;
    if (!incomingToken) {
      return res.status(401).json({ error: 'No authorization token' });
    }
    
    const messages = await listEmails(incomingToken);
    res.json(messages);
  } catch (error) {
    console.error('Error:', error);
    res.status(500).json({ error: 'Failed to fetch messages' });
  }
});

app.post('/api/messages/send', async (req, res) => {
  try {
    const incomingToken = req.headers.authorization;
    if (!incomingToken) {
      return res.status(401).json({ error: 'No authorization token' });
    }
    
    const message = req.body;
    await sendEmail(incomingToken, message);
    res.json({ success: true });
  } catch (error) {
    console.error('Error:', error);
    res.status(500).json({ error: 'Failed to send message' });
  }
});

app.listen(8080, () => {
  console.log('Server running on port 8080');
});

Python

Os exemplos a seguir mostram como chamar APIs downstream de aplicativos Python usando a biblioteca de solicitações e o Flask para tratamento de HTTP:

import os
import json
import requests
from typing import Dict, Any, Optional

def call_downstream_api(
    incoming_token: str,
    service_name: str,
    relative_path: str,
    method: str = 'GET',
    body: Optional[Dict[str, Any]] = None
) -> Any:
    """Call a downstream API via the Microsoft Entra SDK for AgentID."""
    sdk_url = os.getenv('ENTRA_SDK_URL', 'http://localhost:5000')
    
    params = {
        'optionsOverride.RelativePath': relative_path
    }
    
    if method != 'GET':
        params['optionsOverride.HttpMethod'] = method
    
    headers = {'Authorization': incoming_token}
    json_body = None
    
    if body:
        headers['Content-Type'] = 'application/json'
        json_body = body
    
    response = requests.request(
        method,
        f"{sdk_url}/DownstreamApi/{service_name}",
        params=params,
        headers=headers,
        json=json_body
    )
    
    if not response.ok:
        raise Exception(f"SDK error: {response.text}")
    
    data = response.json()
    
    if data['statusCode'] >= 400:
        raise Exception(f"API error {data['statusCode']}: {data['content']}")
    
    return json.loads(data['content'])

# Usage examples
def get_user_profile(incoming_token: str) -> Dict[str, Any]:
    return call_downstream_api(incoming_token, 'Graph', 'me')

def list_emails(incoming_token: str) -> Dict[str, Any]:
    return call_downstream_api(
        incoming_token,
        'Graph',
        'me/messages?$top=10&$select=subject,from,receivedDateTime'
    )

def send_email(incoming_token: str, message: Dict[str, Any]) -> None:
    call_downstream_api(
        incoming_token,
        'Graph',
        'me/sendMail',
        'POST',
        {'message': message}
    )

Se você quiser integrar essas funções a um aplicativo Flask, poderá usar o seguinte exemplo:

# Flask API example
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/api/profile')
def profile():
    incoming_token = request.headers.get('Authorization')
    if not incoming_token:
        return jsonify({'error': 'No authorization token'}), 401
    
    try:
        profile_data = get_user_profile(incoming_token)
        return jsonify(profile_data)
    except Exception as e:
        print(f"Error: {e}")
        return jsonify({'error': 'Failed to fetch profile'}), 500

@app.route('/api/messages')
def messages():
    incoming_token = request.headers.get('Authorization')
    if not incoming_token:
        return jsonify({'error': 'No authorization token'}), 401
    
    try:
        messages_data = list_emails(incoming_token)
        return jsonify(messages_data)
    except Exception as e:
        print(f"Error: {e}")
        return jsonify({'error': 'Failed to fetch messages'}), 500

@app.route('/api/messages/send', methods=['POST'])
def send_message():
    incoming_token = request.headers.get('Authorization')
    if not incoming_token:
        return jsonify({'error': 'No authorization token'}), 401
    
    try:
        message = request.json
        send_email(incoming_token, message)
        return jsonify({'success': True})
    except Exception as e:
        print(f"Error: {e}")
        return jsonify({'error': 'Failed to send message'}), 500

if __name__ == '__main__':
    app.run(port=8080)

Solicitações de POST/PUT/PATCH

O /DownstreamApi endpoint suporta operações de modificação ao passar o método HTTP e o corpo da solicitação. Use esses padrões quando precisar criar, atualizar ou excluir recursos na API downstream.

Criando recursos

// POST example - Create a calendar event
async function createEvent(incomingToken: string, event: any) {
  return await callDownstreamApi(
    incomingToken,
    'Graph',
    'me/events',
    'POST',
    event
  );
}

// Usage
const newEvent = {
  subject: "Team Meeting",
  start: {
    dateTime: "2024-01-15T14:00:00",
    timeZone: "Pacific Standard Time"
  },
  end: {
    dateTime: "2024-01-15T15:00:00",
    timeZone: "Pacific Standard Time"
  }
};

const createdEvent = await createEvent(incomingToken, newEvent);

Atualização de recursos

// PATCH example - Update user profile
async function updateProfile(incomingToken: string, updates: any) {
  return await callDownstreamApi(
    incomingToken,
    'Graph',
    'me',
    'PATCH',
    updates
  );
}

// Usage
await updateProfile(incomingToken, {
  mobilePhone: "+1 555 0100",
  officeLocation: "Building 2, Room 201"
});

Cenários avançados

Os cenários a seguir demonstram configurações avançadas para casos de uso especializados.

Cabeçalhos personalizados

Adicione cabeçalhos personalizados à solicitação de API downstream:

const url = new URL(`${sdkUrl}/DownstreamApi/MyApi`);
url.searchParams.append('optionsOverride.RelativePath', 'items');
url.searchParams.append('optionsOverride.CustomHeader.X-Custom-Header', 'custom-value');
url.searchParams.append('optionsOverride.CustomHeader.X-Request-Id', requestId);

Substituir escopos

Solicite escopos diferentes dos escopos configurados padrão:

const url = new URL(`${sdkUrl}/DownstreamApi/Graph`);
url.searchParams.append('optionsOverride.RelativePath', 'me');
url.searchParams.append('optionsOverride.Scopes', 'User.ReadWrite');
url.searchParams.append('optionsOverride.Scopes', 'Mail.Send');

Com a identidade do agente

Use a identidade do agente para chamar APIs com permissões de aplicativo:

const url = new URL(`${sdkUrl}/DownstreamApi/Graph`);
url.searchParams.append('optionsOverride.RelativePath', 'users');
url.searchParams.append('AgentIdentity', agentClientId);
url.searchParams.append('AgentUsername', 'admin@contoso.com');

Tratamento de erros

Implemente a lógica de repetição de tentativas com recuo exponencial para lidar com falhas transitórias de forma graciosa.

async function callDownstreamApiWithRetry(
  incomingToken: string,
  serviceName: string,
  relativePath: string,
  method: string = 'GET',
  body?: any,
  maxRetries: number = 3
): Promise<any> {
  let lastError: Error;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await callDownstreamApi(
        incomingToken,
        serviceName,
        relativePath,
        method,
        body
      );
    } catch (error) {
      lastError = error as Error;
      
      // Don't retry on client errors (4xx)
      if (error.message.includes('API error 4')) {
        throw error;
      }
      
      // Retry on server errors (5xx) or network errors
      if (attempt < maxRetries) {
        const delay = Math.pow(2, attempt) * 100;
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }
  
  throw new Error(`Failed after ${maxRetries} retries: ${lastError!.message}`);
}

Comparação com a abordagem AuthorizationHeader

O SDK do Microsoft Entra para ID do Agente fornece duas abordagens para chamar APIs downstream. Use essa comparação para determinar qual abordagem melhor atende às suas necessidades.

Comparação de funcionalidades

Capability /DownstreamApi /AuthorizationHeader
Aquisição de token Manipulado pelo SDK Manipulado pelo SDK
Solicitação HTTP Manipulado pelo SDK Sua responsabilidade
Análise de resposta Encapsulado em JSON Resposta HTTP direta
Cabeçalhos personalizados Por meio de parâmetros de consulta Controle HTTP completo
Corpo da solicitação Encaminhado automaticamente Controle total
Tratamento de erros Erros encapsulados pelo SDK Erros HTTP padrão

Quando usar cada abordagem

Caso de Uso Recomendação Mais Adequado Para
Chamadas à API REST padrão com padrões convencionais /DownstreamApi Operações GET, POST, PUT, PATCH, DELETE; reduzindo código padrão
Clientes HTTP complexos que exigem configuração personalizada /AuthorizationHeader Tratamento especializado de solicitação/resposta; controle refinado
Necessidade de acesso direto a códigos de erro HTTP e cabeçalhos /AuthorizationHeader Aplicativos que precisam de controle de comportamento HTTP de baixo nível
Simplicidade e integração rápida priorizadas /DownstreamApi Aplicativos priorizando a simplicidade em relação ao controle de baixo nível

Práticas recomendadas

  1. Reutilizar clientes HTTP: criar uma vez e reutilizar para evitar sobrecarga de conexão
  2. Implementar tratamento de erros: adicionar lógica de tentativa para falhas transitórias com recuo exponencial
  3. Verificar códigos de status: sempre verifique o código de status antes de analisar o conteúdo da resposta
  4. Definir tempos limite: configurar tempos limite de solicitação apropriados para evitar solicitações suspensas
  5. Incluir IDs de correlação: registrar todas as solicitações com identificadores exclusivos para rastreamento de ponta a ponta
  6. Validar entrada: sanitizar e validar dados antes de enviar para APIs downstream
  7. Monitorar o desempenho: acompanhar a latência de chamadas à API e as taxas de falha para observabilidade