Chiamare le API downstream dalle API web

Questo articolo illustra come chiamare le API downstream da API Web ASP.NET Core e OWIN usando Microsoft. Identity.Web. L'articolo è incentrato sul flusso ON-Behalf-Of (OBO), in cui l'API riceve un token da un client e lo scambia per un nuovo token per chiamare un'altra API.

Comprendere il flusso On-Behalf-Of

Il flusso on-Behalf-Of (OBO) consente all'API Web di chiamare le API downstream per conto dell'utente che ha chiamato l'API. Questo flusso mantiene l'identità e le autorizzazioni dell'utente in tutta la catena di chiamate.

Esaminare il diagramma di flusso OBO

Il diagramma seguente illustra il funzionamento del flusso OBO tra l'API, Microsoft Entra ID e l'API downstream.

sequenceDiagram
    participant Client as Client App
    participant YourAPI as Your Web API
    participant AzureAD as Microsoft Entra ID
    participant DownstreamAPI as Downstream API

    Client->>YourAPI: 1. Call with access token
    Note over YourAPI: Validate token
    YourAPI->>AzureAD: 2. OBO request with user token
    AzureAD->>AzureAD: 3. Validate & check consent
    AzureAD->>YourAPI: 4. New access token for downstream API
    Note over YourAPI: Cache token for user
    YourAPI->>DownstreamAPI: 5. Call with new token
    DownstreamAPI->>YourAPI: 6. Return data
    YourAPI->>Client: 7. Return processed data

Verificare i prerequisiti

Prima di iniziare, assicurarsi di disporre dei seguenti elementi:

  • API Web configurata con JWT Bearer per l'autenticazione
  • Registrazione dell'app con autorizzazioni API per l'API downstream
  • L'app client deve avere le autorizzazioni per chiamare l'API
  • L'utente deve aver dato il consenso sia alla vostra API che all'API a valle.

Implementare in ASP.NET Core

I passaggi seguenti illustrano come configurare l'API Web ASP.NET Core per chiamare le API downstream usando il flusso OBO.

1. Configurare l'autenticazione

Configurare l'autenticazione JWT Bearer utilizzando uno schema di autenticazione esplicito:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

// Add authentication with explicit scheme
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

builder.Services.AddAuthorization();
builder.Services.AddControllers();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.Run();

2. Configurare appsettings.json

Aggiungere i dettagli di registrazione dell'app Microsoft Entra e la configurazione dell'API downstream a appsettings.json.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "your-tenant-id",
    "ClientId": "your-api-client-id",
    "ClientCredentials": [
      {
        "SourceType": "ClientSecret",
        "ClientSecret": "your-client-secret"
      }
    ],
    "Audience": "api://your-api-client-id"
  },
  "DownstreamApis": {
    "GraphAPI": {
      "BaseUrl": "https://graph.microsoft.com/v1.0",
      "Scopes": ["https://graph.microsoft.com/.default"]
    },
    "PartnerAPI": {
      "BaseUrl": "https://partnerapi.example.com",
      "Scopes": ["api://partner-api-id/read"]
    }
  }
}

3. Aggiungere il supporto dell'API downstream

Registrare le API downstream dalla sezione di configurazione.

using Microsoft.Identity.Web;

builder.Services.AddDownstreamApis(
    builder.Configuration.GetSection("DownstreamApis"));

4. Effettuare una chiamata all'API downstream dalla propria API

Inietta IDownstreamApi nel tuo controller e utilizzalo per chiamare le API downstream per conto dell'utente.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;
using Microsoft.Identity.Abstractions;

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class DataController : ControllerBase
{
    private readonly IDownstreamApi _downstreamApi;
    private readonly ILogger<DataController> _logger;

    public DataController(
        IDownstreamApi downstreamApi,
        ILogger<DataController> logger)
    {
        _downstreamApi = downstreamApi;
        _logger = logger;
    }

    [HttpGet("userdata")]
    public async Task<ActionResult<UserData>> GetUserData()
    {
        try
        {
            // Call downstream API using OBO flow
            // Token from incoming request is automatically used
            var userData = await _downstreamApi.GetForUserAsync<UserData>(
                "PartnerAPI",
                "api/users/me");

            return Ok(userData);
        }
        catch (MicrosoftIdentityWebChallengeUserException ex)
        {
            // User needs to consent to downstream API permissions
            _logger.LogWarning(ex, "User consent required for downstream API");
            return Unauthorized(new { error = "consent_required", scopes = ex.Scopes });
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Downstream API call failed");
            return StatusCode(500, "Failed to retrieve data from downstream service");
        }
    }

    [HttpPost("process")]
    public async Task<ActionResult<ProcessResult>> ProcessData([FromBody] DataRequest request)
    {
        // Call downstream API with POST
        var result = await _downstreamApi.PostForUserAsync<DataRequest, ProcessResult>(
            "PartnerAPI",
            "api/process",
            request);

        return Ok(result);
    }
}

Configurare la memorizzazione nella cache dei token

Scegliere una strategia di cache dei token in base all'ambiente di distribuzione.

Usare la cache in memoria per lo sviluppo

Il codice seguente aggiunge una cache dei token in memoria, adatta solo allo sviluppo.

builder.Services.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

Avviso: usare la cache distribuita per l'ambiente di produzione.

Usare la cache distribuita per l'ambiente di produzione

Per le API di produzione con più istanze, usare la memorizzazione nella cache distribuita:

using Microsoft.Extensions.Caching.StackExchangeRedis;

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "MyWebApi";
});

builder.Services.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDistributedTokenCaches();

Configurare altri provider di cache distribuita

È anche possibile usare SQL Server, Cosmos DB o PostgreSQL come provider di cache distribuita.

// SQL Server
builder.Services.AddDistributedSqlServerCache(options =>
{
    options.ConnectionString = builder.Configuration.GetConnectionString("TokenCacheDb");
    options.SchemaName = "dbo";
    options.TableName = "TokenCache";
});

// Cosmos DB
builder.Services.AddCosmosDbTokenCaches(options =>
{
    options.DatabaseId = "TokenCache";
    options.ContainerId = "Tokens";
});

// PostgreSQL (requires Microsoft.Extensions.Caching.Postgres)
builder.Services.AddDistributedPostgresCache(options =>
{
    options.ConnectionString = builder.Configuration.GetConnectionString("PostgresCache");
    options.SchemaName = builder.Configuration["PostgresCache:SchemaName"];
    options.TableName = builder.Configuration["PostgresCache:TableName"];
    options.CreateIfNotExists = builder.Configuration.GetValue<bool>("PostgresCache:CreateIfNotExists");
});

Gestire processi a esecuzione prolungata con OBO

Per i processi in background a esecuzione prolungata, è necessaria una gestione speciale perché il token dell'utente potrebbe scadere.

Informazioni sulla richiesta di scadenza del token

Il diagramma seguente illustra come la scadenza del token può influire sui processi a esecuzione prolungata.

graph TD
    A[Client calls API] --> B[API receives user token]
    B --> C[API starts long process]
    C --> D{Token expires?}
    D -->|Yes| E[ OBO fails]
    D -->|No| F[ OBO succeeds]

    style E fill:#f8d7da
    style F fill:#d4edda

Scegliere le strategie chiave della sessione

I processi OBO a esecuzione prolungata usano una chiave di sessione per associare un token OBO memorizzato nella cache a un flusso di lavoro in background specifico. Sono disponibili due opzioni:

Avvicinarsi Quando utilizzare
Chiave esplicita: si specifica la propria chiave (ad esempio, un )Guid Si dispone già di un identificatore naturale per l'elemento di lavoro (ID processo, ID lavoro, ad esempio)
AllocateForMe - Il livello del token genera automaticamente una chiave Non si dispone di un identificatore naturale o si vuole che la piattaforma delle identità gestisca l'univocità delle chiavi. L'SDK userà hash(client_token) internamente

Implementare processi a esecuzione prolungata con una chiave esplicita

