Integre MSAL.NET con Microsoft. Identity.Web en .NET Framework

En esta guía se muestra cómo usar el almacenamiento en caché de tokens de Microsoft.Identity.Web y los conjuntos de certificados con MSAL.NET en .NET Framework, .NET Standard 2.0 y aplicaciones clásicas de .NET (.NET 4.7.2+).

Descripción de la información general

A partir de Microsoft.Identity.Web 1.17+, puede usar los paquetes de utilidad de Microsoft.Identity.Web con MSAL.NET en entornos no ASP.NET Core.

Identificación de las ventajas del paquete

Feature Ventajas
Serialización de caché de tokens Adaptadores de caché reutilizables para en memoria, SQL Server, Redis, Cosmos DB, PostgreSQL
Asistentes de Certificados Carga simplificada de certificados desde keyVault, sistema de archivos o almacenes de certificados
Extensiones de reclamaciones Métodos útiles para la manipulación de ClaimsPrincipal
.NET Standard 2.0 Compatible con .NET Framework 4.7.2 y versiones posteriores, .NET Core y .NET 5+
Dependencias mínimas Paquetes de destino sin dependencias de ASP.NET Core

Revisión de escenarios admitidos

Los siguientes escenarios se admiten con los paquetes de utilidad de destino.

  • .NET Framework Console Applications (escenarios de servicios)
  • Desktop Applications (.NET Framework)
  • Worker Services (.NET Framework)
  • bibliotecas de .NET Standard 2.0 (compatibilidad multiplataforma)
  • Aplicaciones MSAL.NET no web

Nota:

Para aplicaciones ASP.NET MVC/Web API, consulte OWIN Integration en su lugar.


Selección de paquetes

Elija el paquete que coincida con su escenario.

Identificación de paquetes principales para MSAL.NET

Package propósito Dependencias Objetivo de .NET
Microsoft. Identity.Web.TokenCache Serializadores de caché de tokens, ClaimsPrincipal extensiones Mínimo .NET Standard 2.0
Microsoft. Identity.Web.Certificate Utilidades de carga de certificados Mínimo .NET Standard 2.0

Instalación de paquetes

Use uno de los métodos siguientes para agregar los paquetes al proyecto.

Administrador de paquetes Console:

# Token cache serialization
Install-Package Microsoft.Identity.Web.TokenCache

# Certificate management
Install-Package Microsoft.Identity.Web.Certificate

.NET CLI:

dotnet add package Microsoft.Identity.Web.TokenCache
dotnet add package Microsoft.Identity.Web.Certificate

Descripción de las limitaciones del paquete principal

El paquete principal Microsoft.Identity.Web incluye ASP.NET Core dependencias (Microsoft.AspNetCore.*), que:

  • No son compatibles con ASP.NET Framework
  • Aumentar el tamaño del paquete innecesariamente
  • Creación de conflictos de dependencias

Use paquetes de destino en su lugar para escenarios .NET Framework y .NET Estándar.


Configuración de la serialización de caché de tokens

Descripción de los adaptadores de caché de tokens

Microsoft. Identity.Web proporciona adaptadores de caché de tokens que funcionan sin problemas con MSAL.NET IConfidentialClientApplication.

Creación de un cliente confidencial con caché de tokens

En el ejemplo siguiente se crea una aplicación cliente confidencial y se adjunta una caché de tokens en memoria.

using Microsoft.Identity.Client;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.TokenCacheProviders;

public class MsalAppBuilder
{
    private static IConfidentialClientApplication _app;

    public static IConfidentialClientApplication BuildConfidentialClientApplication()
    {
        if (_app == null)
        {
            string clientId = ConfigurationManager.AppSettings["AzureAd:ClientId"];
            string clientSecret = ConfigurationManager.AppSettings["AzureAd:ClientSecret"];
            string tenantId = ConfigurationManager.AppSettings["AzureAd:TenantId"];

            // Create the confidential client application
            _app = ConfidentialClientApplicationBuilder.Create(clientId)
                .WithClientSecret(clientSecret)
                .WithTenantId(tenantId)
                .WithAuthority(AzureCloudInstance.AzurePublic, tenantId)
                .Build();

            // Add token cache serialization (choose one option below)
            _app.AddInMemoryTokenCache();
        }

        return _app;
    }
}

