Freigeben über


Szenario: Verwenden des Microsoft Entra SDK für AgentID aus TypeScript

Erstellen Sie eine TypeScript/Node.js Clientbibliothek, die in das Microsoft Entra SDK für AgentID integriert ist, um Token abzurufen und downstream-APIs aufzurufen. Integrieren Sie diesen Client dann in Express.js- oder NestJS-Anwendungen, um authentifizierte API-Anforderungen zu verarbeiten.

Voraussetzungen

  • Ein Azure-Konto mit einem aktiven Abonnement. Kostenlos ein Konto erstellen.
  • Node.js (Version 14 oder höher) mit npm auf Ihrem Entwicklungscomputer installiert.
  • Das Microsoft Entra SDK für AgentID wird in Ihrer Umgebung bereitgestellt und ausgeführt. Anweisungen zur Einrichtung finden Sie im Installationshandbuch .
  • Downstream-APIs , die im SDK mit Basis-URLs und erforderlichen Bereichen konfiguriert sind.
  • Geeignete Berechtigungen in microsoft Entra ID – Ihr Konto muss über Berechtigungen zum Registrieren von Anwendungen verfügen und API-Berechtigungen erteilen.

Konfiguration

Installieren Sie vor dem Erstellen der Clientbibliothek die erforderlichen Abhängigkeiten zum Erstellen von HTTP-Anforderungen:

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

Clientbibliotheksimplementierung

Erstellen Sie eine wiederverwendbare Clientklasse, die HTTP-Aufrufe an das Microsoft Entra SDK für AgentID umschließt. Diese Klasse behandelt die Tokenweiterleitung, Anforderungskonfiguration und Fehlerbehandlung:

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

Integrieren Sie die Clientbibliothek in eine Express.js-Anwendung, indem Sie Middleware und Routenhandler erstellen, die das eingehende Token extrahieren und nachgeschaltete APIs aufrufen.

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

Erstellen Sie für NestJS-Anwendungen einen Dienst, der die Clientbibliothek umschließt. Dieser Dienst kann in Controller eingefügt werden, um authentifizierte Anforderungen zu verarbeiten:

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

Bewährte Methoden

Wenn Sie das Microsoft Entra SDK für AgentID aus TypeScript verwenden, befolgen Sie die folgenden Methoden, um zuverlässige und verwaltete Anwendungen zu erstellen:

  • Clientinstanz wiederverwenden: Erstellen Sie eine einzelne SidecarClient Instanz, und verwenden Sie sie in der gesamten Anwendung wieder, anstatt neue Instanzen pro Anforderung zu erstellen. Dadurch wird die Leistung und die Ressourcennutzung verbessert.
  • Legen Sie geeignete Timeouts fest: Konfigurieren Sie Anforderungstimeouts basierend auf der nachgeschalteten API-Latenz. Dadurch wird verhindert, dass Ihre Anwendung unbegrenzt hängen bleibt, wenn der SDK- oder downstream-Dienst langsam ist.
  • Implementieren der Fehlerbehandlung: Fügen Sie eine ordnungsgemäße Fehlerbehandlungs- und Wiederholungslogik hinzu, insbesondere für vorübergehende Fehler. Unterscheiden Sie zwischen Clientfehlern (4xx) und Serverfehlern (5xx), um geeignete Antworten zu ermitteln.
  • Verwenden Sie TypeScript-Schnittstellen: Definieren Sie TypeScript-Schnittstellen für API-Antworten, um die Typsicherheit sicherzustellen und Fehler zur Kompilierungszeit anstatt zur Laufzeit abzufangen.
  • Aktivieren von Verbindungspooling: Verwenden Sie HTTP-Agents, um die Wiederverwendung der Verbindung über Anforderungen hinweg zu ermöglichen, wodurch der Aufwand reduziert und der Durchsatz verbessert wird.

Andere Sprachhandbücher

Nächste Schritte

Beginnen Sie mit einem Szenario: