다음을 통해 공유


시나리오: TypeScript에서 AgentID용 Microsoft Entra SDK 사용

AgentID용 Microsoft Entra SDK와 통합되어 토큰을 가져오고 다운스트림 API를 호출하는 TypeScript/Node.js 클라이언트 라이브러리를 만듭니다. 그런 다음 이 클라이언트를 Express.js 또는 NestJS 애플리케이션에 통합하여 인증된 API 요청을 처리합니다.

필수 조건

  • 활성 구독이 있는 Azure 계정. 무료로 계정을 만듭니다.
  • 개발 머신에 npm이 설치된 Node.js (버전 14 이상)
  • 사용자 환경에서 배포되고 실행되는 AgentID용 Microsoft Entra SDK입니다. 설치 지침은 설치 가이드 를 참조하세요.
  • 기본 URL 및 필수 범위를 사용하여 SDK에 구성된 다운스트림 API입니다.
  • Microsoft Entra ID의 적절한 권한 - 계정에 애플리케이션을 등록하고 API 권한을 부여할 수 있는 권한이 있어야 합니다.

설치 프로그램

클라이언트 라이브러리를 만들기 전에 HTTP 요청을 만드는 데 필요한 종속성을 설치합니다.

npm install node-fetch
npm install --save-dev @types/node-fetch

클라이언트 라이브러리 구현

AgentID용 Microsoft Entra SDK에 대한 HTTP 호출을 래핑하는 재사용 가능한 클라이언트 클래스를 만듭니다. 이 클래스는 토큰 전달, 요청 구성 및 오류 처리를 처리합니다.

// sidecar-client.ts
import fetch from 'node-fetch';

export interface SidecarConfig {
  baseUrl: string;
  timeout?: number;
}

export class SidecarClient {
  private readonly baseUrl: string;
  private readonly timeout: number;
  
  constructor(config: SidecarConfig) {
    this.baseUrl = config.baseUrl || process.env.SIDECAR_URL || 'http://localhost:5000';
    this.timeout = config.timeout || 10000;
  }
  
  async getAuthorizationHeader(
    incomingToken: string,
    serviceName: string,
    options?: {
      scopes?: string[];
      tenant?: string;
      agentIdentity?: string;
      agentUsername?: string;
    }
  ): Promise<string> {
    const url = new URL(`${this.baseUrl}/AuthorizationHeader/${serviceName}`);
    
    if (options?.scopes) {
      options.scopes.forEach(scope => 
        url.searchParams.append('optionsOverride.Scopes', scope)
      );
    }
    
    if (options?.tenant) {
      url.searchParams.append('optionsOverride.AcquireTokenOptions.Tenant', options.tenant);
    }
    
    if (options?.agentIdentity) {
      url.searchParams.append('AgentIdentity', options.agentIdentity);
      if (options.agentUsername) {
        url.searchParams.append('AgentUsername', options.agentUsername);
      }
    }
    
    const response = await fetch(url.toString(), {
      headers: { 'Authorization': incomingToken },
      signal: AbortSignal.timeout(this.timeout)
    });
    
    if (!response.ok) {
      throw new Error(`SDK error: ${response.statusText}`);
    }
    
    const data = await response.json();
    return data.authorizationHeader;
  }
  
  async callDownstreamApi<T>(
    incomingToken: string,
    serviceName: string,
    relativePath: string,
    options?: {
      method?: string;
      body?: any;
      scopes?: string[];
    }
  ): Promise<T> {
    const url = new URL(`${this.baseUrl}/DownstreamApi/${serviceName}`);
    url.searchParams.append('optionsOverride.RelativePath', relativePath);
    
    if (options?.method && options.method !== 'GET') {
      url.searchParams.append('optionsOverride.HttpMethod', options.method);
    }
    
    if (options?.scopes) {
      options.scopes.forEach(scope => 
        url.searchParams.append('optionsOverride.Scopes', scope)
      );
    }
    
    const fetchOptions: any = {
      method: options?.method || 'GET',
      headers: { 'Authorization': incomingToken },
      signal: AbortSignal.timeout(this.timeout)
    };
    
    if (options?.body) {
      fetchOptions.headers['Content-Type'] = 'application/json';
      fetchOptions.body = JSON.stringify(options.body);
    }
    
    const response = await fetch(url.toString(), fetchOptions);
    
    if (!response.ok) {
      throw new Error(`SDK error: ${response.statusText}`);
    }
    
    const data = await response.json();
    
    if (data.statusCode >= 400) {
      throw new Error(`API error ${data.statusCode}: ${data.content}`);
    }
    
    return JSON.parse(data.content) as T;
  }
}