Elegir opciones de caché de tokens

Seleccione el proveedor de caché que mejor se adapte a su escenario de implementación.

Configuración de la caché de tokens en memoria

En el ejemplo siguiente se agrega una caché en memoria sencilla:

using Microsoft.Identity.Web.TokenCacheProviders;

_app.AddInMemoryTokenCache();

Caché en memoria con límites de tamaño (Microsoft. Identity.Web 1.20+):

using Microsoft.Extensions.Caching.Memory;

_app.AddInMemoryTokenCache(services =>
{
    // Configure memory cache options
    services.Configure<MemoryCacheOptions>(options =>
    {
        options.SizeLimit = 5000000;  // 5 MB limit
    });
});

Características:

  • Acceso rápido
  • Sin dependencias externas
  • No se comparte entre procesos
  • Perdido en el reinicio de la aplicación

Caso de uso: Aplicaciones de consola de instancia única, aplicaciones de escritorio


Configuración de la caché de tokens en memoria distribuida

Use el código siguiente para agregar una caché distribuida en memoria para entornos de varias instancias:

_app.AddDistributedTokenCaches(services =>
{
    // Requires: Microsoft.Extensions.Caching.Memory (NuGet)
    services.AddDistributedMemoryCache();
});

Características:

  • Compartido entre instancias de aplicación
  • Mejor para escenarios con equilibrio de carga
  • Requiere un paquete NuGet adicional
  • Todavía perdido en el reinicio de la aplicación

Caso de uso: Servicios de varias instancias con la readquisición de tokens aceptable


Configuración de la caché de tokens de SQL Server

Use el código siguiente para agregar una caché de SQL Server distribuida persistente:

using Microsoft.Extensions.Caching.SqlServer;

_app.AddDistributedTokenCaches(services =>
{
    // Requires: Microsoft.Extensions.Caching.SqlServer (NuGet)
    services.AddDistributedSqlServerCache(options =>
    {
        options.ConnectionString = ConfigurationManager.ConnectionStrings["TokenCache"].ConnectionString;
        options.SchemaName = "dbo";
        options.TableName = "TokenCache";

        // IMPORTANT: Set expiration above token lifetime
        // Access tokens typically expire after 1 hour
        options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);
    });
});

Ejecute el siguiente código SQL para crear la tabla de caché necesaria:

-- Create the cache table
CREATE TABLE [dbo].[TokenCache] (
    [Id] NVARCHAR(449) NOT NULL,
    [Value] VARBINARY(MAX) NOT NULL,
    [ExpiresAtTime] DATETIMEOFFSET NOT NULL,
    [SlidingExpirationInSeconds] BIGINT NULL,
    [AbsoluteExpiration] DATETIMEOFFSET NULL,
    PRIMARY KEY ([Id])
);

-- Create index for performance
CREATE INDEX [Index_ExpiresAtTime] ON [dbo].[TokenCache] ([ExpiresAtTime]);

Características:

  • Persistente entre reinicios
  • Compartido entre varias instancias
  • Confiable y escalable
  • Requiere SQL Server configuración

Caso de uso: Servicios de daemon en producción, tareas programadas, trabajos de múltiples instancias


Configuración de la caché de tokens de Redis

Use el código siguiente para agregar una caché distribuida de Redis de alto rendimiento:

using StackExchange.Redis;
using Microsoft.Extensions.Caching.StackExchangeRedis;

_app.AddDistributedTokenCaches(services =>
{
    // Requires: Microsoft.Extensions.Caching.StackExchangeRedis (NuGet)
    services.AddStackExchangeRedisCache(options =>
    {
        options.Configuration = ConfigurationManager.AppSettings["Redis:ConnectionString"];
        options.InstanceName = "TokenCache_";
    });
});

En el ejemplo siguiente se muestra una configuración de Redis lista para producción:

services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = ConfigurationManager.AppSettings["Redis:ConnectionString"];
    options.InstanceName = "MyDaemonApp_";

    // Optional: Configure Redis options
    options.ConfigurationOptions = new ConfigurationOptions
    {
        AbortOnConnectFail = false,
        ConnectTimeout = 5000,
        SyncTimeout = 5000
    };
});

Características:

  • Extremadamente rápido
  • Compartido entre instancias
  • Persistente (con la persistencia de Redis habilitada)
  • Requiere el servidor de Redis

Caso de uso: Aplicaciones de fondo de alto rendimiento, sistemas distribuidos, microservicios


Configuración de la caché de tokens de Cosmos DB

Use el código siguiente para agregar una memoria caché de Cosmos DB distribuida globalmente:

using Microsoft.Extensions.Caching.Cosmos;

_app.AddDistributedTokenCaches(services =>
{
    // Requires: Microsoft.Extensions.Caching.Cosmos (preview)
    services.AddCosmosCache(options =>
    {
        options.ContainerName = "TokenCache";
        options.DatabaseName = "IdentityCache";
        options.ClientBuilder = new CosmosClientBuilder(
            ConfigurationManager.AppSettings["CosmosConnectionString"]);
        options.CreateIfNotExists = true;
    });
});

Características:

  • Distribuido globalmente
  • Alta disponibilidad
  • Escalado automático
  • Mayor latencia que Redis
  • Mayor costo

Caso de uso: Servicios de daemon globales, aplicaciones con distribución geográfica


Configuración de la caché de tokens de PostgreSQL

Use el código siguiente para agregar una caché distribuida de PostgreSQL:

_app.AddDistributedTokenCaches(services =>
{
    // Requires: Microsoft.Extensions.Caching.Postgres (NuGet)
    services.AddDistributedPostgresCache(options =>
    {
        options.ConnectionString = ConfigurationManager.ConnectionStrings["PostgresCache"].ConnectionString;
        options.SchemaName = ConfigurationManager.AppSettings["PostgresCache:SchemaName"];
        options.TableName = ConfigurationManager.AppSettings["PostgresCache:TableName"];
        options.CreateIfNotExists = bool.Parse(
            ConfigurationManager.AppSettings["PostgresCache:CreateIfNotExists"] ?? "true");

        // Set expiration above token lifetime.
        // Access tokens typically expire after 1 hour.
        options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);
    });
});

Características:

  • Persistente entre reinicios
  • Compartido entre varias instancias
  • Semántica de SQL familiar
  • Funciona con Azure Database for PostgreSQL
  • Requiere un servidor PostgreSQL

Caso de uso: Aplicaciones que ya usan PostgreSQL como base de datos principal o servicios alojados en Azure que utilizan Azure Database for PostgreSQL


Creación de una aplicación daemon completa

En el ejemplo siguiente se muestra una aplicación de demonio completa que adquiere tokens mediante credenciales de cliente y una caché de tokens de SQL Server.

using Microsoft.Identity.Client;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.TokenCacheProviders;
using System;
using System.Threading.Tasks;

namespace DaemonApp
{
    class Program
    {
        private static IConfidentialClientApplication _app;

        static async Task Main(string[] args)
        {
            // Build confidential client with token cache
            _app = BuildConfidentialClient();

            // Acquire token for app-only access
            string[] scopes = new[] { "https://graph.microsoft.com/.default" };

            try
            {
                var result = await _app.AcquireTokenForClient(scopes)
                    .ExecuteAsync();

                Console.WriteLine($"Token acquired successfully!");
                Console.WriteLine($"Token source: {result.AuthenticationResultMetadata.TokenSource}");
                Console.WriteLine($"Expires on: {result.ExpiresOn}");

                // Use token to call API
                await CallProtectedApi(result.AccessToken);
            }
            catch (MsalServiceException ex)
            {
                Console.WriteLine($"Error acquiring token: {ex.ErrorCode}");
                Console.WriteLine($"CorrelationId: {ex.CorrelationId}");
            }
        }

