Llamar a las API posteriores desde aplicaciones web

En esta guía se explica cómo llamar a las API de bajada desde aplicaciones web de ASP.NET Core y OWIN mediante Microsoft. Identity.Web. En las aplicaciones web, se adquieren tokens en nombre del usuario que inició sesión para llamar a las APIs con permisos delegados.

Descripción del flujo de tokens

Cuando un usuario inicia sesión en la aplicación web, puede llamar a las API descendentes (Microsoft Graph, servicios de Azure o API personalizadas) en su nombre. Microsoft. Identity.Web controla la adquisición, el almacenamiento en caché y la actualización automática de tokens.

Revisión del flujo de tokens de usuario

sequenceDiagram
    participant User as User Browser
    participant WebApp as Your Web App
    participant AzureAD as Microsoft Entra ID
    participant API as Downstream API

    User->>WebApp: 1. Access page requiring API data
    Note over WebApp: User already signed in
    WebApp->>AzureAD: 2. Request access token for API<br/>(using user's refresh token)
    AzureAD->>AzureAD: 3. Validate & check consent
    AzureAD->>WebApp: 4. Return access token
    Note over WebApp: Cache token
    WebApp->>API: 5. Call API with token
    API->>WebApp: 6. Return data
    WebApp->>User: 7. Render page with data

Revisión de los requisitos previos

Compruebe que el entorno cumple los siguientes requisitos antes de comenzar.

  • Aplicación web configurada con la autenticación de OpenID Connect
  • El inicio de sesión del usuario funciona
  • Registro de aplicaciones con permisos de API configurados
  • Consentimiento del usuario obtenido (o consentimiento del administrador concedido)

Implementación de ASP.NET Core

1. Configuración de la autenticación y la adquisición de tokens

Agregue servicios de autenticación y habilite la adquisición de tokens en el Program.cs archivo.

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

var builder = WebApplication.CreateBuilder(args);

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

builder.Services.AddRazorPages()
    .AddMicrosoftIdentityUI();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});

var app = builder.Build();

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

app.MapRazorPages();
app.Run();

2. Configurar appsettings.json

Defina la configuración del registro de aplicaciones de Microsoft Entra ID y la API descendente en appsettings.json.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "your-tenant-id",
    "ClientId": "your-client-id",
    "CallbackPath": "/signin-oidc",
    "SignedOutCallbackPath": "/signout-callback-oidc",
    "ClientCredentials": [
      {
        "SourceType": "ClientSecret",
        "ClientSecret": "your-client-secret"
      }
    ]
  },
  "DownstreamApis": {
    "GraphAPI": {
      "BaseUrl": "https://graph.microsoft.com/v1.0",
      "Scopes": ["user.read", "mail.read"]
    },
    "MyAPI": {
      "BaseUrl": "https://myapi.example.com",
      "Scopes": ["api://my-api-id/access_as_user"]
    }
  }
}

Importante: En el caso de las aplicaciones web que llaman a las API de bajada, necesita credenciales de cliente (certificado o secreto) además de la configuración de inicio de sesión.

3. Agregar compatibilidad para la API descendente

Elija una de las siguientes opciones para registrar las API de bajada.

Opción A: Registrar API con nombre

El siguiente código registra varias API descendentes desde la configuración.

using Microsoft.Identity.Web;

// Register multiple downstream APIs
builder.Services.AddDownstreamApis(
    builder.Configuration.GetSection("DownstreamApis"));

Option B: Use Microsoft Graph Helper

El código siguiente registra el cliente del SDK de Microsoft Graph desde la configuración.

// Install: Microsoft.Identity.Web.GraphServiceClient
builder.Services.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApis:GraphAPI"));

4. Llamar a la API descendente desde el controlador

Inserte IDownstreamApi en el controlador y llame a la API en nombre del usuario que ha iniciado sesión.

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

[Authorize]
public class ProfileController : Controller
{
    private readonly IDownstreamApi _downstreamApi;
    private readonly ILogger<ProfileController> _logger;

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

    public async Task<IActionResult> Index()
    {
        try
        {
            // Call downstream API on behalf of user
            var userData = await _downstreamApi.GetForUserAsync<UserData>(
                "MyAPI",
                options => options.RelativePath = "api/profile");

            return View(userData);
        }
        catch (MicrosoftIdentityWebChallengeUserException ex)
        {
            // Incremental consent required
            // Redirect user to consent page
            return Challenge(
                new AuthenticationProperties
                {
                    RedirectUri = "/Profile"
                },
                OpenIdConnectDefaults.AuthenticationScheme);
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Failed to call downstream API");
            return View("Error");
        }
    }
}

