Condividi tramite


Esercitazione: autenticare gli utenti nell'applicazione desktop WPF

Si applica a: cerchio verde con un simbolo di segno di spunta bianco che indica il contenuto seguente si applica ai tenant esterni. Tenant esterni (altre informazioni)

Questa esercitazione illustra come creare un'app desktop Windows Presentation Form (WPF) e prepararla per l'autenticazione usando l'interfaccia di amministrazione di Microsoft Entra.

In questa esercitazione, farai:

  • Configurare un'app desktop WPF per utilizzare i dettagli di registrazione dell'app.
  • Creare un'app desktop che effettua l'accesso di un utente e acquisisce un token per conto dell'utente.

Prerequisiti

Creare un'applicazione desktop WPF

  1. Aprire il terminale e passare alla cartella in cui si desidera che il progetto sia attivo.

  2. Inizializzare un'app desktop WPF e passare alla relativa cartella radice.

    dotnet new wpf --language "C#" --name sign-in-dotnet-wpf
    cd sign-in-dotnet-wpf
    

Installare i pacchetti

Installare provider di configurazione che permettano all'app la lettura dei dati di configurazione dalle coppie chiave-valore nel file delle impostazioni dell'app. Queste astrazioni di configurazione consentono di associare valori di configurazione a istanze di oggetti .NET.

dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.Binder

Installare Microsoft Authentication Library (MSAL) che contiene tutti i componenti chiave necessari per acquisire un token. È necessario installare anche la libreria broker MSAL che gestisce le interazioni con i broker di autenticazione desktop.

dotnet add package Microsoft.Identity.Client
dotnet add package Microsoft.Identity.Client.Broker

Creare un file appsettings.json e aggiungere configurazioni di registrazione

  1. Creare il file appsettings.json nella cartella radice dell'app.

  2. Aggiungere i dettagli di registrazione dell'app al file appsettings.json.

    {
        "AzureAd": {
            "Authority": "https://<Enter_the_Tenant_Subdomain_Here>.ciamlogin.com/",
            "ClientId": "<Enter_the_Application_Id_Here>"
        }
    }
    
    • Sostituire Enter_the_Tenant_Subdomain_Here con il sottodominio Directory (tenant).
    • Sostituire Enter_the_Application_Id_Here con l'ID applicazione (client) dell’app registrata in precedenza.
  3. Dopo aver creato il file delle impostazioni dell'app, verrà creato un altro file denominato AzureAdConfig.cs che consentirà di leggere le configurazioni dal file delle impostazioni dell'app. Creare il file AzureAdConfig.cs nella cartella radice dell'app.

  4. Nel file AzureAdConfig.js definire i getter e i setter per le proprietà ClientId e Authority. Aggiungere il codice seguente:

    namespace sign_in_dotnet_wpf
    {
        public class AzureAdConfig
        {
            public string Authority { get; set; }
            public string ClientId { get; set; }
        }
    }
    

Usare un dominio URL personalizzato (facoltativo)

Usare un dominio personalizzato per personalizzare completamente l'URL di autenticazione. Dal punto di vista dell'utente, gli utenti rimangono sul tuo dominio durante il processo di autenticazione, anziché essere reindirizzati al nome di dominio ciamlogin.com.

Seguire questa procedura per usare un dominio personalizzato:

  1. Usare la procedura descritta in Abilitare domini URL personalizzati per le app nei tenant esterni per abilitare il dominio URL personalizzato per il tenant esterno.

  2. Aprire il file appsettings.json.

    1. Aggiornare il valore della proprietà da Authority a https://Enter_the_Custom_Domain_Here/Enter_the_Tenant_ID_Here. Sostituire Enter_the_Custom_Domain_Here con il dominio URL personalizzato e Enter_the_Tenant_ID_Here con l'ID tenant. Se non hai l'ID del tenant, scopri come leggere i dettagli del tenant.
    2. Aggiungere una proprietà knownAuthorities con un valore [Enter_the_Custom_Domain_Here].

Dopo aver apportato le modifiche al file appsettings.json, se il dominio URL personalizzato è login.contoso.com e l'ID tenant è aaaabbbbbb-0000-cccc-1111-dddd2222eee, allora il file sarà simile al frammento di codice seguente:

