Condividi tramite


MSAL.NET con Microsoft. Identity.Web in .NET Framework

Questa guida illustra come usare la cache dei token di Microsoft.Identity.Web e i pacchetti di certificato con MSAL.NET in .NET Framework, .NET Standard 2.0 e applicazioni .NET classiche (.NET 4.7.2+).

Informazioni generali

A partire da Microsoft. Identity.Web 1.17+, è possibile usare Microsoft. Pacchetti di utilità Identity.Web con MSAL.NET in ambienti non ASP.NET Core.

Perché usare questi pacchetti?

Feature Beneficio
Serializzazione della cache dei token Adattatori cache riutilizzabili per in-memory, SQL Server, Redis, Cosmos DB, PostgreSQL
Assistenti certificato Caricamento semplificato dei certificati da Key Vault, file system o archivi di certificati
Estensioni delle attestazioni Metodi di utilità per la manipolazione di ClaimsPrincipal
.NET Standard 2.0 Compatibile con .NET Framework 4.7.2+, .NET Core e .NET 5+
Dipendenze minime Pacchetti mirati senza dipendenze da ASP.NET Core

Scenari supportati

  • .NET Framework Console Applications (scenari demone)
  • Desktop Applications (.NET Framework)
  • Servizi di lavoro (.NET Framework)
  • librerie .NET Standard 2.0 (compatibilità multipiattaforma)
  • Applicazione di MSAL.NET Non Web

Annotazioni

Per ASP.NET MVC/applicazioni API Web, vedere invece OWIN Integration.


Opzioni del pacchetto

Pacchetti principali per MSAL.NET

Package Scopo Dipendenze destinazione .NET
Microsoft. Identity.Web.TokenCache Serializzatori della cache dei token, ClaimsPrincipal estensioni Minime .NET Standard 2.0
Microsoft. Identity.Web.Certificate Utilità di caricamento dei certificati Minime .NET Standard 2.0

Installazione

Gestione pacchetti 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

Perché non Microsoft. Identity.Web (core)?

Il pacchetto core Microsoft.Identity.Web include le dipendenze di ASP.NET Core (Microsoft.AspNetCore.*), che:

  • Non sono compatibili con ASP.NET Framework
  • Aumentare le dimensioni del pacchetto inutilmente
  • Creare conflitti di dipendenza

Usare invece pacchetti di destinazione per .NET Framework e .NET scenari Standard.


Serializzazione della cache dei token

Informazioni generali

Microsoft. Identity.Web fornisce adattatori della cache dei token che funzionano perfettamente con MSAL.NET IConfidentialClientApplication.

Modello: Creazione di client riservati con la cache dei token

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

Opzioni della cache dei token

Opzione 1: cache dei token in memoria

Cache in memoria semplice:

using Microsoft.Identity.Web.TokenCacheProviders;

_app.AddInMemoryTokenCache();

Cache in memoria con limiti di dimensione (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
    });
});

Caratteristiche:

  • Accesso rapido
  • Nessuna dipendenza esterna
  • Non condiviso tra processi
  • Perso al riavvio dell'app

Caso d'uso: App console a istanza singola, applicazioni desktop


Opzione 2: Cache dei token in memoria distribuita

Per gli ambienti a istanze multipla con cache in memoria:

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

Caratteristiche:

  • Condiviso tra istanze dell'app
  • Migliore per gli scenari con carico bilanciato
  • Richiede un pacchetto NuGet aggiuntivo
  • Ancora perso al riavvio dell'app

Caso d'uso: Servizi a istanze multipla con acquisizione di token accettabile


Opzione 3: cache dei token SQL Server

Per la memorizzazione nella cache distribuita 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);
    });
});

Configurazione del database:

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

Caratteristiche:

  • Persistente attraverso i riavvii
  • Condiviso tra più istanze
  • Affidabile e scalabile
  • Richiede SQL Server configurazione