5. Llamar a la API descendente desde la Página de Razor

Inserte IDownstreamApi en el modelo de página de Razor y llame a la API.

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

[Authorize]
public class ProfileModel : PageModel
{
    private readonly IDownstreamApi _downstreamApi;

    public UserData UserData { get; set; }

    public ProfileModel(IDownstreamApi downstreamApi)
    {
        _downstreamApi = downstreamApi;
    }

    public async Task OnGetAsync()
    {
        try
        {
            UserData = await _downstreamApi.GetForUserAsync<UserData>(
                "MyAPI",
                options => options.RelativePath = "api/profile");
        }
        catch (MicrosoftIdentityWebChallengeUserException)
        {
            // Handle incremental consent
            // User will be redirected to consent page
            throw;
        }
    }
}

Llame a Microsoft Graph

Para las llamadas de Microsoft Graph API, utilice el GraphServiceClient dedicado.

Instalación de paquetes

Instale el paquete de Microsoft Graph para Microsoft. Identity.Web.

dotnet add package Microsoft.Identity.Web.GraphServiceClient

Configure el cliente de Graph en el código de inicio.

// Startup configuration
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddMicrosoftGraph(options =>
    {
        options.Scopes = "user.read mail.read";
    })
    .AddInMemoryTokenCaches();

Llamada al Graph API

Inserte GraphServiceClient en el controlador para llamar a los puntos de conexión de Microsoft Graph.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Graph;

[Authorize]
{
    private readonly GraphServiceClient _graphClient;

    public HomeController(GraphServiceClient graphClient)
    {
        _graphClient = graphClient;
    }

    public async Task<IActionResult> Index()
    {
        // Get current user's profile
        var user = await _graphClient.Me.GetAsync();

        // Get user's emails
        var messages = await _graphClient.Me.Messages
            .GetAsync(config => config.QueryParameters.Top = 10);

        return View(new { User = user, Messages = messages });
    }
}

Obtenga más información sobre la integración de Microsoft Graph


Llamar a clientes de SDK de Azure

Para llamar a servicios Azure, use MicrosoftIdentityTokenCredential.

Instalación de paquetes

Instale los paquetes de SDK de Azure necesarios.

dotnet add package Microsoft.Identity.Web.Azure
dotnet add package Azure.Storage.Blobs

Registre la credencial del token de Microsoft Entra en el código de inicio.

using Microsoft.Identity.Web;

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

// Add Azure token credential
builder.Services.AddMicrosoftIdentityAzureTokenCredential();

Acceso a los servicios de Azure

Inserte la credencial del token y úsela con SDK de Azure clientes.

using Azure.Storage.Blobs;
using Microsoft.Identity.Web;

public class StorageController : Controller
{
    private readonly MicrosoftIdentityTokenCredential _credential;

    public StorageController(MicrosoftIdentityTokenCredential credential)
    {
        _credential = credential;
    }

    [Authorize]
    public async Task<IActionResult> ListBlobs()
    {
        var blobClient = new BlobServiceClient(
            new Uri("https://myaccount.blob.core.windows.net"),
            _credential);

        var container = blobClient.GetBlobContainerClient("mycontainer");
        var blobs = new List<string>();

        await foreach (var blob in container.GetBlobsAsync())
        {
            blobs.Add(blob.Name);
        }

        return View(blobs);
    }
}

Obtenga más información sobre la integración de SDK de Azure


Llamada a API personalizadas con IDownstreamApi

Para sus propias API REST, IDownstreamApi proporciona un enfoque sencillo y basado en la configuración.

Configuración de la API

Configure la configuración de la API descendente en appsettings.json.

{
  "DownstreamApis": {
    "MyAPI": {
      "BaseUrl": "https://myapi.example.com",
      "Scopes": ["api://my-api-id/access_as_user"],
      "RequestAppToken": false
    }
  }
}

Envío de solicitudes GET

Recupere datos de la API descendente con parámetros de consulta opcionales.

// Simple GET
var data = await _downstreamApi.GetForUserAsync<MyData>(
    "MyAPI",
    options => options.RelativePath = "api/resource");