{
    "AzureAd": {
        "Authority": "https://login.contoso.com/aaaabbbb-0000-cccc-1111-dddd2222eeee",
        "ClientId": "Enter_the_Application_Id_Here",
        "KnownAuthorities": ["login.contoso.com"]
    }
}

Modificare il file di progetto

  1. Vai al file sign-in-dotnet-wpf.csproj nella directory principale dell’app.

  2. In questo file eseguire i due passaggi seguenti:

    1. Modificare il file sign-in-dotnet-wpf.csproj per indicare all’app di copiare il file appsettings.json nella directory di output al termine della compilazione del progetto. Aggiungere la parte di codice seguente al file sign-in-dotnet-wpf.csproj:
    2. Impostare il framework di destinazione sulla build windows10.0.19041.0 per facilitare la lettura del token memorizzato nella cache dalla classe di supporto dei token, come si noterà nella classe di supporto della cache dei token.
    <Project Sdk="Microsoft.NET.Sdk">
    
        ...
    
        <!-- Set target framework to target windows10.0.19041.0 build -->
        <PropertyGroup>
            <OutputType>WinExe</OutputType>
            <TargetFramework>net7.0-windows10.0.19041.0</TargetFramework> <!-- target framework -->
            <RootNamespace>sign_in_dotnet_wpf</RootNamespace>
            <Nullable>enable</Nullable>
            <UseWPF>true</UseWPF>
        </PropertyGroup>
    
        <!-- Copy appsettings.json file to output folder. -->
        <ItemGroup>
            <None Remove="appsettings.json" />
        </ItemGroup>
    
        <ItemGroup>
            <EmbeddedResource Include="appsettings.json">
                <CopyToOutputDirectory>Always</CopyToOutputDirectory>
            </EmbeddedResource>
        </ItemGroup>
    </Project>
    

Creare una classe helper della cache dei token

Creare una classe di supporto per la cache dei token che inizializza una cache di token. L'applicazione tenta di leggere il token dalla cache prima di tentare di acquisire un nuovo token. Se il token non viene trovato nella cache, l'applicazione acquisisce un nuovo token. Al momento della disconnessione, tutti gli account e tutti i token di accesso corrispondenti verranno rimossi dalla cache.

  1. Creare un file TokenCacheHelper.cs nella cartella radice dell'app.

  2. Aprire il file TokenCacheHelper.cs. Aggiungere i pacchetti e i namespace al file. Nei passaggi seguenti questo file viene popolato con la logica del codice aggiungendo la logica pertinente alla classe TokenCacheHelper.

    using System.IO;
    using System.Security.Cryptography;
    using Microsoft.Identity.Client;
    
    namespace sign_in_dotnet_wpf
    {
        static class TokenCacheHelper{}
    }
    
  3. Aggiungere il costruttore alla classe TokenCacheHelper che definisce il percorso del file della cache. Per le app desktop in un pacchetto (pacchetti MSIX, detto anche Desktop Bridge), la cartella dell’assembly in esecuzione è di sola lettura. In questo caso è necessario usare Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path + "\msalcache.bin", che è una cartella di lettura/scrittura per singola app confezionata.

    namespace sign_in_dotnet_wpf
    {
        static class TokenCacheHelper
        {
            static TokenCacheHelper()
            {
                try
                {
                    CacheFilePath = Path.Combine(Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path, ".msalcache.bin3");
                }
                catch (System.InvalidOperationException)
                {
                    CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalcache.bin3";
                }
            }
            public static string CacheFilePath { get; private set; }
            private static readonly object FileLock = new object();
        }
    }
    
    
  4. Aggiungere codice per gestire la serializzazione della cache dei token. L'interfaccia ITokenCache implementa l'accesso pubblico alle operazioni della cache. L’interfaccia ITokenCache contiene i metodi per sottoscrivere gli eventi di serializzazione della cache, mentre l'interfaccia ITokenCacheSerializer espone i metodi da usare negli eventi di serializzazione della cache, per serializzare/deserializzare la cache. TokenCacheNotificationArgs contiene i parametri usati dalla chiamata Microsoft.Identity.Client (MSAL) che accede alla cache. L’interfaccia ITokenCacheSerializer è disponibile nel callback TokenCacheNotificationArgs.

    Aggiungere il codice seguente alla classe TokenCacheHelper:

        static class TokenCacheHelper
        {
            static TokenCacheHelper()
            {...}
            public static string CacheFilePath { get; private set; }
            private static readonly object FileLock = new object();
    
            public static void BeforeAccessNotification(TokenCacheNotificationArgs args)
            {
                lock (FileLock)
                {
                    args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath)
                            ? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
                                                     null,
                                                     DataProtectionScope.CurrentUser)
                            : null);
                }
            }
    
            public static void AfterAccessNotification(TokenCacheNotificationArgs args)
            {
                if (args.HasStateChanged)
                {
                    lock (FileLock)
                    {
                        File.WriteAllBytes(CacheFilePath,
                                           ProtectedData.Protect(args.TokenCache.SerializeMsalV3(),
                                                                 null,
                                                                 DataProtectionScope.CurrentUser)
                                          );
                    }
                }
            }
        }
    
        internal static void EnableSerialization(ITokenCache tokenCache)
        {
            tokenCache.SetBeforeAccess(BeforeAccessNotification);
            tokenCache.SetAfterAccess(AfterAccessNotification);
        }
    

    Nel metodo BeforeAccessNotification verrà letta la cache dal file system e, se la cache non è vuota, verrà deserializzata e caricata. Il metodo AfterAccessNotification viene chiamato dopo Microsoft.Identity.Client (MSAL) accede alla cache. Se la cache è stata modificata, serializzarla e rendere persistenti le modifiche alla cache.

    EnableSerialization contiene i metodi ITokenCache.SetBeforeAccess() e ITokenCache.SetAfterAccess():

    • ITokenCache.SetBeforeAccess() imposta un delegato per la ricezione delle modifiche prima che qualsiasi metodo della libreria acceda alla cache. Ciò consente al delegato di deserializzare una voce della cache per l'applicazione e gli account specificati in TokenCacheNotificationArgs.
    • ITokenCache.SetAfterAccess() imposta un delegato per la ricezione delle notifiche dopo che qualsiasi metodo della libreria accede alla cache. Ciò consente al delegato di serializzare una voce della cache per l'applicazione e gli account specificati in TokenCacheNotificationArgs.

