Scenario: utilizzo di Microsoft Entra ID Auth SDK (sidecar) da TypeScript

Creare una libreria client TypeScript/Node.js che si integra con Microsoft Entra ID Auth SDK (sidecar) per ottenere i token e chiamare le API downstream. Integrare quindi questo client in applicazioni Express.js o NestJS per gestire le richieste API autenticate.

Prerequisiti

  • Un account Azure con una sottoscrizione attiva. Creare un account gratuito.
  • Node.js (versione 14 o successiva) con npm installato nel computer di sviluppo.
  • Microsoft Entra ID Auth SDK (sidecar) distribuito e in esecuzione nel tuo ambiente. Per istruzioni sull'installazione, vedere Guida all'installazione .
  • API downstream configurate nell'SDK con URL di base e ambiti obbligatori.
  • Autorizzazioni appropriate in Microsoft Entra ID: l'account deve disporre delle autorizzazioni per registrare le applicazioni e concedere autorizzazioni API.

Configurazione

Prima di creare la libreria client, installare le dipendenze necessarie per effettuare richieste HTTP:

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

Implementazione della libreria client

Creare una classe client riutilizzabile che esegue il wrapping delle chiamate HTTP al Microsoft Entra ID Auth SDK (sidecar). Questa classe gestisce l'inoltro dei token, la configurazione delle richieste e la gestione degli errori:

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

integrazione Express.js

Integrare la libreria client in un'applicazione Express.js creando middleware per estrarre il token in ingresso e i gestori di route che chiamano le API downstream:

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

Integrazione di NestJS

Per le applicazioni NestJS, creare un servizio che esegue il wrapping della libreria client. Questo servizio può essere inserito nei controller per gestire le richieste autenticate:

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

Procedure consigliate

Quando si usa Microsoft Entra ID Auth SDK (sidecar) da TypeScript, seguire queste procedure per creare applicazioni affidabili e gestibili:

  • Riutilizzare l'istanza client: creare una singola SidecarClient istanza e riutilizzarla in tutta l'applicazione invece di creare nuove istanze per ogni richiesta. Ciò migliora le prestazioni e l'utilizzo delle risorse.
  • Impostare timeout appropriati: configurare i timeout delle richieste in base alla latenza dell'API downstream. Ciò impedisce all'applicazione di bloccarsi a tempo indefinito se l'SDK o il servizio downstream è lento.
  • Implementare la gestione degli errori: aggiungere una corretta gestione degli errori e logica di ripetizione dei tentativi, in particolare per gli errori temporanei. Distinguere tra errori client (4xx) ed errori del server (5xx) per determinare le risposte appropriate.
  • Usare le interfacce TypeScript: definire le interfacce TypeScript per le risposte api per garantire la sicurezza dei tipi e intercettare gli errori in fase di compilazione anziché in fase di esecuzione.
  • Abilita pool di connessioni: usare agenti HTTP per abilitare il riutilizzo della connessione tra le richieste, riducendo il sovraccarico e migliorando la velocità effettiva.