// GET with query parameters
var results = await _downstreamApi.GetForUserAsync<SearchResults>(
    "MyAPI",
    options =>
    {
        options.RelativePath = "api/search";
        options.QueryParameters = new Dictionary<string, string>
        {
            ["query"] = "test",
            ["limit"] = "10"
        };
    });

Envío de solicitudes POST

Cree un nuevo recurso en la API de bajada publicando un cuerpo de solicitud.

var newItem = new CreateItemRequest
{
    Name = "New Item",
    Description = "Item description"
};

var created = await _downstreamApi.PostForUserAsync<CreateItemRequest, CreatedItem>(
    "MyAPI",
    newItem,
    options => options.RelativePath = "api/items");

Envío de solicitudes PUT y DELETE

Actualice o elimine recursos en la API de bajada.

// PUT request
var updated = await _downstreamApi.PutForUserAsync<UpdateRequest, UpdatedItem>(
    "MyAPI",
    updateData,
    options => options.RelativePath = "api/items/123");

// DELETE request
await _downstreamApi.DeleteForUserAsync(
    "MyAPI",
    null,
    options => options.RelativePath = "api/items/123");

Más información sobre las llamadas API personalizadas


Usar IAuthorizationHeaderProvider (avanzado)

Para un control máximo sobre las solicitudes HTTP, use IAuthorizationHeaderProvider.

Registro del cliente HTTP

Registre un cliente HTTP con nombre para la API de nivel inferior.

builder.Services.AddHttpClient("MyAPI", client =>
{
    client.BaseAddress = new Uri("https://myapi.example.com");
});

Creación de solicitudes HTTP personalizadas

Compile y envíe solicitudes HTTP con encabezados personalizados y autorización.

using Microsoft.Identity.Abstractions;

public class CustomApiService
{
    private readonly IAuthorizationHeaderProvider _authProvider;
    private readonly IHttpClientFactory _httpClientFactory;

    public CustomApiService(
        IAuthorizationHeaderProvider authProvider,
        IHttpClientFactory httpClientFactory)
    {
        _authProvider = authProvider;
        _httpClientFactory = httpClientFactory;
    }

    public async Task<MyData> GetDataAsync()
    {
        // Get authorization header
        var authHeader = await _authProvider.CreateAuthorizationHeaderForUserAsync(
            new[] { "api://my-api-id/access_as_user" });

        // Create HTTP request with custom logic
        var client = _httpClientFactory.CreateClient("MyAPI");
        var request = new HttpRequestMessage(HttpMethod.Get, "api/resource");
        request.Headers.Add("Authorization", authHeader);
        request.Headers.Add("X-Custom-Header", "custom-value");

        var response = await client.SendAsync(request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<MyData>();
    }
}

Más información sobre la lógica HTTP personalizada


Al llamar a las API de bajada, es posible que la aplicación tenga que controlar escenarios en los que se requiera la interacción del usuario. Esto sucede en tres escenarios principales:

  1. Consentimiento incremental : solicitar permisos adicionales más allá de lo que se concedió inicialmente
  2. Acceso condicional : cumplir los requisitos de seguridad como MFA, cumplimiento de dispositivos o directivas de ubicación
  3. Expulsión de la caché de tokens: repoblar la caché de tokens después de reiniciar la aplicación o de que expire la caché

Microsoft. Identity.Web proporciona control automático de estos escenarios con un código mínimo necesario.

Descripción del flujo

Cuándo Microsoft. Identity.Web detecta que se necesita interacción del usuario, genera una MicrosoftIdentityWebChallengeUserException. El marco lo controla automáticamente a través del [AuthorizeForScopes] atributo o el MicrosoftIdentityConsentAndConditionalAccessHandler servicio (para Blazor), que:

  1. Redirige al usuario a Microsoft Entra ID para la autenticación o consentimiento
  2. Conserva la dirección URL de solicitud original.
  3. Devuelve el usuario a su destino previsto después de completar el flujo.
  4. Almacena en caché los tokens recién adquiridos.

Revisión de los requisitos previos

Para habilitar el control automático del consentimiento, asegúrese de que Program.cs incluye la siguiente configuración.

builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd")
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi("MyAPI", builder.Configuration.GetSection("MyAPI"))
    .AddInMemoryTokenCaches();

// For MVC applications - enables the account controller
builder.Services.AddControllersWithViews()
    .AddMicrosoftIdentityUI();

// Ensure routes are mapped
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers(); // Required for AccountController

Aplicar [AuthorizeForScopes] en controladores MVC

El atributo [AuthorizeForScopes], establecido en controladores o acciones de controlador, gestiona MicrosoftIdentityWebChallengeUserException automáticamente desafiando al usuario cuando se necesitan permisos adicionales.

Declarar ámbitos insertados

Especifique los ámbitos necesarios directamente en el atributo .

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

[Authorize]
[AuthorizeForScopes(Scopes = new[] { "user.read" })]
public class ProfileController : Controller
{
    private readonly IDownstreamApi _downstreamApi;