Nell'esempio seguente viene illustrato come usare una chiave esplicita, ad esempio un ID processo, per i flussi di lavoro in background a esecuzione prolungata.

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class ProcessingController : ControllerBase
{
    private readonly IDownstreamApi _downstreamApi;
    private readonly IBackgroundTaskQueue _taskQueue;

    public ProcessingController(
        IDownstreamApi downstreamApi,
        IBackgroundTaskQueue taskQueue)
    {
        _downstreamApi = downstreamApi;
        _taskQueue = taskQueue;
    }

    [HttpPost("start")]
    public async Task<ActionResult<ProcessStatus>> StartLongProcess([FromBody] ProcessRequest request)
    {
        var processId = Guid.NewGuid();

        // Queue the long-running task
        _taskQueue.QueueBackgroundWorkItem(async (cancellationToken) =>
        {
            await ProcessDataAsync(processId, request, cancellationToken);
        });

        return Accepted(new ProcessStatus
        {
            ProcessId = processId,
            Status = "Started"
        });
    }

    private async Task ProcessDataAsync(
        Guid processId,
        ProcessRequest request,
        CancellationToken cancellationToken)
    {
        try
        {
            // The cached refresh token allows token acquisition even if original token expired
            var data = await _downstreamApi.GetForUserAsync<ProcessData>(
                "PartnerAPI",
                options => {
                   options.RelativePath = "api/process/data";
                   options.AcquireTokenOptions.LongRunningWebApiSessionKey = processId.ToString()
                },
                cancellationToken: cancellationToken);

            // Process data...
            await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken);

            // Call API again (token may need refresh)
            await _downstreamApi.PostForUserAsync<ProcessData, ProcessResult>(
                "PartnerAPI",
                options => {
                   options.RelativePath = "api/process/complete";
                   options.AcquireTokenOptions.LongRunningWebApiSessionKey = processId.ToString()
                },
                data,
                cancellationToken: cancellationToken);
        }
        catch (Exception ex)
        {
            // Log error and update process status
        }
    }
}

Implementare processi a esecuzione prolungata con AllocateForMe

Invece di gestire la propria chiave, imposta LongRunningWebApiSessionKey sul valore sentinel speciale AcquireTokenOptions.LongRunningWebApiSessionKeyAuto (la stringa "AllocateForMe"). Nella prima chiamata il livello di acquisizione del token genera automaticamente una chiave di sessione univoca e la riscriva nella stessa AcquireTokenOptions istanza. Si legge quindi la chiave generata e la si passa a tutte le chiamate successive.

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class AutoKeyProcessingController : ControllerBase
{
    private readonly IDownstreamApi _downstreamApi;
    private readonly IBackgroundTaskQueue _taskQueue;

    public AutoKeyProcessingController(
        IDownstreamApi downstreamApi,
        IBackgroundTaskQueue taskQueue)
    {
        _downstreamApi = downstreamApi;
        _taskQueue = taskQueue;
    }

    [HttpPost("start")]
    public async Task<ActionResult<ProcessStatus>> StartLongProcess([FromBody] ProcessRequest request)
    {
        // First call: let the platform allocate a session key
        var options = new DownstreamApiOptions
        {
            RelativePath = "api/process/data",
            AcquireTokenOptions = new AcquireTokenOptions
            {
                // Sentinel value — the platform will replace this with a generated key
                LongRunningWebApiSessionKey = AcquireTokenOptions.LongRunningWebApiSessionKeyAuto  // "AllocateForMe"
            }
        };

        var data = await _downstreamApi.GetForUserAsync<ProcessData>(
            "PartnerAPI",
            optionsOverride => {
                optionsOverride.RelativePath = options.RelativePath;
                optionsOverride.AcquireTokenOptions.LongRunningWebApiSessionKey =
                    options.AcquireTokenOptions.LongRunningWebApiSessionKey;
            });

        // After the call, the platform has replaced the sentinel with the generated key.
        string generatedSessionKey = options.AcquireTokenOptions.LongRunningWebApiSessionKey;
        // generatedSessionKey is now a unique string such as "a1b2c3d4..." — no longer "AllocateForMe".

        // Queue background work using the generated key
        _taskQueue.QueueBackgroundWorkItem(async (cancellationToken) =>
        {
            await ContinueProcessingAsync(generatedSessionKey, data, cancellationToken);
        });

        return Accepted(new ProcessStatus
        {
            SessionKey = generatedSessionKey,
            Status = "Started"
        });
    }

    private async Task ContinueProcessingAsync(
        string sessionKey,
        ProcessData data,
        CancellationToken cancellationToken)
    {
        // Process data...
        await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken);

        // Subsequent calls: reuse the generated session key
        await _downstreamApi.PostForUserAsync<ProcessData, ProcessResult>(
            "PartnerAPI",
            options => {
                options.RelativePath = "api/process/complete";
                options.AcquireTokenOptions.LongRunningWebApiSessionKey = sessionKey;
            },
            data,
            cancellationToken: cancellationToken);
    }
}

Esaminare le considerazioni importanti

Tenere presente quanto segue quando si implementano processi OBO a esecuzione prolungata.

  1. Durata chiave di sessione: Archiviare la chiave di sessione generata insieme all'elemento di lavoro (database, messaggio della coda e così via) in modo che i ruoli di lavoro in background possano recuperarle.
  2. Cache dei token: usare la cache distribuita per i processi in background.
  3. Contesto utente: il ruolo di lavoro in background può accedere a HttpContext.User.
  4. Gestione degli errori: il token potrebbe comunque scadere se l'utente revoca il consenso.