        private static IConfidentialClientApplication BuildConfidentialClient()
        {
            var app = ConfidentialClientApplicationBuilder
                .Create(ConfigurationManager.AppSettings["ClientId"])
                .WithClientSecret(ConfigurationManager.AppSettings["ClientSecret"])
                .WithTenantId(ConfigurationManager.AppSettings["TenantId"])
                .Build();

            // Add SQL Server token cache for persistence
            app.AddDistributedTokenCaches(services =>
            {
                services.AddDistributedSqlServerCache(options =>
                {
                    options.ConnectionString = ConfigurationManager
                        .ConnectionStrings["TokenCache"].ConnectionString;
                    options.SchemaName = "dbo";
                    options.TableName = "TokenCache";
                    options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);
                });
            });

            return app;
        }

        private static async Task CallProtectedApi(string accessToken)
        {
            // Your API call logic
        }
    }
}

Administración de certificados

Comprender la carga de certificados

Microsoft. Identity.Web simplifica la carga de certificados desde varios orígenes para los flujos de credenciales de cliente.

Carga de certificados con DefaultCertificateLoader

En el ejemplo siguiente se muestra cómo cargar un certificado desde Azure Key Vault y crear una aplicación cliente confidencial.

using Microsoft.Identity.Web;
using Microsoft.Identity.Client;

public class CertificateHelper
{
    public static IConfidentialClientApplication CreateAppWithCertificate()
    {
        string clientId = ConfigurationManager.AppSettings["AzureAd:ClientId"];
        string tenantId = ConfigurationManager.AppSettings["AzureAd:TenantId"];

        // Define certificate source
        var certDescription = CertificateDescription.FromKeyVault(
            keyVaultUrl: "https://my-keyvault.vault.azure.net",
            keyVaultCertificateName: "MyCertificate"
        );

        // Load certificate
        ICertificateLoader certificateLoader = new DefaultCertificateLoader();
        certificateLoader.LoadIfNeeded(certDescription);

        // Create confidential client with certificate
        var app = ConfidentialClientApplicationBuilder.Create(clientId)
            .WithCertificate(certDescription.Certificate)
            .WithTenantId(tenantId)
            .Build();

        // Add token cache
        app.AddInMemoryTokenCache();

        return app;
    }
}

Elegir orígenes de certificados

Cargar desde Azure Key Vault

Cargue un certificado almacenado en Azure Key Vault especificando la dirección URL del almacén y el nombre del certificado.

var certDescription = CertificateDescription.FromKeyVault(
    keyVaultUrl: "https://my-keyvault.vault.azure.net",
    keyVaultCertificateName: "MyApplicationCert"
);

ICertificateLoader loader = new DefaultCertificateLoader();
loader.LoadIfNeeded(certDescription);

var app = ConfidentialClientApplicationBuilder.Create(clientId)
    .WithCertificate(certDescription.Certificate)
    .WithTenantId(tenantId)
    .Build();

Requisitos previos:

  • Identidad administrada o Principal de servicio con acceso a Key Vault
  • paquete NuGet Azure.Identity
  • El permiso de Key Vault: Get para certificados

Cargar desde el almacén de certificados

Cargue un certificado del almacén de certificados de Windows por nombre distintivo.

var certDescription = CertificateDescription.FromStoreWithDistinguishedName(
    distinguishedName: "CN=MyApp.contoso.com",
    storeName: StoreName.My,
    storeLocation: StoreLocation.CurrentUser
);

ICertificateLoader loader = new DefaultCertificateLoader();
loader.LoadIfNeeded(certDescription);

var app = ConfidentialClientApplicationBuilder.Create(clientId)
    .WithCertificate(certDescription.Certificate)
    .WithTenantId(tenantId)
    .Build();

También puede encontrar un certificado mediante huella digital:

var certDescription = CertificateDescription.FromStoreWithThumbprint(
    thumbprint: "ABCDEF1234567890ABCDEF1234567890ABCDEF12",
    storeName: StoreName.My,
    storeLocation: StoreLocation.LocalMachine
);