    public ProfileController(IDownstreamApi downstreamApi)
    {
        _downstreamApi = downstreamApi;
    }

    public async Task<IActionResult> Index()
    {
        // AuthorizeForScopes automatically handles consent challenges
        var userData = await _downstreamApi.GetForUserAsync<UserData>(
            "MyAPI",
            options => options.RelativePath = "api/profile");

        return View(userData);
    }

    // Different action requires additional scopes
    [AuthorizeForScopes(Scopes = new[] { "user.read", "mail.read" })]
    public async Task<IActionResult> Emails()
    {
        var emails = await _downstreamApi.GetForUserAsync<EmailList>(
            "GraphAPI",
            options => options.RelativePath = "me/messages");

        return View(emails);
    }
}

Configuración de ámbitos desde appsettings

Almacene alcances en appsettings.json para mejorar la capacidad de mantenimiento.

appsettings.json:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "common",
    "ClientId": "[Your-Client-ID]",
    "ClientCredentials": [
      {
        "SourceType": "ClientSecret",
        "ClientSecret": "[Your-Client-Secret]"
      }
    ]
  },
  "DownstreamApis": {
    "TodoList": {
      "BaseUrl": "https://localhost:5001",
      "Scopes": [ "api://[API-Client-ID]/access_as_user" ]
    },
    "GraphAPI": {
      "BaseUrl": "https://graph.microsoft.com/v1.0",
      "Scopes": [ "https://graph.microsoft.com/Mail.Read", "https://graph.microsoft.com/Mail.Send" ]
    }
  }
}

Controlador:

[Authorize]
[AuthorizeForScopes(ScopeKeySection = "DownstreamApis:TodoList:Scopes:0")]
public class TodoListController : Controller
{
    private readonly IDownstreamApi _downstreamApi;

    public TodoListController(IDownstreamApi downstreamApi)
    {
        _downstreamApi = downstreamApi;
    }

    public async Task<IActionResult> Index()
    {
        var todos = await _downstreamApi.GetForUserAsync<IEnumerable<TodoItem>>(
            "TodoList",
            options => options.RelativePath = "api/todolist");

        return View(todos);
    }

    [AuthorizeForScopes(ScopeKeySection = "DownstreamApis:GraphAPI:Scopes:0")]
    public async Task<IActionResult> EmailTodos()
    {
        // If user hasn't consented to Mail.Send, they'll be prompted
        await _downstreamApi.PostForUserAsync<EmailMessage, object>(
            "GraphAPI",
            new EmailMessage { /* ... */ },
            options => options.RelativePath = "me/sendMail");

        return RedirectToAction("Index");
    }
}

Configuración de Id. externa de Microsoft Entra con flujos de usuario

Para las aplicaciones con ID Externo (B2C) que tienen varios flujos de usuario, especifique el flujo de usuario en el atributo.

[Authorize]
public class AccountController : Controller
{
    private const string SignUpSignInFlow = "b2c_1_susi";
    private const string EditProfileFlow = "b2c_1_edit_profile";
    private const string ResetPasswordFlow = "b2c_1_reset";

    [AuthorizeForScopes(
        ScopeKeySection = "DownstreamApis:TodoList:Scopes:0",
        UserFlow = SignUpSignInFlow)]
    public async Task<IActionResult> Index()
    {
        var data = await _downstreamApi.GetForUserAsync<UserData>(
            "TodoList",
            options => options.RelativePath = "api/data");

        return View(data);
    }

    [AuthorizeForScopes(
        Scopes = new[] { "openid", "offline_access" },
        UserFlow = EditProfileFlow)]
    public async Task<IActionResult> EditProfile()
    {
        // This triggers the B2C edit profile flow
        return RedirectToAction("Index");
    }
}

Aplicar [AuthorizeForScopes] en Razor Pages

Aplique [AuthorizeForScopes] a la clase de modelo de página:

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

