Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
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.
- 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.
- Cache dei token: usare la cache distribuita per i processi in background.
-
Contesto utente: il ruolo di lavoro in background può accedere a
HttpContext.User. - 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
});
}
}
Gestire i requisiti di consenso nelle app client
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
oidetidesistano 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.
Esplorare il contenuto correlato
- Processi di lunga durata
- Memorizzazione nella cache dei token
- Chiamate dalle App Web
- Scenari di API Web
- API dietro un gateway
- Registrazione e diagnostica - Risoluzione dei problemi di autenticazione e token
- Guida all'autorizzazione - RequiredScope e convalida delle autorizzazioni dell'app
- Guida alla personalizzazione - Personalizzazione avanzata delle acquisizioni di token
Passaggi successivi: informazioni su calling Microsoft Graph o api personalizzate con modelli di integrazione specializzati.