Creare l'interfaccia utente dell'app desktop WPF

Modificare il file MainWindow.xaml per aggiungere gli elementi dell'interfaccia utente per l'app. Aprire il file MainWindow.xaml nella cartella radice dell'app e aggiungere la parte di codice seguente con la sezione del controllo <Grid></Grid>.

    <StackPanel Background="Azure">
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
            <Button x:Name="SignInButton" Content="Sign-In" HorizontalAlignment="Right" Padding="5" Click="SignInButton_Click" Margin="5" FontFamily="Segoe Ui"/>
            <Button x:Name="SignOutButton" Content="Sign-Out" HorizontalAlignment="Right" Padding="5" Click="SignOutButton_Click" Margin="5" Visibility="Collapsed" FontFamily="Segoe Ui"/>
        </StackPanel>
        <Label Content="Authentication Result" Margin="0,0,0,-5" FontFamily="Segoe Ui" />
        <TextBox x:Name="ResultText" TextWrapping="Wrap" MinHeight="120" Margin="5" FontFamily="Segoe Ui"/>
        <Label Content="Token Info" Margin="0,0,0,-5" FontFamily="Segoe Ui" />
        <TextBox x:Name="TokenInfoText" TextWrapping="Wrap" MinHeight="70" Margin="5" FontFamily="Segoe Ui"/>
    </StackPanel>

Questo codice aggiunge elementi chiave dell'interfaccia utente. I metodi e gli oggetti che gestiscono la funzionalità degli elementi dell'interfaccia utente vengono definiti nel file MainWindow.xaml.cs che verrà creato nel passaggio successivo.

  • Un pulsante che permette all'utente di accedere. Quando l’utente seleziona questo pulsante viene chiamato il metodo SignInButton_Click.
  • Pulsante che consente all’utente di disconnettersi. Quando l’utente seleziona questo pulsante viene chiamato il metodo SignOutButton_Click.
  • Casella di testo che visualizza i dettagli del risultato dell'autenticazione dopo il tentativo di accesso da parte dell'utente. Le informazioni visualizzate qui vengono restituite dall'oggetto ResultText.
  • Casella di testo che visualizza i dettagli del token dopo l'accesso da parte dell'utente. Le informazioni visualizzate qui vengono restituite dall'oggetto TokenInfoText.

