Compartir vía


Tutorial: Autenticación de usuarios en la aplicación de escritorio de WPF

Este tutorial es la parte final de una serie que demuestra la construcción de una aplicación de escritorio Windows Presentation Form (WPF) y su preparación para la autenticación mediante el centro de administración de Microsoft Entra. En la parte 1 de esta serie, registró una aplicación y configuró los flujos de usuario en el inquilino externo. En este tutorial se muestra cómo crear una aplicación de escritorio WPF de .NET y cómo iniciar y cerrar la sesión de un usuario con Id. externa de Microsoft Entra.

En este tutorial, hará lo siguiente:

  • Configure una aplicación de escritorio de WPF para usarla en los detalles del registro de la aplicación.
  • Cree una aplicación de escritorio que inicie la sesión de un usuario y adquiera un token en nombre del usuario.

Requisitos previos

Creación de una aplicación de escritorio de WPF

  1. Abra el terminal y vaya a la carpeta donde desea tener el proyecto.

  2. Inicialice una aplicación de escritorio de WPF y navegue a su carpeta raíz.

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

Instalar paquetes

Instale proveedores de configuración que ayuden a nuestra aplicación a leer los datos de configuración de pares clave-valor en nuestro archivo de configuración de la aplicación. Estas abstracciones de configuración proporcionan la capacidad de enlazar valores de configuración a instancias de objetos .NET.

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

Instale la Biblioteca de autenticación de Microsoft (MSAL) que contiene todos los componentes clave que necesita para adquirir un token. También se instala la biblioteca de agente de MSAL que controla las interacciones con los agentes de autenticación de escritorio.

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

Crear un archivo appsettings.json y agregar configuraciones de registro

  1. Cree un archivo appsettings.json en la carpeta raíz de la aplicación.

  2. Agregue los detalles del registro de aplicación al archivo appsettings.json.

    {
        "AzureAd": {
            "Authority": "https://<Enter_the_Tenant_Subdomain_Here>.ciamlogin.com/",
            "ClientId": "<Enter_the_Application_Id_Here>"
        }
    }
    
    • Reemplace Enter_the_Tenant_Subdomain_Here por el subdominio del directorio (inquilino).
    • Reemplace Enter_the_Application_Id_Here con el Id. de aplicación (cliente) de la aplicación que registró anteriormente.
  3. Después de crear el archivo de configuración de la aplicación, crearemos otro archivo denominado AzureAdConfig.cs que le ayudará a leer las configuraciones del archivo de configuración de la aplicación. Cree el archivo AzureAdConfig.cs en la carpeta raíz de la aplicación.

  4. En el archivo AzureAdConfig.js, defina los captadores y establecedores para las propiedades ClientId y Authority. Agregue el siguiente código:

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

Uso del dominio de dirección URL personalizada (opcional)

Use un dominio personalizado para personalizar completamente la dirección URL de autenticación. Desde el punto de vista del usuario, este permanece en el dominio durante el proceso de autenticación, en lugar de que se le redirija al nombre de dominio ciamlogin.com.

Siga estos pasos para usar un dominio personalizado:

  1. Siga los pasos descritos en Habilitar dominios de dirección URL personalizados para aplicaciones en inquilinos externos para habilitar la dirección URL de dominio personalizado para el inquilino externo.

  2. Abra el archivo appsettings.json:

    1. Actualice el valor de la propiedad Authority a https://Enter_the_Custom_Domain_Here/Enter_the_Tenant_ID_Here. Reemplace Enter_the_Custom_Domain_Here por la dirección URL de dominio personalizado y Enter_the_Tenant_ID_Here por el id. del inquilino. Si no tiene el identificador del inquilino, obtenga información sobre cómo leer los detalles del inquilino.
    2. Agregue la propiedad knownAuthorities con un valor [Escriba_aquí_el_dominio_personalizado].

Después de realizar los cambios en el archivo appsettings.json, si la dirección URL del dominio personalizado es login.contoso.comy el identificador de inquilino es aaaabbbb-0000-cccc-1111-dddd2222eeee, el archivo debe tener un aspecto similar al siguiente fragmento de código:

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