[Authorize]
[AuthorizeForScopes(ScopeKeySection = "DownstreamApis:MyAPI:Scopes:0")]
public class IndexModel : PageModel
{
    private readonly IDownstreamApi _downstreamApi;

    public UserData UserData { get; set; }

    public IndexModel(IDownstreamApi downstreamApi)
    {
        _downstreamApi = downstreamApi;
    }

    public async Task OnGetAsync()
    {
        // Automatically handles consent challenges
        UserData = await _downstreamApi.GetForUserAsync<UserData>(
            "MyAPI",
            options => options.RelativePath = "api/profile");
    }
}

Las aplicaciones blazor Server requieren un control explícito de excepciones mediante el MicrosoftIdentityConsentAndConditionalAccessHandler servicio.

Configuración de Program.cs

Registre el controlador de consentimiento para Blazor Server en el código de inicio.

builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd")
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApis("TodoList", builder.Configuration.GetSection("DownstreamApis"))
    .AddInMemoryTokenCaches();

// Register the consent handler for Blazor
builder.Services.AddServerSideBlazor()
    .AddMicrosoftIdentityConsentHandler();

Creación del componente Blazor

Envuelva las llamadas a la API en bloques try-catch y use ConsentHandler.HandleException() para gestionar los desafíos de consentimiento.

@page "/todolist"
@using Microsoft.Identity.Web
@using Microsoft.Identity.Abstractions
@using MyApp.Models

@inject MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler
@inject IDownstreamApi DownstreamApi

<h3>My Todo List</h3>

@if (todos == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <ul>
        @foreach (var todo in todos)
        {
            <li>@todo.Title</li>
        }
    </ul>
}

@code {
    private IEnumerable<TodoItem> todos;

    protected override async Task OnInitializedAsync()
    {
        await LoadTodosAsync();
    }

    [AuthorizeForScopes(ScopeKeySection = "DownstreamApis:TodoList:Scopes:0")]
    private async Task LoadTodosAsync()
    {
        try
        {
            todos = await DownstreamApi.GetForUserAsync<IEnumerable<TodoItem>>(
                "TodoList",
                options => options.RelativePath = "api/todolist");
        }
        catch (Exception ex)
        {
            // Handles MicrosoftIdentityWebChallengeUserException
            // and initiates user consent/authentication flow
            ConsentHandler.HandleException(ex);
        }
    }

    private async Task AddTodoAsync(string title)
    {
        try
        {
            await DownstreamApi.PostForUserAsync<TodoItem, TodoItem>(
                "TodoList",
                new TodoItem { Title = title },
                options => options.RelativePath = "api/todolist");

            await LoadTodosAsync();
        }
        catch (Exception ex)
        {
            ConsentHandler.HandleException(ex);
        }
    }
}

Controlar excepciones manualmente (avanzado)

Si necesita lógica de flujo de consentimiento personalizada, controle MicrosoftIdentityWebChallengeUserException explícitamente:

[Authorize]
public class AdvancedController : Controller
{
    private readonly IDownstreamApi _downstreamApi;
    private readonly ILogger<AdvancedController> _logger;

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

    public async Task<IActionResult> SendEmail()
    {
        try
        {
            await _downstreamApi.PostForUserAsync<EmailMessage, object>(
                "GraphAPI",
                new EmailMessage
                {
                    Subject = "Test",
                    Body = "Test message"
                },
                options => options.RelativePath = "me/sendMail");

            return RedirectToAction("Success");
        }
        catch (MicrosoftIdentityWebChallengeUserException ex)
        {
            // Log the consent requirement
            _logger.LogWarning(
                "Consent required for scopes: {Scopes}. Challenging user.",
                string.Join(", ", ex.Scopes));

            // Custom properties for redirect
            var properties = new AuthenticationProperties
            {
                RedirectUri = Url.Action("SendEmail", "Advanced"),
            };

            // Add custom state if needed
            properties.Items["consent_attempt"] = "1";

            return Challenge(properties, OpenIdConnectDefaults.AuthenticationScheme);
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Failed to send email");
            return View("Error");
        }
    }
}

Control de escenarios de acceso condicional

Las directivas de acceso condicional pueden requerir factores de autenticación adicionales. La gestión es idéntica al consentimiento incremental.

[Authorize]
[AuthorizeForScopes(ScopeKeySection = "DownstreamApis:SecureAPI:Scopes:0")]
public class SecureDataController : Controller
{
    private readonly IDownstreamApi _downstreamApi;

    public SecureDataController(IDownstreamApi downstreamApi)
    {
        _downstreamApi = downstreamApi;
    }

    public async Task<IActionResult> Index()
    {
        // If conditional access requires MFA, AuthorizeForScopes
        // automatically challenges the user
        var sensitiveData = await _downstreamApi.GetForUserAsync<SensitiveData>(
            "SecureAPI",
            options => options.RelativePath = "api/sensitive");

        return View(sensitiveData);
    }
}

Desencadenadores de acceso condicional comunes:

  • Autenticación multifactor (MFA)
  • Requisito de dispositivo compatible
  • Ubicación de red de confianza
  • Aceptación de los términos de uso
  • Requisito de cambio de contraseña

Seguimiento de los procedimientos recomendados

Aplique estas recomendaciones al implementar el consentimiento y el control del acceso condicional.

Uso [AuthorizeForScopes] - Enfoque más sencillo para controladores MVC y Razor Pages

Almacenar ámbitos en la configuración - Utilice ScopeKeySection = "DownstreamApis:ApiName:Scopes:0" para hacer referencia a los ámbitos en appsettings.json

Aplicar en el nivel de controlador : establecimiento de ámbitos predeterminados en el controlador, invalidación en acciones específicas

Control de excepciones en Blazor - siempre encapsule las llamadas a la API con try-catch y use ConsentHandler.HandleException()

Permitir que se vuelvan a producir excepciones : si detecta MicrosoftIdentityWebChallengeUserException, vuelva a iniciarla para [AuthorizeForScopes] que pueda procesarla.

Prueba del acceso condicional : compruebe que la aplicación controla MFA y otras directivas de CA correctamente.

No suprimir excepciones - Capturar sin relanzar interrumpe el flujo de consentimiento.

No almacenar en caché las respuestas indefinidamente : los tokens expiran; diseño para volver a autenticar


Permisos estáticos (consentimiento del administrador)

Todos los permisos se solicitan durante el registro de aplicaciones y los da su consentimiento un administrador de inquilinos:

Ventajas:

  • Los usuarios nunca ven las solicitudes de consentimiento
  • Necesario para aplicaciones de Microsoft de primera parte
  • Experiencia de usuario más sencilla

Desventajas:

  • Requiere la participación del administrador de inquilinos
  • Con privilegios excesivos desde el principio
  • Menos flexible para escenarios multiinquilino

Configuration:

// Request all pre-approved scopes for Microsoft Graph
var scopes = new[] { "https://graph.microsoft.com/.default" };

var userData = await _downstreamApi.GetForUserAsync<UserData>(
    "GraphAPI",
    options =>
    {
        options.RelativePath = "me";
        options.Scopes = scopes; // Use .default scope
    });

Consentimiento incremental (dinámico)

Los permisos se solicitan según sea necesario durante el tiempo de ejecución:

Ventajas:

  • Mejor seguridad (principio de privilegios mínimos)
  • Los usuarios dan su consentimiento a lo que realmente usan
  • Funciona para aplicaciones multiusuario

Desventajas:

  • Es posible que los usuarios experimenten interrupciones debido a solicitudes de consentimiento
  • Requiere manejo MicrosoftIdentityWebChallengeUserException

Recomendación: Uso del consentimiento incremental para aplicaciones multiinquilino; usar permisos estáticos para aplicaciones empresariales de primera entidad en las que se garantiza el consentimiento del administrador


Configuración del almacenamiento en caché de tokens

Microsoft. Identity.Web almacena en caché tokens para mejorar el rendimiento y reducir las llamadas a Microsoft Entra.

Uso de la caché en memoria (valor predeterminado)

Agregue una caché de tokens en memoria para escenarios de desarrollo o de servidor único.

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches(); // In-memory cache

Usar para:

  • Desarrollo
  • Implementaciones de servidor único
  • Base de usuarios pequeños

Limitaciones:

  • No se comparte entre instancias
  • Perdido en el reinicio de la aplicación
  • El consumo de memoria crece con los usuarios

Configure una caché distribuida como Redis o SQL Server para implementaciones de producción.

// Install: Microsoft.Identity.Web.TokenCache

// Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration["Redis:ConnectionString"];
    options.InstanceName = "MyApp_";
});

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDistributedTokenCaches();

// SQL Server
builder.Services.AddDistributedSqlServerCache(options =>
{
    options.ConnectionString = builder.Configuration["SqlCache:ConnectionString"];
    options.SchemaName = "dbo";
    options.TableName = "TokenCache";
});

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDistributedTokenCaches();

Usar para:

  • Implementaciones de varios servidores (con equilibrio de carga)
  • Escenarios de alta disponibilidad
  • Base de usuarios de gran tamaño
  • Caché persistente entre reinicios

Manejo de fallos en la adquisición de tokens

Detectar excepciones comunes

En el código siguiente se muestra cómo detectar y controlar las excepciones de adquisición de tokens más comunes.

try
{
    var data = await _downstreamApi.GetForUserAsync<MyData>(
        "MyAPI",
        options => options.RelativePath = "api/resource");
}
catch (MicrosoftIdentityWebChallengeUserException ex)
{
    // User needs to consent or reauthenticate
    _logger.LogWarning($"User consent required: {ex.Message}");
    return Challenge(new AuthenticationProperties { RedirectUri = Request.Path });
}
catch (MsalUiRequiredException ex)
{
    // User interaction required (sign-in again, MFA, etc.)
    _logger.LogWarning($"User interaction required: {ex.Message}");
    return Challenge(OpenIdConnectDefaults.AuthenticationScheme);
}
catch (MsalServiceException ex)
{
    // Service error (Microsoft Entra ID unavailable, etc.)
    _logger.LogError(ex, "Microsoft Entra ID service error");
    return StatusCode(503, "Authentication service temporarily unavailable");
}
catch (HttpRequestException ex)
{
    // Downstream API unreachable
    _logger.LogError(ex, "Downstream API call failed");
    return StatusCode(503, "Downstream service unavailable");
}

Implementar una degradación gradual

Cargue datos opcionales de las API de bajada y vuelva a los valores predeterminados cuando se produzca un error en las llamadas.

public async Task<IActionResult> Dashboard()
{
    var model = new DashboardModel();

    // Try to load optional data from downstream API
    try
    {
        model.EnrichedData = await _downstreamApi.GetForUserAsync<EnrichedData>(
            "MyAPI",
            options => options.RelativePath = "api/enriched");
    }
    catch (Exception ex)
    {
        _logger.LogWarning(ex, "Failed to load enriched data, using defaults");
        model.EnrichedData = new EnrichedData { /* defaults */ };
    }

    return View(model);
}

Implementación de OWIN (.NET Framework)

Para las aplicaciones web basadas en OWIN en .NET Framework, siga estos pasos.

1. Instalar paquetes

Instale los paquetes NuGet necesarios.

Install-Package Microsoft.Identity.Web.OWIN
Install-Package Microsoft.Owin.Host.SystemWeb

2. Configurar inicio

Configura la autenticación y adquisición de tokens de Microsoft Entra en la clase de inicio de OWIN.

using Microsoft.Identity.Web;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Owin;

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

        app.UseCookieAuthentication(new CookieAuthenticationOptions());

        app.AddMicrosoftIdentityWebApp(
            Configuration,
            configSectionName: "AzureAd",
            openIdConnectScheme: "OpenIdConnect",
            cookieScheme: CookieAuthenticationDefaults.AuthenticationType,
            subscribeToOpenIdConnectMiddlewareDiagnosticsEvents: true);

        app.EnableTokenAcquisitionToCallDownstreamApi();
        app.AddDistributedTokenCaches();
    }
}

3. Llamada a la API de bajada

Adquiera un token y llame a la API de bajada desde un controlador MVC.

using Microsoft.Identity.Web;
using System.Threading.Tasks;
using System.Web.Mvc;

[Authorize]
public class ProfileController : Controller
{
    public async Task<ActionResult> Index()
    {
        var downstreamApi = TokenAcquirerFactory.GetDefaultInstance()
            .GetTokenAcquirer()
            .GetDownstreamApi();

        var userData = await downstreamApi.GetForUserAsync<UserData>(
            "MyAPI",
            options => options.RelativePath = "api/profile");

        return View(userData);
    }
}

Note: la compatibilidad con OWIN tiene algunas diferencias de ASP.NET Core. Consulte la documentación de OWIN para obtener más información.


Seguir los procedimientos recomendados de seguridad

Administrar alcances

Aplique el principio de privilegios mínimos al solicitar permisos de API.

Sí:

  • Solicitar solo los ámbitos que necesita
  • Uso del consentimiento incremental para características avanzadas
  • Documentar los ámbitos necesarios en tu aplicación

No:

  • Solicitar ámbitos innecesarios por adelantado
  • Solicitar ámbitos solo de administrador sin justificación
  • Supongamos que se concederán todos los ámbitos

Maneja tokens de forma segura

Siga estas instrucciones para proteger los tokens de acceso en la aplicación.

Sí:

  • Deje que Microsoft.Identity.Web gestione los tokens
  • Uso de la caché distribuida en producción
  • Gestionar adecuadamente los fallos en la adquisición de tokens

No:

  • Almacene los tokens usted mismo
  • Tokens de acceso de registro
  • Envío de tokens al código del lado cliente

Manejo de errores

Implemente un control sólido de errores para errores de autenticación y llamadas API.

Sí:

  • Detectar y controlar excepciones de consentimiento
  • Proporcionar mensajes de error claros a los usuarios
  • Registro de errores para la depuración

No:

  • Exponer errores de token a los usuarios
  • Error silencioso de llamadas API
  • Omitir excepciones de autenticación

Solucionar problemas comunes

Revise estas soluciones para ver los errores de autenticación detectados con frecuencia.

Problema: "AADSTS65001: el usuario o administrador no ha consentido"

Causa: El usuario no ha consentido los permisos necesarios.

Solution:

catch (MicrosoftIdentityWebChallengeUserException ex)
{
    // Redirect to consent page
    return Challenge(
        new AuthenticationProperties { RedirectUri = Request.Path },
        OpenIdConnectDefaults.AuthenticationScheme);
}

Problema: "AADSTS50076: autenticación multifactor requerida"

Causa: El usuario debe completar MFA.

Solution:

catch (MsalUiRequiredException)
{
    // Redirect user to sign in with MFA
    return Challenge(OpenIdConnectDefaults.AuthenticationScheme);
}

Problema: Los tokens no persisten en los reinicios de la aplicación

Causa: Uso de caché en memoria.

Solution: Cambiar a caché distribuida (Redis, SQL Server o Cosmos DB).

Problema: 401 No autorizado desde la API de nivel inferior

Causas posibles:

  • Ámbitos incorrectos solicitados
  • Permiso de API no concedido en el registro de aplicaciones
  • Token expirado

Solution:

  1. Verificar que los ámbitos en appsettings.json coincidan con los requisitos de la API
  2. Comprobación de que el registro de aplicaciones tiene permisos de API
  3. Asegúrese de que los tokens se almacenan en caché y se actualizan

Para obtener diagnósticos detallados: Consulte Guía de registro y diagnóstico para ver los identificadores de correlación, la depuración de caché de tokens y patrones de solución de problemas completos.


Optimización del rendimiento

Planeamiento de la estrategia de almacenamiento en caché de tokens

Seleccione una estrategia de almacenamiento en caché que coincida con la topología de implementación.

  • Uso de la caché distribuida para implementaciones de varios servidores
  • Configuración de la expiración de caché adecuada
  • Supervisión del rendimiento de la memoria caché

Minimizar las solicitudes de token

Microsoft. Identity.Web almacena en caché los tokens automáticamente. Ambas llamadas en el ejemplo siguiente reutilizan el mismo token almacenado en caché.

// Bad: Multiple token acquisitions
var profile = await _downstreamApi.GetForUserAsync<Profile>(
    "API",
    options => options.RelativePath = "profile");
var settings = await _downstreamApi.GetForUserAsync<Settings>(
    "API",
    options => options.RelativePath = "settings");

// Good: Single token, multiple calls (token is cached)
// Both calls use the same cached token
var profile = await _downstreamApi.GetForUserAsync<Profile>(
    "API",
    options => options.RelativePath = "profile");
var settings = await _downstreamApi.GetForUserAsync<Settings>(
    "API",
    options => options.RelativePath = "settings");

Realización de llamadas API paralelas

Llame simultáneamente a varias API de bajada para reducir la latencia general.

// Call multiple APIs in parallel
var profileTask = _downstreamApi.GetForUserAsync<Profile>(
    "API1",
    options => options.RelativePath = "profile");
var settingsTask = _downstreamApi.GetForUserAsync<Settings>(
    "API2",
    options => options.RelativePath = "settings");

await Task.WhenAll(profileTask, settingsTask);

var profile = profileTask.Result;
var settings = settingsTask.Result;

Busque instrucciones adicionales para escenarios relacionados.