Aggiungere codice al file MainWindow.xaml.cs

Il file MainWindow.xaml.cs contiene il codice che fornisce la logica di runtime per il comportamento degli elementi dell'interfaccia utente nel file MainWindow.xaml.

  1. Aprire il file MainWindow.xaml.cs nella cartella radice dell'app.

  2. Aggiungere il codice seguente nel file per importare i pacchetti e definire i segnaposto per i metodi creati.

    using Microsoft.Identity.Client;
    using System;
    using System.Linq;
    using System.Windows;
    using System.Windows.Interop;
    
    namespace sign_in_dotnet_wpf
    {
        public partial class MainWindow : Window
        {
            string[] scopes = new string[] { };
    
            public MainWindow()
            {
                InitializeComponent();
            }
    
            private async void SignInButton_Click(object sender, RoutedEventArgs e){...}
    
            private async void SignOutButton_Click(object sender, RoutedEventArgs e){...}
    
            private void DisplayBasicTokenInfo(AuthenticationResult authResult){...}
        }
    }
    
  3. Aggiungere il codice seguente al metodo SignInButton_Click. Questo metodo viene chiamato quando l'utente seleziona il pulsante Accedi.

    private async void SignInButton_Click(object sender, RoutedEventArgs e)
    {
        AuthenticationResult authResult = null;
        var app = App.PublicClientApp;
    
        ResultText.Text = string.Empty;
        TokenInfoText.Text = string.Empty;
    
        IAccount firstAccount;
    
        var accounts = await app.GetAccountsAsync();
        firstAccount = accounts.FirstOrDefault();
    
        try
        {
            authResult = await app.AcquireTokenSilent(scopes, firstAccount)
                    .ExecuteAsync();
        }
        catch (MsalUiRequiredException ex)
        {
            try
            {
                authResult = await app.AcquireTokenInteractive(scopes)
                    .WithAccount(firstAccount)
                    .WithParentActivityOrWindow(new WindowInteropHelper(this).Handle) 
                    .WithPrompt(Prompt.SelectAccount)
                    .ExecuteAsync();
            }
            catch (MsalException msalex)
            {
                ResultText.Text = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}";
            }
            catch (Exception ex)
            {
                ResultText.Text = $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}";
                return;
            }
    
            if (authResult != null)
            {
                ResultText.Text = "Sign in was successful.";
                DisplayBasicTokenInfo(authResult);
                this.SignInButton.Visibility = Visibility.Collapsed;
                this.SignOutButton.Visibility = Visibility.Visible;
            }
        }
    }
    

    GetAccountsAsync() restituisce tutti gli account disponibili nella cache dei token utente per l'app. L'interfaccia IAccount fornisce informazioni su un singolo account.

    Per acquisire i token, l'app tenta di acquisire il token in modo invisibile all'utente usando il metodo AcquireTokenSilent per verificare se la cache contiene un token accettabile. Il metodo AcquireTokenSilent potrebbe non avere esito positivo, ad esempio, perché l'utente si è disconnesso. Se MSAL rileva che il problema può essere risolto con un'azione interattiva, attiva un'eccezione MsalUiRequiredException. Questa eccezione fa sì che l'app acquisisca un token in modo interattivo.

    Se si chiama il metodo AcquireTokenInteractive, viene visualizzata una finestra in cui viene chiesto agli utenti di eseguire l'accesso. Le app richiedono in genere agli utenti di accedere in modo interattivo la prima volta che devono eseguire l'autenticazione. Potrebbe essere necessario accedere anche quando è in corso un'operazione invisibile per acquisire un token. Dopo la prima esecuzione di AcquireTokenInteractive, AcquireTokenSilent diventa il metodo consueto usato per ottenere i token

  4. Aggiungere il codice seguente al metodo SignOutButton_Click. Questo metodo viene chiamato quando l'utente seleziona il pulsante Disconnessione.

    private async void SignOutButton_Click(object sender, RoutedEventArgs e)
    {
        var accounts = await App.PublicClientApp.GetAccountsAsync();
        if (accounts.Any())
        {
            try
            {
                await App.PublicClientApp.RemoveAsync(accounts.FirstOrDefault());
                this.ResultText.Text = "User has signed-out";
                this.TokenInfoText.Text = string.Empty;
                this.SignInButton.Visibility = Visibility.Visible;
                this.SignOutButton.Visibility = Visibility.Collapsed;
            }
            catch (MsalException ex)
            {
                ResultText.Text = $"Error signing-out user: {ex.Message}";
            }
        }
    }
    

    Il metodo SignOutButton_Click rimuove tutti gli account e tutti i token di accesso corrispondenti dalla cache. Al successivo tentativo di accesso, l'utente dovrà farlo in modo interattivo.

  5. Aggiungere il codice seguente al metodo DisplayBasicTokenInfo. Questo metodo visualizza informazioni di base sul token.

    private void DisplayBasicTokenInfo(AuthenticationResult authResult)
    {
        TokenInfoText.Text = "";
        if (authResult != null)
        {
            TokenInfoText.Text += $"Username: {authResult.Account.Username}" + Environment.NewLine;
            TokenInfoText.Text += $"{authResult.Account.HomeAccountId}" + Environment.NewLine;
        }
    }
    