Gestire gli errori nelle API

Le API Web richiedono modelli di gestione degli errori specifici perché non possono reindirizzare gli utenti ai flussi di consenso interattivi.

Gestire MicrosoftIdentityWebChallengeUserException

Nelle API Web non è possibile reindirizzare gli utenti al consenso. Restituire invece una risposta di errore corretta:

[HttpGet("data")]
public async Task<ActionResult> GetData()
{
    try
    {
        var data = await _downstreamApi.GetForUserAsync<Data>("PartnerAPI", "api/data");
        return Ok(data);
    }
    catch (MicrosoftIdentityWebChallengeUserException ex)
    {
        // Return 401 with consent information
        return Unauthorized(new
        {
            error = "consent_required",
            error_description = "Additional user consent required",
            scopes = ex.Scopes,
            claims = ex.Claims
        });
    }
}

L'app client deve gestire la risposta 401 e attivare il consenso:

// Client app code
var response = await httpClient.GetAsync("https://yourapi.example.com/api/data");

if (response.StatusCode == HttpStatusCode.Unauthorized)
{
    var error = await response.Content.ReadFromJsonAsync<ConsentError>();

    if (error?.error == "consent_required")
    {
        // Trigger incremental consent in client app
        // This will redirect user to Microsoft Entra ID for consent
        throw new MsalUiRequiredException(error.error_description, error.scopes);
    }
}

Gestire gli errori dell'API downstream

Associa le risposte di errore dell'API downstream ai codici di stato HTTP appropriati per i chiamanti.

[HttpGet("data")]
public async Task<ActionResult> GetData()
{
    try
    {
        var data = await _downstreamApi.GetForUserAsync<Data>("PartnerAPI", "api/data");
        return Ok(data);
    }
    catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
    {
        return NotFound("Resource not found in downstream service");
    }
    catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.BadRequest)
    {
        return BadRequest("Invalid request to downstream service");
    }
    catch (HttpRequestException ex)
    {
        _logger.LogError(ex, "Downstream API returned {StatusCode}", ex.StatusCode);
        return StatusCode(502, "Downstream service error");
    }
}

Implementare in OWIN (framework .NET)

I passaggi seguenti illustrano come configurare un'API Web basata su OWIN per chiamare le API downstream.

1. Configurare Startup.cs

Configurare il middleware OWIN con Microsoft.Identity.Web nella classe Startup.

using Microsoft.Identity.Web;
using Microsoft.Identity.Web.OWIN;
using Owin;

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
      OwinTokenAcquirerFactory factory = TokenAcquirerFactory.GetDefaultInstance<OwinTokenAcquirerFactory>();
      app.AddMicrosoftIdentityWebApi(factory);
      factory.Services
        .AddMicrosoftGraph()
        .AddDownstreamApis(factory.Configuration.GetSection("DownstreamAPIs"));
       factory.Build();
    }
}

2. Chiamare l'API dai controller

Utilizzare i metodi di estensione nel controller per ottenere il client di Graph, il supporto per l'API downstream o il fornitore di intestazioni di autorizzazione.

using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web;
using System.Web.Http;

[Authorize]
public class DataController : ApiController
{
    private readonly IDownstreamApi _downstreamApi;

    public DataController()
    {
      GraphServiceClient graphServiceClient = this.GetGraphServiceClient();
      var me = await graphServiceClient.Me.Request().GetAsync();

      // OR - Example calling a downstream directly with the IDownstreamApi helper (uses the
      // authorization header provider, encapsulates MSAL.NET)
      // downstreamApi won't be null if you added services.AddMicrosoftGraph()
      // in the Startup.auth.cs
      IDownstreamApi downstreamApi = this.GetDownstreamApi();
      var result = await downstreamApi.CallApiForUserAsync("DownstreamAPI");

      // OR - Get an authorization header (uses the token acquirer)
      IAuthorizationHeaderProvider authorizationHeaderProvider =
           this.GetAuthorizationHeaderProvider();
    }

    [HttpGet]
    [Route("api/data")]
    public async Task<IHttpActionResult> GetData()
    {
        var data = await _downstreamApi.GetForUserAsync<Data>(
            "PartnerAPI",
            options => options.RelativePath = "api/data",
            options => options.Scopes = new[] { "api://partner/read" });

        return Ok(data);
    }
}