// Usage
const sidecar = new SidecarClient({ baseUrl: 'http://localhost:5000' });

// Get authorization header
const authHeader = await sidecar.getAuthorizationHeader(token, 'Graph');

// Call API
interface UserProfile {
  displayName: string;
  mail: string;
  userPrincipalName: string;
}

const profile = await sidecar.callDownstreamApi<UserProfile>(
  token,
  'Graph',
  'me'
);

Express.js 통합

들어오는 토큰을 추출하는 미들웨어를 만들어 클라이언트 라이브러리를 Express.js 애플리케이션에 통합하고 다운스트림 API를 호출하는 처리기를 라우팅합니다.

import express from 'express';
import { SidecarClient } from './sidecar-client';

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

const sidecar = new SidecarClient({ baseUrl: process.env.SIDECAR_URL! });

// Middleware to extract token
app.use((req, res, next) => {
  const token = req.headers.authorization;
  if (!token && !req.path.startsWith('/health')) {
    return res.status(401).json({ error: 'No authorization token' });
  }
  req.userToken = token;
  next();
});

// Routes
app.get('/api/profile', async (req, res) => {
  try {
    const profile = await sidecar.callDownstreamApi(
      req.userToken,
      'Graph',
      'me'
    );
    res.json(profile);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.get('/api/messages', async (req, res) => {
  try {
    const messages = await sidecar.callDownstreamApi(
      req.userToken,
      'Graph',
      'me/messages?$top=10'
    );
    res.json(messages);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

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

NestJS 통합

NestJS 애플리케이션의 경우 클라이언트 라이브러리를 래핑하는 서비스를 만듭니다. 이 서비스를 컨트롤러에 삽입하여 인증된 요청을 처리할 수 있습니다.

import { Injectable } from '@nestjs/common';
import { SidecarClient } from './sidecar-client';

@Injectable()
export class GraphService {
  private readonly sidecar: SidecarClient;
  
  constructor() {
    this.sidecar = new SidecarClient({ 
      baseUrl: process.env.SIDECAR_URL! 
    });
  }
  
  async getUserProfile(token: string) {
    return await this.sidecar.callDownstreamApi(
      token,
      'Graph',
      'me'
    );
  }
  
  async getUserMessages(token: string, top: number = 10) {
    return await this.sidecar.callDownstreamApi(
      token,
      'Graph',
      `me/messages?$top=${top}`
    );
  }
}

모범 사례

TypeScript에서 AgentID용 Microsoft Entra SDK를 사용하는 경우 다음 방법을 따라 안정적이고 유지 관리 가능한 애플리케이션을 빌드합니다.

  • 클라이언트 인스턴스 다시 사용: 요청당 새 인스턴스를 만드는 대신 단일 SidecarClient 인스턴스를 만들고 애플리케이션 전체에서 다시 사용합니다. 이렇게 하면 성능 및 리소스 사용량이 향상됩니다.
  • 적절한 시간 제한 설정: 다운스트림 API 대기 시간에 따라 요청 시간 제한을 구성합니다. 이렇게 하면 SDK 또는 다운스트림 서비스가 느린 경우 애플리케이션이 무기한 중단되지 않습니다.
  • 오류 처리 구현: 특히 일시적인 오류에 대해 적절한 오류 처리 및 재시도 논리를 추가합니다. 클라이언트 오류(4xx)와 서버 오류(5xx)를 구분하여 적절한 응답을 확인합니다.
  • TypeScript 인터페이스 사용: API 응답에 대한 TypeScript 인터페이스를 정의하여 런타임이 아닌 컴파일 시간에 형식 안전성을 보장하고 오류를 catch합니다.
  • 연결 풀링 사용: HTTP 에이전트를 사용하여 요청 간에 연결을 다시 사용할 수 있으므로 오버헤드가 줄어들고 처리량이 향상됩니다.

기타 언어 가이드

다음 단계

시나리오 시작: