Anropa underordnade API:er från webb-API:er

Den här artikeln visar hur du anropar underordnade API:er från ASP.NET Core och OWIN-webb-API:er med hjälp av Microsoft. Identity.Web. Artikeln fokuserar på On-Behalf-Of (OBO) flödet, där ditt API tar emot en token från en klient och utbyter den mot en ny token för att anropa ett annat API.

Förstå flödet för på-uppdrag-av

Med OBO-flödet (On-Behalf-Of) kan webb-API:et anropa underordnade API:er för den användare som anropade ditt API. Det här flödet underhåller användarens identitet och behörigheter i hela anropskedjan.

Granska OBO-flödesdiagrammet

Följande diagram visar hur OBO-flödet fungerar mellan ditt API, Microsoft Entra ID och det underordnade API:et.

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

Förutsättningar för granskning

Kontrollera att följande finns på plats innan du börjar:

  • Webb-API som konfigurerats med JWT Bearer-autentisering
  • Appregistrering med API-behörigheter till underordnat API
  • Klientappen måste ha behörighet att anropa ditt API
  • Användaren måste ha samtyckt till både ditt API och underordnat API

Implementera i ASP.NET Core

Följande steg visar hur du konfigurerar ditt ASP.NET Core webb-API för att anropa underordnade API:er med hjälp av OBO-flödet.

1. Konfigurera autentisering

Konfigurera JWT Bearer-autentisering med explicit autentiseringsschema:

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. Konfigurera appsettings.json

Lägg till din Microsoft Entra app-registreringsinformation och API-konfigurationen i 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. Lägg till underordnat API-stöd

Registrera underordnade API:er från konfigurationsavsnittet.

using Microsoft.Identity.Web;

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

4. Anropa underordnat API från ditt API

Mata IDownstreamApi in i kontrollanten och använd den för att anropa underordnade API:er för användarens räkning.

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

Konfigurera cachelagring av token

Välj en strategi för tokencache baserat på distributionsmiljön.

Använda minnesintern cache för utveckling

Följande kod lägger till en minnesintern tokencache, som endast är lämplig för utveckling.

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

Varning! Använd distribuerad cache för produktion.

Använda distribuerad cache för produktion

För produktions-API:er med flera instanser använder du distribuerad cachelagring:

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();

Konfigurera andra distribuerade cacheprovidrar

Du kan också använda SQL Server, Cosmos DB eller PostgreSQL som distribuerad cacheprovider.

// 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");
});

Hantera långvariga processer med OBO

För långvariga bakgrundsprocesser behöver du särskild hantering eftersom användarens token kan upphöra att gälla.

Förstå förfalloutmaningen för token

Följande diagram visar hur förfallodatum för token kan påverka tidskrävande processer.

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

Välj strategier för sessionsnycklar

Långvariga OBO-processer använder en sessionsnyckel för att associera en cachelagrad OBO-token med ett visst bakgrundsarbetsflöde. Det finns två alternativ:

Tillvägagångssätt När det bör användas
Explicit nyckel – du anger din egen nyckel (till exempel en Guid) Du har redan en naturlig identifierare för arbetsobjektet (process-ID, jobb-ID osv.)
AllocateForMe – tokenskiktet genererar automatiskt en nyckel Du har ingen naturlig identifierare, eller så vill du att identitetsplattformen ska hantera nyckel unikhet. SDK använder hash(client_token) internt

Implementera långvariga processer med en explicit nyckel

I följande exempel visas hur du använder en explicit nyckel, till exempel ett process-ID, för långvariga bakgrundsarbetsflöden.

[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
        }
    }
}

Implementera långvariga processer med AllocateForMe

I stället för att hantera din egen nyckel anger du LongRunningWebApiSessionKey till det särskilda sentinel-värdet AcquireTokenOptions.LongRunningWebApiSessionKeyAuto (strängen "AllocateForMe"). Vid det första anropet genererar anskaffningsskiktet för token automatiskt en unik sessionsnyckel och skriver tillbaka den till samma AcquireTokenOptions instans. Sedan läser du den genererade nyckeln och skickar den till alla efterföljande anrop.

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

Granska viktiga överväganden