Caso d'uso: Servizi daemon di produzione, attività pianificate, ruoli di lavoro a istanze multiple


Opzione 4: Cache dei token Redis

Per la memorizzazione nella cache distribuita a prestazioni elevate:

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

Configurazione di produzione:

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

Caratteristiche:

  • Estremamente veloce
  • Condivisi tra istanze
  • Persistente (con persistenza Redis abilitata)
  • Richiede il server Redis

Caso d'uso: App daemon con volumi elevati, sistemi distribuiti, microservizi


Opzione 5: Cache dei token di Cosmos DB

Per la memorizzazione nella cache distribuita a livello globale:

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

Caratteristiche:

  • Distribuito a livello globale
  • A disponibilità elevata
  • Scalabilità automatica
  • Latenza più elevata rispetto a Redis
  • Costo più elevato

Caso d'uso: Servizi daemon globali, applicazioni con distribuzione geografica


Opzione 6: Cache dei token PostgreSQL

Per la memorizzazione nella cache distribuita relazionale con 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);
    });
});

Caratteristiche:

  • Persistente attraverso i riavvii
  • Condiviso tra più istanze
  • Semantica SQL familiare
  • Funziona con Database di Azure per PostgreSQL
  • Richiede un server PostgreSQL

Caso d'uso: Applicazioni che già utilizzano PostgreSQL come database primario, o servizi ospitati su Azure che utilizzano Azure Database per PostgreSQL


Esempio completo: Applicazione Daemon

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

Gestione dei certificati

Informazioni generali

Microsoft. Identity.Web semplifica il caricamento dei certificati da varie origini per i flussi di credenziali client.

Modello: caricamento di certificati con DefaultCertificateLoader

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

Origini certificato

1. Da Azure Key Vault

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

Prerequisiti:

  • Identità gestita o Principale del Servizio con accesso a Key Vault
  • pacchetto NuGet Azure.Identity
  • autorizzazione Key Vault: Get sui certificati

2. Dall'archivio certificati

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

Oppure trova tramite impronta digitale:

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

3. Dal file system

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 sulla sicurezza: Non memorizzare mai password nel codice. Usare la configurazione sicura.


4. Da stringa codificata in Base64

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

Caricamento del certificato basato sulla configurazione

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>

Codice C#:

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

Applicazioni di esempio

Esempi ufficiali di Microsoft

Esempio Piattaforma Descrizione
ConfidentialClientTokenCache Console (Framework .NET) Modelli di serializzazione della cache dei token
active-directory-dotnetcore-daemon-v2 Console (.NET Core) Caricamento di certificati da Key Vault

Procedure consigliate

Cose da fare

1. Usare il modello singleton per IConfidentialClientApplication:

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. Impostare la scadenza appropriata della cache dei token:

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

3. Usare l'archiviazione sicura dei certificati:

// 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. Implementare una corretta gestione degli errori:

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

5. Utilizzare la cache distribuita per la produzione:

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

Cose da non fare

1. Non creare ripetutamente nuove istanze IConfidentialClientApplication:

// 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. Non inserire dati sensibili nel codice:

// Wrong
.WithClientSecret("supersecretvalue123")

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

3. Non usare la cache in memoria per i servizi a istanze multipla:

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

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

4. Non ignorare la convalida del certificato:

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

// Correct - validate certificates properly

Migrazione da ADAL.NET

Differenze principali

Aspetto ADAL.NET (deprecato) MSAL.NET + Microsoft. Identity.Web
Ambiti Basato sulle risorse (https://graph.microsoft.com) Basato sull'ambito (https://graph.microsoft.com/.default)
Token Cache Serializzazione manuale richiesta Adattatori integrati tramite metodi di estensione
Attestati Caricamento manuale di X509Certificate2 DefaultCertificateLoader con più origini
Authority Fissato durante la costruzione Può essere sovrascritto per richiesta

Esempio di migrazione

ADAL.NET (Old):

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

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

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