Scénario : Appeler une API en aval

Utilisez le sdk Microsoft Entra pour l’ID d’agent pour gérer à la fois l’acquisition de jetons et la communication HTTP en une seule opération. Le Kit de développement logiciel (SDK) échange votre jeton entrant contre un jeton personnalisé pour l'API en aval, effectue ensuite l'appel HTTP et retourne la réponse. Ce guide vous montre comment configurer des API en aval, implémenter des appels dans TypeScript et Python, gérer différentes méthodes HTTP et gérer des erreurs avec des nouvelles tentatives.

Prerequisites

  • Un compte Azure avec un abonnement actif. Créez un compte gratuitement.
  • Microsoft Entra SDK pour AgentID déployé et en cours d’exécution dans votre environnement. Consultez le Guide d’installation pour obtenir des instructions d’installation.
  • API en aval configurée dans le Kit de développement logiciel (SDK) avec l’URL de base et les étendues requises pour les API que vous souhaitez appeler.
  • Jetons de porteur provenant de clients authentifiés - votre application reçoit des jetons des applications clientes que vous allez transférer au Kit de développement logiciel (SDK).
  • Autorisations d’identification dans Microsoft Entra ID : votre compte doit disposer des autorisations nécessaires pour inscrire des applications et accorder des autorisations d’API.

Paramétrage

Configurez l'API downstream dans vos paramètres d'environnement SDK Microsoft Entra pour ID d'agent :

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"

La configuration spécifie :

  • BaseUrl : point de terminaison racine de votre API en aval
  • Étendues : autorisations requises pour accéder à l’API en aval

TypeScript/Node.js

Les exemples suivants montrent comment appeler des API en aval à partir de TypeScript et d’applications Node.js. Le code illustre à la fois une fonction réutilisable et une intégration avec 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 }
  );
}

L’exemple suivant montre comment intégrer ces fonctions dans une application Express.js à l’aide d’intergiciels et de gestionnaires de routage :

// 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

Les exemples suivants montrent comment appeler des API en aval à partir de Python applications à l’aide de la bibliothèque de requêtes et flask pour la gestion 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}
    )

Si vous souhaitez intégrer ces fonctions dans une application Flask, vous pouvez utiliser l’exemple suivant :

# 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)

Requêtes POST/PUT/PATCH

Le /DownstreamApi point de terminaison prend en charge les opérations de modification en passant la méthode HTTP et le corps de la requête. Utilisez ces modèles lorsque vous devez créer, mettre à jour ou supprimer des ressources dans l’API en aval.

Création de ressources

// 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);

Mise à jour des ressources

// 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"
});

Scénarios avancés

Les scénarios suivants illustrent des configurations avancées pour les cas d’usage spécialisés.

En-têtes personnalisés

Ajoutez des en-têtes personnalisés à la demande d’API en aval :

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);

Remplacer les périmètres

Demandez des étendues différentes des étendues configurées par défaut :

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');

Avec l’identité de l’agent

Utilisez l’identité de l’agent pour appeler des API avec des autorisations d’application :

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');

Gestion des erreurs

Implémentez une logique de nouvelle tentative avec interruption exponentielle pour gérer correctement les défaillances temporaires :

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}`);
}

Comparaison avec l’approche AuthorizationHeader

Le sdk Microsoft Entra pour l’ID d’agent fournit deux approches pour appeler des API en aval. Utilisez cette comparaison pour déterminer l’approche la mieux adaptée à vos besoins.

Comparaison des fonctionnalités

Capacité /DownstreamApi /AuthorizationHeader
Acquisition de jetons Géré par le Kit de développement logiciel (SDK) Géré par le Kit de développement logiciel (SDK)
Demande HTTP Géré par le Kit de développement logiciel (SDK) Votre responsabilité
Analyse de réponse Encapsulé dans JSON Réponse HTTP directe
En-têtes personnalisés Via les paramètres de requête Contrôle HTTP complet
Corps de la requête Transféré automatiquement Contrôle total
Gestion des erreurs Erreurs enveloppées (ou encapsulées) du Kit de développement logiciel (SDK) Erreurs HTTP standard

Quand utiliser chaque approche

Cas d’usage Recommandation Idéal pour
Appels d’API REST standard avec des modèles conventionnels /DownstreamApi GET, POST, PUT, PATCH, DELETE, réduction du code répétitif
Clients HTTP complexes nécessitant une configuration personnalisée /AuthorizationHeader Gestion des demandes/réponses spécialisées ; Contrôle affiné
Accès direct aux codes d’erreur HTTP et aux en-têtes nécessaires /AuthorizationHeader Applications nécessitant un contrôle de comportement HTTP de bas niveau
Simplicité et intégration rapide priorisées /DownstreamApi Applications hiérarchisant la simplicité par rapport au contrôle de bas niveau

Meilleures pratiques

  1. Réutiliser les clients HTTP : créez une fois et réutilisez pour éviter la surcharge de connexion
  2. Implémenter la gestion des erreurs : ajouter une logique de réessai pour les échecs temporaires avec retrait exponentiel
  3. Vérifier les codes d’état : vérifiez toujours le code d’état avant d’analyser le contenu de la réponse
  4. Définir des délais d’expiration : configurer les délais d’expiration de requête appropriés pour empêcher les demandes suspendues
  5. Inclure des ID de corrélation : journaliser toutes les demandes avec des identificateurs uniques pour le suivi de bout en bout
  6. Valider l’entrée : nettoyer et valider les données avant l’envoi aux API en aval
  7. Surveiller les performances : suivre la latence des appels d’API et les taux d’échec pour l’observabilité