Cargar desde el sistema de archivos

Cargue un certificado desde un archivo PFX en el sistema de archivos local.

var certDescription = CertificateDescription.FromPath(
    path: @"C:\Certificates\MyAppCert.pfx",
    password: ConfigurationManager.AppSettings["Certificate:Password"]
);

ICertificateLoader loader = new DefaultCertificateLoader();
loader.LoadIfNeeded(certDescription);

var app = ConfidentialClientApplicationBuilder.Create(clientId)
    .WithCertificate(certDescription.Certificate)
    .WithTenantId(tenantId)
    .Build();

Nota de seguridad: Nunca codifique contraseñas de forma dura. Use la configuración segura.


Cargar desde una cadena codificada en Base64

Cargue un certificado de una cadena codificada en Base64 almacenada en la configuración.

string base64Cert = ConfigurationManager.AppSettings["Certificate:Base64"];

var certDescription = CertificateDescription.FromBase64Encoded(
    base64EncodedValue: base64Cert,
    password: ConfigurationManager.AppSettings["Certificate:Password"]  // Optional
);

ICertificateLoader loader = new DefaultCertificateLoader();
loader.LoadIfNeeded(certDescription);

Configuración de la carga de certificados desde App.config

Defina los ajustes del certificado en el archivo App.config y cárguelos en tiempo de ejecución.

App.config:

<appSettings>
  <add key="AzureAd:ClientId" value="your-client-id" />
  <add key="AzureAd:TenantId" value="your-tenant-id" />

  <!-- Option 1: KeyVault -->
  <add key="Certificate:SourceType" value="KeyVault" />
  <add key="Certificate:KeyVaultUrl" value="https://my-vault.vault.azure.net" />
  <add key="Certificate:KeyVaultCertificateName" value="MyCert" />

  <!-- Option 2: Store -->
  <!--
  <add key="Certificate:SourceType" value="StoreWithThumbprint" />
  <add key="Certificate:CertificateThumbprint" value="ABCD..." />
  <add key="Certificate:CertificateStorePath" value="CurrentUser/My" />
  -->
</appSettings>

<connectionStrings>
  <add name="TokenCache"
       connectionString="Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=TokenCache;Integrated Security=True;" />
</connectionStrings>

Use el siguiente método auxiliar para cargar el certificado en función de la configuración:

public static CertificateDescription GetCertificateFromConfig()
{
    string sourceType = ConfigurationManager.AppSettings["Certificate:SourceType"];

    return sourceType switch
    {
        "KeyVault" => CertificateDescription.FromKeyVault(
            ConfigurationManager.AppSettings["Certificate:KeyVaultUrl"],
            ConfigurationManager.AppSettings["Certificate:KeyVaultCertificateName"]
        ),

        "StoreWithThumbprint" => CertificateDescription.FromStoreWithThumbprint(
            ConfigurationManager.AppSettings["Certificate:CertificateThumbprint"],
            StoreName.My,
            StoreLocation.CurrentUser
        ),

        _ => throw new ConfigurationErrorsException("Invalid certificate source type")
    };
}

Exploración de aplicaciones de ejemplo

Revise estos ejemplos para ver las implementaciones de trabajo.

Revisar ejemplos oficiales de Microsoft

En la tabla siguiente se enumeran ejemplos oficiales que muestran el almacenamiento en caché de tokens y la carga de certificados.

Ejemplo Plataforma Descripción
ConfidentialClientTokenCache Consola (.NET Framework) Patrones de serialización de caché de tokens
active-directory-dotnetcore-daemon-v2 Consola (.NET Core) Carga de certificados desde Key Vault

Seguimiento de los procedimientos recomendados

Aplique estos patrones para crear aplicaciones confiables y seguras.

1. Use el patrón singleton para IConfidentialClientApplication:

Cree una sola instancia y reutilícela en la aplicación.

private static IConfidentialClientApplication _app;

public static IConfidentialClientApplication GetApp()
{
    if (_app == null)
    {
        _app = ConfidentialClientApplicationBuilder.Create(clientId)
            .WithClientSecret(clientSecret)
            .WithTenantId(tenantId)
            .Build();

        _app.AddDistributedTokenCaches(/* ... */);
    }

    return _app;
}

2. Establecer la expiración de caché de tokens adecuada:

Configure la expiración deslizante para que sea mayor que la duración del token y así evitar la readquisición innecesaria.

// Access tokens typically expire after 1 hour
// Set cache expiration ABOVE token lifetime
options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);

3. Use almacenamiento seguro de certificados:

Almacene certificados en Azure Key Vault o en un almacén de certificados protegido correctamente.

// Azure Key Vault (production)
var cert = CertificateDescription.FromKeyVault(keyVaultUrl, certName);

// Certificate store with proper permissions
var cert = CertificateDescription.FromStoreWithThumbprint(
    thumbprint, StoreName.My, StoreLocation.LocalMachine);

4. Implemente el control de errores adecuado:

Capture excepciones de MSAL y registre el identificador de correlación para solucionar problemas.

try
{
    var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
}
catch (MsalServiceException ex)
{
    logger.Error($"Token acquisition failed. CorrelationId: {ex.CorrelationId}, ErrorCode: {ex.ErrorCode}");
    throw;
}

5. Use una caché distribuida para producción:

Una caché distribuida comparte tokens entre instancias y persiste en los reinicios.

// Correct for daemon services
app.AddDistributedTokenCaches(services =>
{
    services.AddDistributedSqlServerCache(/* ... */);
});

Evitar errores comunes

1. No cree nuevas instancias IConfidentialClientApplication repetidamente:

// Wrong - creates new instance every time
public void AcquireToken()
{
    var app = ConfidentialClientApplicationBuilder.Create(clientId).Build();
    // ...
}

// Correct - use singleton
private static readonly IConfidentialClientApplication _app = BuildApp();

2. No codifique los secretos de forma dura:

// Wrong
.WithClientSecret("supersecretvalue123")

// Correct
.WithClientSecret(ConfigurationManager.AppSettings["AzureAd:ClientSecret"])

3. No use la memoria caché en memoria para los servicios de varias instancias:

// Wrong for services with multiple instances
app.AddInMemoryTokenCache();

// Correct - use distributed cache
app.AddDistributedTokenCaches(services =>
{
    services.AddDistributedSqlServerCache(/* ... */);
});

4. No ignore la validación de certificados:

// Wrong - skips validation
ServicePointManager.ServerCertificateValidationCallback = (sender, cert, chain, errors) => true;

// Correct - validate certificates properly

Migración desde ADAL.NET

Revise las diferencias clave y actualice el código para usar MSAL.NET con Microsoft. Identity.Web.

Descripción de las diferencias clave

Aspecto ADAL.NET (en desuso) MSAL.NET + Microsoft. Identity.Web
Ámbitos Basado en recursos (https://graph.microsoft.com) Basado en ámbito (https://graph.microsoft.com/.default)
Caché de tokens Serialización manual necesaria Adaptadores integrados mediante métodos de extensión
Certificados Carga manual de X509Certificate2 DefaultCertificateLoader con varios orígenes
Autoridad Fijado durante la construcción Se puede invalidar por solicitud

Comparación de ejemplos de migración

ADAL.NET (Antiguo):

AuthenticationContext authContext = new AuthenticationContext(authority);
ClientCredential credential = new ClientCredential(clientId, clientSecret);
AuthenticationResult result = await authContext.AcquireTokenAsync(resource, credential);

MSAL.NET con Microsoft. Identity.Web (Nuevo):

var app = ConfidentialClientApplicationBuilder.Create(clientId)
    .WithClientSecret(clientSecret)
    .WithTenantId(tenantId)
    .Build();

app.AddInMemoryTokenCache();  // Add token cache

string[] scopes = new[] { "https://graph.microsoft.com/.default" };
AuthenticationResult result = await app.AcquireTokenForClient(scopes).ExecuteAsync();

Use estos recursos para obtener más información sobre los escenarios relacionados.