Tänk på följande när du implementerar långvariga OBO-processer.

  1. Livslängd för sessionsnyckel: Lagra den genererade sessionsnyckeln tillsammans med ditt arbetsobjekt (databas, kömeddelande osv.) så att bakgrundsarbetare kan hämta den.
  2. Tokencache: Använd distribuerad cache för bakgrundsprocesser.
  3. Användarkontext: Bakgrundsarbetaren kan komma åt HttpContext.User.
  4. Felhantering: Token kan fortfarande upphöra att gälla om användaren återkallar medgivandet.

Hantera fel i API:er

Webb-API:er kräver specifika felhanteringsmönster eftersom de inte kan omdirigera användare till interaktiva medgivandeflöden.

Hantera MicrosoftIdentityWebChallengeUserException

I webb-API:er kan du inte omdirigera användare till medgivande. Returnera i stället ett korrekt felsvar:

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

Klientappen ska hantera 401-svaret och utlösarmedgivandet:

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

Hantera underordnade API-fel

Mappa underordnade API-felsvar till lämpliga HTTP-statuskoder för dina anropare.

[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");
    }
}

Implementera i OWIN (.NET Framework)

Följande steg visar hur du konfigurerar ett OWIN-baserat webb-API för att anropa underordnade API:er.

1. Konfigurera Startup.cs

Konfigurera OWIN-mellanprogram med Microsoft. Identity.Web i klassen 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. Anropa API från kontroller

Använd tilläggsmetoder på kontrollanten för att hämta Graph-klienten, underordnad API-hjälp eller auktoriseringshuvudprovider.

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

Anropa flera underordnade API:er

Ditt API kan anropa flera underordnade API:er i en enda begäran:

[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");
    }
}

Följ metodtipsen

Använd dessa rekommendationer för att förbättra tillförlitligheten och säkerheten för dina API-till-API-anrop.

Använda distribuerad cache i produktion

Undvik minnesinterna cacheminnen i produktionsdistributioner. I följande exempel jämförs de två metoderna.

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

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

Konfigurera loggning

Lägg till strukturerad loggning för att samla in autentiserings- och underordnade API-händelser.

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

Ange lämpliga tidsgränser

Konfigurera HTTP-klienttimeouter för att förhindra långa väntetider för underordnade tjänster som inte svarar.

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

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

Verifiera inkommande token

Kontrollera att api:et verifierar token korrekt. Följande kod binder tokenverifieringsinställningar från konfigurationen.

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

Felsök vanliga fel

Använd de här lösningarna för att lösa vanliga problem med OBO-flödet.

Åtgärda "AADSTS50013: Signaturvalideringen för påståendet misslyckades"

Orsak: Klienthemligheten eller certifikatet är felkonfigurerat i API:ets appregistrering.

Solution: Kontrollera att klientautentiseringsuppgifterna i appsettings.json matchar Microsoft Entra ID appregistrering.

Lös "AADSTS65001: Användaren eller administratören har inte samtyckt"

Orsak: Användaren har inte samtyckt till att ditt API anropar det underordnade API:et.

Lösning: Returnera rätt fel till klientappen och utlösa medgivandeflödet i klienten.

Lös "AADSTS500133: Försäkran ligger inte inom dess giltiga tidsintervall"

Orsak: Klocksnedvridning mellan servrar eller en token som har upphört att gälla.

Lösning:

  • Synkronisera serverklockor
  • Kontrollera förfallodatum för token
  • Kontrollera att tokencache fungerar korrekt

Lös OBO-token som inte cachelagrats

Orsak: Den distribuerade cachen är inte konfigurerad eller så finns det problem med cachenyckeln.

Lösning:

  • Verifiera distribuerad cacheanslutning
  • Kontrollera att oid och tid påståenden finns i inkommande token
  • Aktivera felsökningsloggning för att se cacheåtgärder

Lösa flera API-instanser som inte delar cache

Orsak: API:et använder minnesintern cache i stället för distribuerad cache.

Solution: Växla till distribuerad cache (Redis, SQL Server, Cosmos DB).

För detaljerad diagnostik: Se Loggnings- och diagnostikguiden för korrelations-ID:n, felsökning av tokencache, PII-loggningskonfiguration och omfattande felsökningsarbetsflöden.


Nästa steg: Lär dig mer om calling Microsoft Graph eller anpassade API:er med specialiserade integrationsmönster.