Aggiungere codice al file App.xaml.cs

App.xaml è dove di dichiarano le risorse usate nell'app. Si tratta del punto di ingresso per l'app. App.xaml.cs è il file code-behind per App.xaml. App.xaml.cs definisce anche la finestra iniziale per l'applicazione.

Aprire il file App.xaml.cs nella cartella radice dell'app e quindi aggiungerne il codice seguente.

using System.Windows;
using System.Reflection;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Broker;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;

namespace sign_in_dotnet_wpf
{
    public partial class App : Application
    {
        static App()
        {
            CreateApplication();
        }

        public static void CreateApplication()
        {
            var assembly = Assembly.GetExecutingAssembly();
            using var stream = assembly.GetManifestResourceStream("sign_in_dotnet_wpf.appsettings.json");
            AppConfiguration = new ConfigurationBuilder()
                .AddJsonStream(stream)
                .Build();

            AzureAdConfig azureADConfig = AppConfiguration.GetSection("AzureAd").Get<AzureAdConfig>();

            var builder = PublicClientApplicationBuilder.Create(azureADConfig.ClientId)
                .WithAuthority(azureADConfig.Authority)
                .WithDefaultRedirectUri();

            _clientApp = builder.Build();
            TokenCacheHelper.EnableSerialization(_clientApp.UserTokenCache);
        }

        private static IPublicClientApplication _clientApp;
        private static IConfiguration AppConfiguration;
        public static IPublicClientApplication PublicClientApp { get { return _clientApp; } }
    }
}

In questo passaggio verrà caricato il file appsettings.json. Il generatore di configurazione consente di leggere le configurazioni dell'app definite nel file appsettings.json. È anche possibile definire l'app WPF come app client pubblica perché si tratta di un'app desktop. Il metodo TokenCacheHelper.EnableSerialization abilita la serializzazione della cache dei token.

Eseguire l'app

Esegui l'app e effettua l'accesso per testare l'applicazione

  1. Nel terminale, passare alla cartella radice dell'app WPF ed eseguire l'app eseguendo il comando dotnet run nel terminale.

  2. Dopo aver avviato l'esempio, viene visualizzata in genere una finestra con un pulsante Accedi. Selezionare il pulsante Accedi.

    Screenshot della schermata di accesso per un'applicazione desktop WPF.

  3. Nella pagina di accesso immettere l'indirizzo di posta elettronica dell'account. Se non si dispone di un account, selezionare il collegamento Nessun account? Creare un account per avviare il flusso di iscrizione. Seguire questo flusso per creare un nuovo account e accedere.

  4. Dopo l'accesso, verrà visualizzata una schermata che mostra l'accesso riuscito e le informazioni di base sull'account utente archiviato nel token recuperato. Le informazioni di base vengono visualizzate nella sezione Informazioni token della schermata di accesso

Vedere anche