シナリオ: ダウンストリーム API を呼び出す

Microsoft Entra SDK for Agent ID を使用して、トークンの取得と HTTP 通信の両方を 1 回の操作で処理します。 SDK は、受信トークンをダウンストリーム API にスコープ指定されたトークンと交換し、HTTP 呼び出しを行い、応答を返します。 このガイドでは、ダウンストリーム API の構成、TypeScript とPythonでの呼び出しの実装、さまざまな HTTP メソッドの処理、再試行によるエラーの管理を行う方法について説明します。

[前提条件]

  • アクティブなサブスクリプションを持つAzure アカウント。 無料でアカウントを作成できます
  • Microsoft Entra SDK for AgentID環境でデプロイされて実行されます。 セットアップ手順については 、インストール ガイド を参照してください。
  • SDKでは、呼び出すAPIのベースURLと必要なスコープを用いてダウンストリームAPIを構成します。
  • 認証されたクライアントからのベアラー トークン - アプリケーションは、SDK に転送するクライアント アプリケーションからトークンを受け取ります。
  • Microsoft Entra ID のアクセス許可を適用する - アカウントには、アプリケーションを登録し、API アクセス許可を付与するためのアクセス許可が必要です。

コンフィギュレーション

Microsoft Entra SDK for Agent ID 環境設定でダウンストリーム API を構成します。

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"

この構成では、次を指定します。

  • BaseUrl: ダウンストリーム API のルート エンドポイント
  • スコープ: ダウンストリーム API へのアクセスに必要なアクセス許可

TypeScript/Node.js

次の例は、TypeScript および Node.js アプリケーションからダウンストリーム API を呼び出す方法を示しています。 このコードは、再利用可能な関数と 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 }
  );
}

次の例では、ミドルウェアとルート ハンドラーを使用して、これらの関数を Express.js アプリケーションに統合する方法を示します。

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

次の例では、HTTP 処理のために要求ライブラリと Flask を使用して、Python アプリケーションからダウンストリーム API を呼び出す方法を示します。

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

これらの関数を Flask アプリケーションに統合する場合は、次の例を使用できます。

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

POST/PUT/PATCH リクエスト

/DownstreamApi エンドポイントは、HTTP メソッドと要求本文を渡すことによって変更操作をサポートします。 これらのパターンは、ダウンストリーム API でリソースを作成、更新、または削除する必要がある場合に使用します。

リソースの作成

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

リソースの更新

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

高度なシナリオ

次のシナリオは、特殊なユース ケースの高度な構成を示しています。

カスタム ヘッダー

ダウンストリーム API 要求にカスタム ヘッダーを追加します。

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

スコープをオーバーライドする

既定で構成されたスコープとは異なるスコープを要求します。

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

エージェント ID を用いた認証

エージェント ID を使用して、アプリケーションのアクセス許可を持つ API を呼び出します。

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

エラー処理

一時的な障害を適切に処理するために、指数バックオフを使用して再試行ロジックを実装します。

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

AuthorizationHeader アプローチとの比較

Microsoft Entra SDK for Agent ID には、ダウンストリーム API を呼び出すための 2 つの方法が用意されています。 この比較を使用して、ニーズに最も適したアプローチを決定します。

機能の比較

能力 /DownstreamApi /AuthorizationHeader
トークンの取得 SDK によって処理されます SDK によって処理されます
HTTP 要求 SDK によって処理されます お客様の責任
応答の解析 JSON 形式で包む 直接 HTTP 応答
カスタム ヘッダー クエリ パラメーターを使用する 完全な HTTP 制御
要求本文 自動的に転送される フル コントロール
エラー処理 SDK によってラップされたエラー 標準 HTTP エラー

各アプローチを使用するタイミング

使用事例 勧告 最適な対象者
従来のパターンを使用した標準 REST API 呼び出し /DownstreamApi GET、POST、PUT、PATCH、DELETE 操作。定型句の削減
カスタム構成を必要とする複雑な HTTP クライアント /AuthorizationHeader 特殊な要求/応答処理。きめ細かい制御
必要な HTTP エラー コードとヘッダーへの直接アクセス /AuthorizationHeader 低レベルの HTTP 動作制御を必要とするアプリケーション
シンプルで迅速な統合の優先順位付け /DownstreamApi 低レベルの制御よりもシンプルさを優先するアプリケーション

ベスト プラクティス

  1. HTTP クライアントを再利用する: 接続のオーバーヘッドを回避するために 1 回作成して再利用する
  2. エラー処理を実装する: 指数バックオフを伴う一時的な障害の再試行ロジックを追加する
  3. 状態コードの確認: 応答コンテンツを解析する前に、常に状態コードを確認してください
  4. タイムアウトの設定: 要求のハングを防ぐために適切な要求タイムアウトを構成する
  5. 関連付け ID を含める: エンド ツー エンドトレースの一意の識別子を持つすべての要求をログに記録する
  6. 入力の検証: ダウンストリーム API に送信する前にデータをサニタイズして検証する
  7. パフォーマンスの監視: API 呼び出しの待機時間と失敗率を追跡して監視可能