Modificar el archivo del proyecto

  1. Vaya al archivo sign-in-dotnet-wpf.csproj en la carpeta raíz de la aplicación.

  2. En este archivo, siga estos dos pasos:

    1. Modifique el archivo sign-in-dotnet-wpf.csproj para indicar a la aplicación que copie el archivo appsettings.json en el directorio de salida cuando se compile el proyecto. Agregue el siguiente fragmento de código al archivo sign-in-dotnet-wpf.csproj:
    2. Establezca la plataforma de destino en la compilación windows10.0.19041.0 para ayudar a leer el token almacenado en caché de la caché de tokens, como verá en la clase auxiliar de caché de tokens.
    <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>
    

Creación de una clase auxiliar de caché de tokens

Cree una clase auxiliar de caché de tokens que inicialice una caché de tokens. La aplicación intenta leer el token de la memoria caché antes de intentar adquirir un nuevo token. Si el token no se encuentra en la memoria caché, la aplicación adquiere un nuevo token. Al cerrar sesión, la memoria caché se borra de todas las cuentas y de todos los tokens de acceso correspondientes.

  1. Cree un archivo TokenCacheHelper.cs en la carpeta raíz de la aplicación.

  2. Abra el archivo TokenCacheHelper.cs. Agregue los paquetes y los espacios de nombres al archivo. En los pasos siguientes, rellenará este archivo con la lógica de código agregando la lógica pertinente a la clase TokenCacheHelper.

    using System.IO;
    using System.Security.Cryptography;
    using Microsoft.Identity.Client;
    
    namespace sign_in_dotnet_wpf
    {
        static class TokenCacheHelper{}
    }
    
  3. Agregue un constructor a la clase TokenCacheHelper que define la ruta de acceso del archivo de caché. En el caso de las aplicaciones de escritorio empaquetadas (paquetes MSIX, también denominados puente de escritorio), la carpeta de ensamblado en ejecución es de solo lectura. En ese caso, es necesario usar Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path + "\msalcache.bin", que es una carpeta de lectura y escritura por aplicación para aplicaciones empaquetadas.

    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. Agregue código para controlar la serialización de la caché de tokens. La interfaz ITokenCache implementa el acceso público a las operaciones de caché. La interfaz ITokenCache contiene los métodos para suscribirse a los eventos de serialización de caché, mientras que la interfaz ITokenCacheSerializer expone los métodos que necesita usar en los eventos de serialización de caché, con el fin de serializar o deserializar la memoria caché. TokenCacheNotificationArgs contiene parámetros usados por la llamada Microsoft.Identity.Client (MSAL) que accede a la memoria caché. La interfaz ITokenCacheSerializer está disponible en la devolución de llamada TokenCacheNotificationArgs.

    Agregue el siguiente código a la clase 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);
        }
    

    En el método BeforeAccessNotification, leerá la memoria caché del sistema de archivos y, si la memoria caché no está vacía, la deserializará y la cargará. Se llama al método AfterAccessNotification después de que Microsoft.Identity.Client (MSAL) tenga acceso a la memoria caché. Si la memoria caché ha cambiado, la serializa y conserva los cambios en la memoria caché.

    EnableSerialization contiene los métodos ITokenCache.SetBeforeAccess() y ITokenCache.SetAfterAccess():

    • ITokenCache.SetBeforeAccess() establece un delegado que se notificará antes de que cualquier método de biblioteca acceda a la memoria caché. Esto proporciona una opción al delegado para deserializar una entrada de caché para la aplicación y las cuentas especificadas en TokenCacheNotificationArgs.
    • ITokenCache.SetAfterAccess() establece un delegado que se notificará antes de que cualquier método de biblioteca acceda a la memoria caché. Esto proporciona una opción al delegado para serializar una entrada de caché para la aplicación y las cuentas especificadas en TokenCacheNotificationArgs.

Creación de la interfaz de usuario de la aplicación de escritorio de WPF

Modifique el archivo MainWindow.xaml para agregar los elementos de la interfaz de usuario para la aplicación. Abra el archivo MainWindow.xaml en la carpeta raíz de la aplicación y agregue el siguiente fragmento de código con la sección de control <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>

Este código agrega elementos clave de la interfaz de usuario. Los métodos y objetos que controlan la funcionalidad de los elementos de la interfaz de usuario se definen en el archivo MainWindow.xaml.cs que crearemos en el paso siguiente.

  • Botón que inicia la sesión del usuario. Se llama al método SignInButton_Click cuando el usuario selecciona este botón.
  • Botón que cierra la sesión del usuario. Se llama al método SignOutButton_Click cuando el usuario selecciona este botón.
  • Cuadro de texto que muestra los detalles del resultado de autenticación después de que el usuario intente iniciar sesión. El objeto ResultText devuelve la información que se muestra aquí.
  • Cuadro de texto que muestra los detalles del token después de que el usuario inicie sesión correctamente. El objeto TokenInfoText devuelve la información que se muestra aquí.