Chiamare più API trasversali

L'API può chiamare più API downstream in una singola richiesta:

[HttpGet("dashboard")]
public async Task<ActionResult<Dashboard>> GetDashboard()
{
    try
    {
        // Call multiple APIs in parallel
        var userTask = _downstreamApi.GetForUserAsync<User>(
            "GraphAPI", "me");

        var dataTask = _downstreamApi.GetForUserAsync<Data>(
            "PartnerAPI", "api/data");

        var settingsTask = _downstreamApi.GetForUserAsync<Settings>(
            "PartnerAPI", "api/settings");

        await Task.WhenAll(userTask, dataTask, settingsTask);

        return Ok(new Dashboard
        {
            User = userTask.Result,
            Data = dataTask.Result,
            Settings = settingsTask.Result
        });
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to retrieve dashboard data");
        return StatusCode(500, "Failed to retrieve dashboard");
    }
}

Seguire le migliori pratiche

Applicare queste raccomandazioni per migliorare l'affidabilità e la sicurezza delle chiamate API a API.

Usare la cache distribuita nell'ambiente di produzione

Evitare cache in memoria nelle distribuzioni di produzione. Nell'esempio seguente vengono confrontati i due approcci.

//  Bad: In-memory cache in production
.AddInMemoryTokenCaches();

//  Good: Distributed cache in production
.AddDistributedTokenCaches();

Configurare la registrazione

Aggiungere la registrazione strutturata per acquisire l'autenticazione e gli eventi dell'API downstream.

builder.Services.AddLogging(config =>
{
    config.AddConsole();
    config.AddApplicationInsights();
    config.SetMinimumLevel(LogLevel.Information);
});

Impostare i timeout appropriati

Configurare i timeout del client HTTP per evitare attese lunghe per i servizi downstream non rispondenti.

builder.Services.AddDownstreamApi("PartnerAPI", options =>
{
    options.BaseUrl = "https://partnerapi.example.com";
    options.HttpClientName = "PartnerAPI";
});

builder.Services.AddHttpClient("PartnerAPI", client =>
{
    client.Timeout = TimeSpan.FromSeconds(30);
});

Convalidare i token in ingresso

Assicurarsi che l'API convalida correttamente i token. Il codice seguente associa le impostazioni di convalida dei token dalla configurazione.

builder.Services.AddMicrosoftIdentityWebApi(options =>
{
    builder.Configuration.Bind("AzureAd", options);
});

Risolvere gli errori comuni

Usare queste soluzioni per risolvere i problemi riscontrati frequentemente con il flusso OBO.

Risolvere "AADSTS50013: Convalida della firma dell'asserzione non riuscita"

Causa: il segreto client o il certificato non è configurato correttamente nella registrazione dell'applicazione API.

Solution: verificare che le credenziali client in appsettings.json corrispondano alla registrazione dell'app Microsoft Entra ID.

Risolvere "AADSTS65001: l'utente o l'amministratore non ha acconsentito"

Causa: l'utente non ha acconsentito alla tua API di effettuare chiamate all'API a valle.

Soluzione: restituire un errore corretto al client e attivare il flusso di consenso del client.

Risolvere "AADSTS500133: l'asserzione non rientra nell'intervallo di tempo valido"

Causa: sfasamento dell'orologio tra i server o un token scaduto.

Soluzione:

  • Sincronizzare gli orologi del server
  • Controllare la scadenza del token
  • Verificare che la cache dei token funzioni correttamente

Risolvere il token OBO non memorizzato nella cache

Causa: la cache distribuita non è configurata o si verificano problemi di chiave della cache.

Soluzione:

  • Verificare la connessione alla cache distribuita
  • Verificare che le attestazioni oid e tid esistano nel token in ingresso.
  • Abilitare la registrazione di debug per visualizzare le operazioni della cache

Risolvere più istanze dell'API che non condividono la cache

Causa: l'API usa la cache in memoria anziché la cache distribuita.

Solution: passare alla cache distribuita (Redis, SQL Server, Cosmos DB).

Per una diagnostica dettagliata: Vedere Guida alla registrazione e alla diagnostica per id di correlazione, debug della cache dei token, configurazione della registrazione delle informazioni personali e flussi di lavoro completi per la risoluzione dei problemi.


Passaggi successivi: informazioni su calling Microsoft Graph o api personalizzate con modelli di integrazione specializzati.