Agregar código al archivo MainWindow.xaml.cs

El archivo MainWindow.xaml.cs contiene el código que proporciona la lógica de runtime para el comportamiento de los elementos de la interfaz de usuario en el archivo MainWindow.xaml.

  1. Abra el archivo MainWindow.xaml.cs en la carpeta raíz de la aplicación.

  2. Agregue el código siguiente en el archivo para importar los paquetes y defina marcadores de posición para los métodos que creamos.

    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. Agregue el siguiente código al método SignInButton_Click. Se llama a este método cuando el usuario selecciona el botón Iniciar sesión.

    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() devuelve todas las cuentas disponibles en la caché de tokens de usuario de la aplicación. La interfaz IAccount representa información sobre una sola cuenta.

    Para adquirir tokens, la aplicación intenta adquirir el token de forma silenciosa mediante el método AcquireTokenSilent para comprobar si hay un token aceptable en la memoria caché. El método AcquireTokenSilent puede producir un error, por ejemplo, porque el usuario ha cerrado la sesión. Si MSAL detecta que el problema se puede resolver al requerir una acción interactiva, produce una excepción MsalUiRequiredException. Esta excepción hace que la aplicación adquiera un token de forma interactiva.

    La llamada al método AcquireTokenInteractive genera una ventana que pide al usuario que inicie sesión. Las aplicaciones suelen requerir a los usuarios que inicien sesión de forma interactiva la primera vez que necesitan autenticarse. Es posible que también deban iniciar sesión cuando se produce una operación silenciosa para adquirir un token. Después de que se ejecute AcquireTokenInteractive por primera vez, AcquireTokenSilent se vuelve el método usual que se usará para obtener tokens

  4. Agregue el siguiente código al método SignOutButton_Click. Se llama a este método cuando el usuario selecciona el botón Cerrar sesión.

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

    El método SignOutButton_Click borra la memoria caché de todas las cuentas y todos los tokens de acceso correspondientes. La próxima vez que el usuario intente iniciar sesión, tendrá que hacerlo de forma interactiva.

  5. Agregue el siguiente código al método DisplayBasicTokenInfo. Este método muestra información básica sobre el 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;
        }
    }
    

Agregar código al archivo App.xaml.cs

App.xaml es donde se declaran recursos que se usan en la aplicación. Es el punto de entrada para la aplicación. App.xaml.cs es el archivo de código subyacente de App.xaml. App.xaml.cs también define la ventana de inicio de la aplicación.

Abra el archivo App.xaml.cs en la carpeta raíz de la aplicación y agregue el código siguiente en él.

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

En este paso, cargará el archivo appsettings.json. El generador de configuración le ayuda a leer las configuraciones de la aplicación definidas en el archivo appsettings.json. También define la aplicación de WPF como una aplicación cliente pública, ya que es una aplicación de escritorio. El método TokenCacheHelper.EnableSerialization habilita la serialización de la caché de tokens.

Ejecutar la aplicación

Ejecución de la aplicación e inicio de sesión para probar la aplicación

  1. En el terminal, vaya a la carpeta raíz de la aplicación de WPF y ejecute la aplicación con el comando dotnet run en el terminal.

  2. Después de iniciar el ejemplo, debería ver una ventana con un botón para Iniciar sesión. Haga clic en el botón Inicial sesión.

    Captura de pantalla de la pantalla de inicio de sesión para una aplicación de escritorio de WPF.

  3. En la página de inicio de sesión, escriba la dirección de correo electrónico de su cuenta. Si no tiene una cuenta, seleccione ¿No tiene una cuenta? Cree una, que inicia el flujo de registro. Siga este flujo para crear una nueva cuenta e iniciar sesión.

  4. Una vez que inicie sesión, verá una pantalla en la que se muestra el inicio de sesión correcto y la información básica sobre la cuenta de usuario almacenada en el token recuperado. La información básica se muestra en la sección Información del token de la pantalla de inicio de sesión

Consulte también