Crear un servicio de inicio de sesión de Windows Hello
Esta es la segunda parte de un tutorial completo sobre cómo usar Windows Hello como alternativa a los sistemas tradicionales de autenticación de nombre de usuario y contraseña en aplicaciones de Windows empaquetadas. En este artículo se recoge dónde se dejó la parte 1, la aplicación de inicio de sesión de Windows Hello, y amplía la funcionalidad para demostrar cómo puede integrar Windows Hello en la aplicación existente.
Para compilar este proyecto, necesitarás experiencia con C#y XAML. También deberá usar Visual Studio 2022 en una máquina Windows 10 o Windows 11. Consulte Introducción a WinUI para obtener instrucciones completas sobre cómo configurar el entorno de desarrollo.
Ejercicio 1: Lógica del lado servidor
En este ejercicio, comenzará con la aplicación Windows Hello integrada en el primer laboratorio y creará un servidor y una base de datos ficticio local. Este laboratorio práctico está diseñado para enseñar cómo Se puede integrar Windows Hello en un sistema existente. Mediante el uso de un servidor ficticio y una base de datos simulada, se elimina una gran cantidad de configuraciones no relacionadas. En sus propias aplicaciones, deberá reemplazar los objetos ficticios por los servicios y bases de datos reales.
Para empezar, abra la solución WindowsHelloLogin desde el primer laboratorio práctico de Windows Hello.
Comenzará implementando el servidor ficticio y la base de datos ficticia. Cree una carpeta denominada "AuthService". En Explorador de soluciones, haga clic con el botón derecho en el proyecto WindowsHelloLogin y seleccione Agregar>nueva carpeta.
Cree clases UserAccount y WindowsHelloDevices que actuarán como modelos para que los datos se guarden en la base de datos ficticia. UserAccount será similar al modelo de usuario implementado en un servidor de autenticación tradicional. Haga clic con el botón derecho en la carpeta AuthService y agregue una nueva clase denominada "UserAccount".
Cambie el ámbito de clase para que sea público y agregue las siguientes propiedades públicas para la clase UserAccount . Deberá agregar una instrucción using para el
System.ComponentModel.DataAnnotations
espacio de nombres .using System; using System.ComponentModel.DataAnnotations; namespace WindowsHelloLogin.AuthService { public class UserAccount { [Key, Required] public Guid UserId { get; set; } [Required] public string Username { get; set; } public string Password { get; set; } // public List<WindowsHelloDevice> WindowsHelloDevices = new(); } }
Es posible que haya observado la lista comentada de WindowsHelloDevices. Se trata de una modificación que deberá realizar en un modelo de usuario existente en la implementación actual. La lista de WindowsHelloDevices contendrá un deviceID, la clave pública realizada desde Windows Hello y keyCredentialAttestationResult. Para este ejercicio, tendrá que implementar keyAttestationResult, ya que solo los proporciona Windows Hello en dispositivos que tienen un chip TPM (módulos de plataforma segura). KeyCredentialAttestationResult es una combinación de varias propiedades y tendría que dividirse para guardarlas y cargarlas con una base de datos.
Cree una nueva clase en la carpeta AuthService denominada "WindowsHelloDevice.cs". Este es el modelo para los dispositivos Windows Hello como se ha descrito anteriormente. Cambie el ámbito de clase para que sea público y agregue las siguientes propiedades.
using System; namespace WindowsHelloLogin.AuthService { public class WindowsHelloDevice { // These are the new variables that will need to be added to the existing UserAccount in the Database // The DeviceName is used to support multiple devices for the one user. // This way the correct public key is easier to find as a new public key is made for each device. // The KeyAttestationResult is only used if the User device has a TPM (Trusted Platform Module) chip, // in most cases it will not. So will be left out for this hands on lab. public Guid DeviceId { get; set; } public byte[] PublicKey { get; set; } // public KeyCredentialAttestationResult KeyAttestationResult { get; set; } } }
Vuelva a UserAccount.cs y quite la marca de comentario de la lista de dispositivos Windows Hello.
using System.Collections.Generic; namespace WindowsHelloLogin.AuthService { public class UserAccount { [Key, Required] public Guid UserId { get; set; } [Required] public string Username { get; set; } public string Password { get; set; } public List<WindowsHelloDevice> WindowsHelloDevices = new(); } }
Con el modelo de UserAccount y WindowsHelloDevice creados, debe crear otra clase nueva en la carpeta AuthService que actuará como base de datos ficticia, ya que se trata de una base de datos ficticia desde donde guardará y cargará una lista de cuentas de usuario localmente. En el mundo real, esta sería la implementación de la base de datos. Cree una nueva clase en la carpeta AuthService denominada "MockStore.cs". Cambie el ámbito de clase a público.
Como el almacén ficticio guardará y cargará una lista de cuentas de usuario localmente, puede implementar la lógica para guardar y cargar esa lista mediante xmlSerializer. También deberá recordar el nombre de archivo y guardar la ubicación. En MockStore.cs implemente lo siguiente:
using System.Collections.Generic; using System; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml.Serialization; using Windows.Storage; namespace WindowsHelloLogin.AuthService { public class MockStore { private const string USER_ACCOUNT_LIST_FILE_NAME = "userAccountsList.txt"; // This cannot be a const because the LocalFolder is accessed at runtime private string _userAccountListPath = Path.Combine( ApplicationData.Current.LocalFolder.Path, USER_ACCOUNT_LIST_FILE_NAME); private List<UserAccount> _mockDatabaseUserAccountsList; #region Save and Load Helpers /// <summary> /// Create and save a useraccount list file. (Replacing the old one) /// </summary> private async Task SaveAccountListAsync() { string accountsXml = SerializeAccountListToXml(); if (File.Exists(_userAccountListPath)) { StorageFile accountsFile = await StorageFile.GetFileFromPathAsync(_userAccountListPath); await FileIO.WriteTextAsync(accountsFile, accountsXml); } else { StorageFile accountsFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(USER_ACCOUNT_LIST_FILE_NAME); await FileIO.WriteTextAsync(accountsFile, accountsXml); } } /// <summary> /// Gets the useraccount list file and deserializes it from XML to a list of useraccount objects. /// </summary> /// <returns>List of useraccount objects</returns> private async Task LoadAccountListAsync() { if (File.Exists(_userAccountListPath)) { StorageFile accountsFile = await StorageFile.GetFileFromPathAsync(_userAccountListPath); string accountsXml = await FileIO.ReadTextAsync(accountsFile); DeserializeXmlToAccountList(accountsXml); } // If the UserAccountList does not contain the sampleUser Initialize the sample users // This is only needed as it in a Hand on Lab to demonstrate a user being migrated. // In the real world, user accounts would just be in a database. if (!_mockDatabaseUserAccountsList.Any(f => f.Username.Equals("sampleUsername"))) { //If the list is empty, call InitializeSampleAccounts and return the list //await InitializeSampleUserAccountsAsync(); } } /// <summary> /// Uses the local list of accounts and returns an XML formatted string representing the list /// </summary> /// <returns>XML formatted list of accounts</returns> private string SerializeAccountListToXml() { var xmlizer = new XmlSerializer(typeof(List<UserAccount>)); var writer = new StringWriter(); xmlizer.Serialize(writer, _mockDatabaseUserAccountsList); return writer.ToString(); } /// <summary> /// Takes an XML formatted string representing a list of accounts and returns a list object of accounts /// </summary> /// <param name="listAsXml">XML formatted list of accounts</param> /// <returns>List object of accounts</returns> private List<UserAccount> DeserializeXmlToAccountList(string listAsXml) { var xmlizer = new XmlSerializer(typeof(List<UserAccount>)); TextReader textreader = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(listAsXml))); return _mockDatabaseUserAccountsList = (xmlizer.Deserialize(textreader)) as List<UserAccount>; } #endregion } }
En el método LoadAccountListAsync , es posible que haya observado que se ha comentado un método InitializeSampleUserAccountsAsync . Deberá crear este método en el MockStore.cs. Este método rellenará la lista de cuentas de usuario para que se pueda realizar un inicio de sesión. En el mundo real, la base de datos de usuario ya se rellenaría. En este paso, también creará un constructor que inicializará la lista de usuarios y llamará a LoadAccountListAsync.
namespace WindowsHelloLogin.AuthService { public class MockStore { private const string USER_ACCOUNT_LIST_FILE_NAME = "userAccountsList.txt"; // This cannot be a const because the LocalFolder is accessed at runtime private string _userAccountListPath = Path.Combine( ApplicationData.Current.LocalFolder.Path, USER_ACCOUNT_LIST_FILE_NAME); private List<UserAccount> _mockDatabaseUserAccountsList; public MockStore() { _mockDatabaseUserAccountsList = new List<UserAccount>(); _ = LoadAccountListAsync(); } private async Task InitializeSampleUserAccountsAsync() { // Create a sample Traditional User Account that only has a Username and Password // This will be used initially to demonstrate how to migrate to use Windows Hello var sampleUserAccount = new UserAccount() { UserId = Guid.NewGuid(), Username = "sampleUsername", Password = "samplePassword", }; // Add the sampleUserAccount to the _mockDatabase _mockDatabaseUserAccountsList.Add(sampleUserAccount); await SaveAccountListAsync(); } } }
Ahora que el método InitializeSampleUserAccountsAsync existe anula la marca de comentario de la llamada al método en el método LoadAccountListAsync .
private async Task LoadAccountListAsync() { if (File.Exists(_userAccountListPath)) { StorageFile accountsFile = await StorageFile.GetFileFromPathAsync(_userAccountListPath); string accountsXml = await FileIO.ReadTextAsync(accountsFile); DeserializeXmlToAccountList(accountsXml); } // If the UserAccountList does not contain the sampleUser Initialize the sample users // This is only needed as it in a Hand on Lab to demonstrate a user migrating // In the real world user accounts would just be in a database if (!_mockDatabaseUserAccountsList.Any(f = > f.Username.Equals("sampleUsername"))) { //If the list is empty InitializeSampleUserAccountsAsync and return the list await InitializeSampleUserAccountsAsync(); } }
La lista de cuentas de usuario en el almacén ficticio ahora se puede guardar y cargar. Otras partes de la aplicación tendrán que tener acceso a esta lista, por lo que deberá haber algunos métodos para recuperar estos datos. Debajo del método InitializeSampleUserAccountsAsync , agregue los métodos siguientes para obtener datos. Le permitirá obtener un identificador de usuario, un único usuario, una lista de usuarios para un dispositivo Windows Hello específico y también obtener la clave pública para el usuario en un dispositivo específico.
public Guid GetUserId(string username) { if (_mockDatabaseUserAccountsList.Any()) { UserAccount account = _mockDatabaseUserAccountsList.FirstOrDefault(f => f.Username.Equals(username)); if (account != null) { return account.UserId; } } return Guid.Empty; } public UserAccount GetUserAccount(Guid userId) { return _mockDatabaseUserAccountsList.FirstOrDefault(f => f.UserId.Equals(userId)); } public List<UserAccount> GetUserAccountsForDevice(Guid deviceId) { var usersForDevice = new List<UserAccount>(); foreach (UserAccount account in _mockDatabaseUserAccountsList) { if (account.WindowsHelloDevices.Any(f => f.DeviceId.Equals(deviceId))) { usersForDevice.Add(account); } } return usersForDevice; } public byte[] GetPublicKey(Guid userId, Guid deviceId) { UserAccount account = _mockDatabaseUserAccountsList.FirstOrDefault(f => f.UserId.Equals(userId)); if (account != null) { if (account.WindowsHelloDevices.Any()) { return account.WindowsHelloDevices.FirstOrDefault(p => p.DeviceId.Equals(deviceId)).PublicKey; } } return null; }
Los métodos siguientes que se van a implementar controlarán las operaciones sencillas para agregar una cuenta, quitar una cuenta y también quitar un dispositivo. Se necesita quitar un dispositivo, ya que Windows Hello es específico del dispositivo. Para cada dispositivo en el que inicie sesión, Windows Hello creará un nuevo par de claves pública y privada. Es como tener una contraseña diferente para cada dispositivo que inicie sesión, lo único que es que no es necesario recordar todas esas contraseñas; lo hace el servidor. Agregue los métodos siguientes al MockStore.cs.
public async Task<UserAccount> AddAccountAsync(string username) { UserAccount newAccount = null; try { newAccount = new UserAccount() { UserId = Guid.NewGuid(), Username = username, }; _mockDatabaseUserAccountsList.Add(newAccount); await SaveAccountListAsync(); } catch (Exception) { throw; } return newAccount; } public async Task<bool> RemoveAccountAsync(Guid userId) { UserAccount userAccount = GetUserAccount(userId); if (userAccount != null) { _mockDatabaseUserAccountsList.Remove(userAccount); await SaveAccountListAsync(); return true; } return false; } public async Task<bool> RemoveDeviceAsync(Guid userId, Guid deviceId) { UserAccount userAccount = GetUserAccount(userId); WindowsHelloDevice deviceToRemove = null; if (userAccount != null) { foreach (WindowsHelloDevice device in userAccount.WindowsHelloDevices) { if (device.DeviceId.Equals(deviceId)) { deviceToRemove = device; break; } } } if (deviceToRemove != null) { //Remove the WindowsHelloDevice userAccount.WindowsHelloDevices.Remove(deviceToRemove); await SaveAccountListAsync(); } return true; }
En la clase MockStore, agregue un método que agregará información relacionada de Windows Hello a un UserAccount existente. Este método se denominará "WindowsHelloUpdateDetailsAsync" y tomará parámetros para identificar al usuario y los detalles de Windows Hello. KeyAttestationResult se ha comentado al crear un WindowsHelloDevice, en una aplicación real que necesitarías.
using System.Threading.Tasks; using Windows.Security.Credentials; public async Task WindowsHelloUpdateDetailsAsync(Guid userId, Guid deviceId, byte[] publicKey, KeyCredentialAttestationResult keyAttestationResult) { UserAccount existingUserAccount = GetUserAccount(userId); if (existingUserAccount != null) { if (!existingUserAccount.WindowsHelloDevices.Any(f => f.DeviceId.Equals(deviceId))) { existingUserAccount.WindowsHelloDevices.Add(new WindowsHelloDevice() { DeviceId = deviceId, PublicKey = publicKey, // KeyAttestationResult = keyAttestationResult }); } } await SaveAccountListAsync(); }
La clase MockStore ya está completa, ya que representa la base de datos que debe considerarse privada. Para acceder a MockStore, se necesita una clase AuthService para manipular los datos de la base de datos. En la carpeta AuthService , cree una nueva clase denominada "AuthService.cs". Cambie el ámbito de clase a público y agregue un patrón de instancia singleton para asegurarse de que solo se crea una instancia.
namespace WindowsHelloLogin.AuthService { public class AuthService { // Singleton instance of the AuthService // The AuthService is a mock of what a real world server and service implementation would be private static AuthService _instance; public static AuthService Instance { get { if (null == _instance) { _instance = new AuthService(); } return _instance; } } private AuthService() { } } }
La clase AuthService debe crear una instancia de la clase MockStore y proporcionar acceso a las propiedades del objeto MockStore .
namespace WindowsHelloLogin.AuthService { public class AuthService { //Singleton instance of the AuthService //The AuthService is a mock of what a real world server and database implementation would be private static AuthService _instance; public static AuthService Instance { get { if (null == _instance) { _instance = new AuthService(); } return _instance; } } private AuthService() { } private MockStore _mockStore = new(); public Guid GetUserId(string username) { return _mockStore.GetUserId(username); } public UserAccount GetUserAccount(Guid userId) { return _mockStore.GetUserAccount(userId); } public List<UserAccount> GetUserAccountsForDevice(Guid deviceId) { return _mockStore.GetUserAccountsForDevice(deviceId); } } }
Necesita métodos en la clase AuthService para tener acceso a los métodos de detalles de Agregar, quitar y actualizar Windows Hello en el objeto MockStore . Al final de la definición de la clase AuthService , agregue los métodos siguientes.
using System.Threading.Tasks; using Windows.Security.Credentials; public async Task RegisterAsync(string username) { await _mockStore.AddAccountAsync(username); } public async Task<bool> WindowsHelloRemoveUserAsync(Guid userId) { return await _mockStore.RemoveAccountAsync(userId); } public async Task<bool> WindowsHelloRemoveDeviceAsync(Guid userId, Guid deviceId) { return await _mockStore.RemoveDeviceAsync(userId, deviceId); } public async Task WindowsHelloUpdateDetailsAsync(Guid userId, Guid deviceId, byte[] publicKey, KeyCredentialAttestationResult keyAttestationResult) { await _mockStore.WindowsHelloUpdateDetailsAsync(userId, deviceId, publicKey, keyAttestationResult); }
La clase AuthService debe proporcionar un método para validar las credenciales. Este método tomará un nombre de usuario y una contraseña y se asegurará de que la cuenta existe y la contraseña es válida. Un sistema existente tendría un método equivalente a este que comprueba que el usuario está autorizado. Agregue el siguiente método ValidateCredentials al archivo AuthService.cs.
public bool ValidateCredentials(string username, string password) { if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password)) { // This would be used for existing accounts migrating to use Windows Hello Guid userId = GetUserId(username); if (userId != Guid.Empty) { UserAccount account = GetUserAccount(userId); if (account != null) { if (string.Equals(password, account.Password)) { return true; } } } } return false; }
La clase AuthService necesita un método de desafío de solicitud que devuelva un desafío al cliente para validar si el usuario es quien dice ser. A continuación, se necesita otro método en la clase AuthService para recibir el desafío firmado de vuelta del cliente. Para este laboratorio práctico, el método de cómo se determina si se ha completado el desafío firmado se ha dejado incompleto. Cada implementación de Windows Hello en un sistema de autenticación existente será ligeramente diferente. La clave pública almacenada en el servidor debe coincidir con el resultado que devolvió el cliente al servidor. Agregue estos dos métodos a AuthService.cs.
using Windows.Security.Cryptography; using Windows.Storage.Streams; public IBuffer WindowsHelloRequestChallenge() { return CryptographicBuffer.ConvertStringToBinary("ServerChallenge", BinaryStringEncoding.Utf8); } public bool SendServerSignedChallenge(Guid userId, Guid deviceId, byte[] signedChallenge) { // Depending on your company polices and procedures this step will be different // It is at this point you will need to validate the signedChallenge that is sent back from the client. // Validation is used to ensure the correct user is trying to access this account. // The validation process will use the signedChallenge and the stored PublicKey // for the username and the specific device signin is called from. // Based on the validation result you will return a bool value to allow access to continue or to block the account. // For this sample validation will not happen as a best practice solution does not apply and will need to // be configured for each company. // Simply just return true. // You could get the User's Public Key with something similar to the following: byte[] userPublicKey = _mockStore.GetPublicKey(userId, deviceId); return true; }
Ejercicio 2: Lógica del lado cliente
En este ejercicio, cambiará las vistas del lado cliente y las clases auxiliares del primer laboratorio para usar la clase AuthService . En el mundo real, AuthService sería el servidor de autenticación y tendría que usar la API web para enviar y recibir datos del servidor. Para este laboratorio práctico, tanto el cliente como el servidor son locales para simplificar las cosas. El objetivo es aprender a usar las API de Windows Hello.
En el MainPage.xaml.cs, puede quitar la llamada al método AccountHelper.LoadAccountListAsync en el método cargado, ya que la clase AuthService crea una instancia de MockStore para cargar la lista de cuentas. El
Loaded
método debería tener ahora un aspecto similar al fragmento de código siguiente. Tenga en cuenta que la definición del método asincrónico se quita a medida que no se espera nada.private void MainPage_Loaded(object sender, RoutedEventArgs e) { Frame.Navigate(typeof(UserSelection)); }
Actualice la interfaz de la página de inicio de sesión para requerir que se escriba una contraseña. En este laboratorio práctico se muestra cómo se podría migrar un sistema existente para usar Windows Hello y las cuentas existentes tendrán un nombre de usuario y una contraseña. Actualice también la explicación en la parte inferior del XAML para incluir la contraseña predeterminada. Actualice el código XAML siguiente en Login.xaml.
<Grid> <StackPanel> <TextBlock Text="Login" FontSize="36" Margin="4" TextAlignment="Center"/> <TextBlock x:Name="ErrorMessage" Text="" FontSize="20" Margin="4" Foreground="Red" TextAlignment="Center"/> <TextBlock Text="Enter your credentials below" Margin="0,0,0,20" TextWrapping="Wrap" Width="300" TextAlignment="Center" VerticalAlignment="Center" FontSize="16"/> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> <!-- Username Input --> <TextBlock x:Name="UserNameTextBlock" Text="Username: " FontSize="20" Margin="4" Width="100"/> <TextBox x:Name="UsernameTextBox" PlaceholderText="sampleUsername" Width="200" Margin="4"/> </StackPanel> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> <!-- Password Input --> <TextBlock x:Name="PasswordTextBlock" Text="Password: " FontSize="20" Margin="4" Width="100"/> <PasswordBox x:Name="PasswordBox" PlaceholderText="samplePassword" Width="200" Margin="4"/> </StackPanel> <Button x:Name="LoginButton" Content="Login" Background="DodgerBlue" Foreground="White" Click="LoginButton_Click" Width="80" HorizontalAlignment="Center" Margin="0,20"/> <TextBlock Text="Don't have an account?" TextAlignment="Center" VerticalAlignment="Center" FontSize="16"/> <TextBlock x:Name="RegisterButtonTextBlock" Text="Register now" PointerPressed="RegisterButtonTextBlock_OnPointerPressed" Foreground="DodgerBlue" TextAlignment="Center" VerticalAlignment="Center" FontSize="16"/> <Border x:Name="WindowsHelloStatus" Background="#22B14C" Margin="0,20" Height="100"> <TextBlock x:Name="WindowsHelloStatusText" Text="Windows Hello is ready to use!" Margin="4" TextAlignment="Center" VerticalAlignment="Center" FontSize="20"/> </Border> <TextBlock x:Name="LoginExplanation" FontSize="24" TextAlignment="Center" TextWrapping="Wrap" Text="Please Note: To demonstrate a login, validation will only occur using the default username 'sampleUsername' and default password 'samplePassword'"/> </StackPanel> </Grid>
En el archivo de código subyacente de la clase Login, deberá cambiar la
Account
variable privada en la parte superior de la clase para que sea .UserAccount
Cambie elOnNavigateTo
evento para convertir el tipo como .UserAccount
También necesitará la siguiente instrucción using.using WindowsHelloLogin.AuthService; namespace WindowsHelloLogin.Views { public sealed partial class Login : Page { private UserAccount _account; private bool _isExistingAccount; public Login() { this.InitializeComponent(); } protected override async void OnNavigatedTo(NavigationEventArgs e) { //Check Windows Hello is setup and available on this machine if (await WindowsHelloHelper.WindowsHelloAvailableCheckAsync()) { if (e.Parameter != null) { _isExistingAccount = true; //Set the account to the existing account being passed in _account = (UserAccount)e.Parameter; UsernameTextBox.Text = _account.Username; await SignInWindowsHelloAsync(); } } } private async void LoginButton_Click(object sender, RoutedEventArgs e) { ErrorMessage.Text = ""; await SignInWindowsHelloAsync(); } } }
Como la página Inicio de sesión usa un
UserAccount
objeto en lugar del objeto anteriorAccount
, el WindowsHelloHelper.cs deberá actualizarse para usar unUserAccount
como tipo de parámetro para algunos métodos. Deberá cambiar los parámetros siguientes para los métodos CreateWindowsHelloKeyAsync, RemoveWindowsHelloAccountAsync y GetWindowsHelloAuthenticationMessageAsync. Como laUserAccount
clase tiene unGuid
para un UserId, comenzará a usar el identificador en más lugares para ser más precisos.public static async Task<bool> CreateWindowsHelloKeyAsync(Guid userId, string username) { KeyCredentialRetrievalResult keyCreationResult = await KeyCredentialManager.RequestCreateAsync(username, KeyCredentialCreationOption.ReplaceExisting); return true; } public static async void RemoveWindowsHelloAccountAsync(UserAccount account) { } public static async Task<bool> GetWindowsHelloAuthenticationMessageAsync(UserAccount account) { KeyCredentialRetrievalResult openKeyResult = await KeyCredentialManager.OpenAsync(account.Username); //Calling OpenAsync will allow the user access to what is available in the app and will not require user credentials again. //If you wanted to force the user to sign in again you can use the following: //var consentResult = await Windows.Security.Credentials.UI.UserConsentVerifier.RequestVerificationAsync(account.Username); //This will ask for the either the password of the currently signed in Microsoft Account or the PIN used for Windows Hello. if (openKeyResult.Status == KeyCredentialStatus.Success) { //If OpenAsync has succeeded, the next thing to think about is whether the client application requires access to backend services. //If it does here you would Request a challenge from the Server. The client would sign this challenge and the server //would check the signed challenge. If it is correct it would allow the user access to the backend. //You would likely make a new method called RequestSignAsync to handle all this //for example, RequestSignAsync(openKeyResult); //Refer to the second Windows Hello sample for information on how to do this. //For this sample there is not concept of a server implemented so just return true. return true; } else if (openKeyResult.Status == KeyCredentialStatus.NotFound) { //If the _account is not found at this stage. It could be one of two errors. //1. Windows Hello has been disabled //2. Windows Hello has been disabled and re-enabled cause the Windows Hello Key to change. //Calling CreateWindowsHelloKeyAsync and passing through the account will attempt to replace the existing Windows Hello Key for that account. //If the error really is that Windows Hello is disabled then the CreateWindowsHelloKeyAsync method will output that error. if (await CreateWindowsHelloKeyAsync(account.UserId, account.Username)) { //If the Windows Hello Key was again successfully created, Windows Hello has just been reset. //Now that the Windows Hello Key has been reset for the account retry sign in. return await GetWindowsHelloAuthenticationMessageAsync(account); } } // Can't use Windows Hello right now, try again later return false; }
El método SignInWindowsHelloAsync en Login.xaml.cs archivo deberá actualizarse para usar AuthService en lugar de AccountHelper. La validación de credenciales se realizará a través de AuthService. Para este laboratorio práctico, la única cuenta configurada es "sampleUsername". Esta cuenta se crea en el método InitializeSampleUserAccountsAsync en MockStore.cs. Actualice el método SignInWindowsHelloAsync en Login.xaml.cs ahora para reflejar el fragmento de código siguiente.
private async Task SignInWindowsHelloAsync() { if (_isExistingAccount) { if (await WindowsHelloHelper.GetWindowsHelloAuthenticationMessageAsync(_account)) { Frame.Navigate(typeof(Welcome), _account); } } else if (AuthService.AuthService.Instance.ValidateCredentials(UsernameTextBox.Text, PasswordBox.Password)) { Guid userId = AuthService.AuthService.Instance.GetUserId(UsernameTextBox.Text); if (userId != Guid.Empty) { //Now that the account exists on server try and create the necessary details and add them to the account if (await WindowsHelloHelper.CreateWindowsHelloKeyAsync(userId, UsernameTextBox.Text)) { Debug.WriteLine("Successfully signed in with Windows Hello!"); //Navigate to the Welcome Screen. _account = AuthService.AuthService.Instance.GetUserAccount(userId); Frame.Navigate(typeof(Welcome), _account); } else { //The Windows Hello account creation failed. //Remove the account from the server as the details were not configured await AuthService.AuthService.Instance.WindowsHelloRemoveUserAsync(userId); ErrorMessage.Text = "Account Creation Failed"; } } } else { ErrorMessage.Text = "Invalid Credentials"; } }
Como Windows Hello creará un par de claves pública y privada diferente para cada cuenta de cada dispositivo, la página principal deberá mostrar una lista de dispositivos registrados para la cuenta de inicio de sesión y permitir que se olvide cada una de ellas. En Welcome.xaml, agregue el siguiente XAML debajo de .
ForgetButton
Esto implementará un botón olvidar dispositivo, un área de texto de error y una lista para mostrar todos los dispositivos.<Grid> <StackPanel> <TextBlock x:Name="Title" Text="Welcome" FontSize="40" TextAlignment="Center"/> <TextBlock x:Name="UserNameText" FontSize="28" TextAlignment="Center"/> <Button x:Name="BackToUserListButton" Content="Back to User List" Click="Button_Restart_Click" HorizontalAlignment="Center" Margin="0,20" Foreground="White" Background="DodgerBlue"/> <Button x:Name="ForgetButton" Content="Forget Me" Click="Button_Forget_User_Click" Foreground="White" Background="Gray" HorizontalAlignment="Center"/> <Button x:Name="ForgetDeviceButton" Content="Forget Device" Click="Button_Forget_Device_Click" Foreground="White" Background="Gray" Margin="0,40,0,20" HorizontalAlignment="Center"/> <TextBlock x:Name="ForgetDeviceErrorTextBlock" Text="Select a device first" TextWrapping="Wrap" Width="300" Foreground="Red" TextAlignment="Center" VerticalAlignment="Center" FontSize="16" Visibility="Collapsed"/> <ListView x:Name="UserListView" MaxHeight="500" MinWidth="350" Width="350" HorizontalAlignment="Center"> <ListView.ItemTemplate> <DataTemplate> <Grid Background="Gray" Height="50" Width="350" HorizontalAlignment="Center" VerticalAlignment="Stretch" > <TextBlock Text="{Binding DeviceId}" HorizontalAlignment="Center" TextAlignment="Center" VerticalAlignment="Center" Foreground="White"/> </Grid> </DataTemplate> </ListView.ItemTemplate> </ListView> </StackPanel> </Grid>
En el archivo Welcome.xaml.cs, debe cambiar la variable privada
Account
en la parte superior de la clase para que sea una variable privadaUserAccount
. A continuación, actualice elOnNavigatedTo
método para usar AuthService y recuperar información de la cuenta actual. Cuando tenga la información de la cuenta, puede establecer laItemsSource
de la lista para mostrar los dispositivos. Deberá agregar una referencia al espacio de nombres AuthService .using WindowsHelloLogin.AuthService; namespace WindowsHelloLogin.Views { public sealed partial class Welcome : Page { private UserAccount _activeAccount; public Welcome() { InitializeComponent(); } protected override void OnNavigatedTo(NavigationEventArgs e) { _activeAccount = (UserAccount)e.Parameter; if (_activeAccount != null) { UserAccount account = AuthService.AuthService.Instance.GetUserAccount(_activeAccount.UserId); if (account != null) { UserListView.ItemsSource = account.WindowsHelloDevices; UserNameText.Text = account.Username; } } } } }
Como usará AuthService al quitar una cuenta, se puede quitar la referencia a AccountHelper en el
Button_Forget_User_Click
método . El método debería tener ahora el siguiente aspecto.private async void Button_Forget_User_Click(object sender, RoutedEventArgs e) { //Remove it from Windows Hello await WindowsHelloHelper.RemoveWindowsHelloAccountAsync(_activeAccount); Debug.WriteLine($"User {_activeAccount.Username} deleted."); //Navigate back to UserSelection page. Frame.Navigate(typeof(UserSelection)); }
El método WindowsHelloHelper no usa AuthService para quitar la cuenta. Debe realizar una llamada a AuthService y pasar el userId.
public static async void RemoveWindowsHelloAccountAsync(UserAccount account) { //Open the account with Windows Hello KeyCredentialRetrievalResult keyOpenResult = await KeyCredentialManager.OpenAsync(account.Username); if (keyOpenResult.Status == KeyCredentialStatus.Success) { // In the real world you would send key information to server to unregister await AuthService.AuthService.Instance.WindowsHelloRemoveUserAsync(account.UserId); } //Then delete the account from the machines list of Windows Hello Accounts await KeyCredentialManager.DeleteAsync(account.Username); }
Para poder terminar de implementar la página principal , debe crear un método en WindowsHelloHelper.cs que permitirá quitar un dispositivo. Cree un nuevo método que llamará a WindowsHelloRemoveDeviceAsync en AuthService.
public static async Task RemoveWindowsHelloDeviceAsync(UserAccount account, Guid deviceId) { await AuthService.AuthService.Instance.WindowsHelloRemoveDeviceAsync(account.UserId, deviceId); }
En Welcome.xaml.cs, implemente el controlador de eventos Button_Forget_Device_Click . Esto usará el dispositivo seleccionado de la lista de dispositivos y usará el asistente de Windows Hello para llamar a quitar dispositivo. Recuerde que el controlador de eventos es asincrónico.
private async void Button_Forget_Device_Click(object sender, RoutedEventArgs e) { WindowsHelloDevice selectedDevice = UserListView.SelectedItem as WindowsHelloDevice; if (selectedDevice != null) { //Remove it from Windows Hello await WindowsHelloHelper.RemoveWindowsHelloDeviceAsync(_activeAccount, selectedDevice.DeviceId); Debug.WriteLine($"User {_activeAccount.Username} deleted."); if (!UserListView.Items.Any()) { //Navigate back to UserSelection page. Frame.Navigate(typeof(UserSelection)); } } else { ForgetDeviceErrorTextBlock.Visibility = Visibility.Visible; } }
La página siguiente que actualizará es la página UserSelection . La página UserSelection deberá usar AuthService para recuperar todas las cuentas de usuario del dispositivo actual. Actualmente, no hay ninguna manera de obtener un identificador de dispositivo para pasar a AuthService para que pueda devolver cuentas de usuario para ese dispositivo. En la carpeta Utils , cree una nueva clase denominada "Helpers.cs". Cambie el ámbito de clase para que sea estático público y agregue el siguiente método que le permitirá recuperar el identificador de dispositivo actual.
using System; using Windows.Security.ExchangeActiveSyncProvisioning; namespace WindowsHelloLogin.Utils { public static class Helpers { public static Guid GetDeviceId() { //Get the Device ID to pass to the server var deviceInformation = new EasClientDeviceInformation(); return deviceInformation.Id; } } }
En la clase de página UserSelection , solo es necesario cambiar el código subyacente, no la interfaz de usuario. En UserSelection.xaml.cs, actualice el método UserSelection_Loaded y el método UserSelectionChanged para usar la
UserAccount
clase en lugar de laAccount
clase . También deberá obtener todos los usuarios para este dispositivo a través de AuthService.using System.Linq; using WindowsHelloLogin.AuthService; namespace WindowsHelloLogin.Views { public sealed partial class UserSelection : Page { public UserSelection() { InitializeComponent(); Loaded += UserSelection_Loaded; } private void UserSelection_Loaded(object sender, RoutedEventArgs e) { List<UserAccount> accounts = AuthService.AuthService.Instance.GetUserAccountsForDevice(Helpers.GetDeviceId()); if (accounts.Any()) { UserListView.ItemsSource = accounts; UserListView.SelectionChanged += UserSelectionChanged; } else { //If there are no accounts navigate to the Login page Frame.Navigate(typeof(Login)); } } /// <summary> /// Function called when an account is selected in the list of accounts /// Navigates to the Login page and passes the chosen account /// </summary> private void UserSelectionChanged(object sender, RoutedEventArgs e) { if (((ListView)sender).SelectedValue != null) { UserAccount account = (UserAccount)((ListView)sender).SelectedValue; if (account != null) { Debug.WriteLine($"Account {account.Username} selected!"); } Frame.Navigate(typeof(Login), account); } } } }
La página WindowsHelloRegister debe tener actualizado el archivo de código subyacente. La interfaz de usuario no necesita ningún cambio. En WindowsHelloRegister.xaml.cs, quite la variable privada
Account
en la parte superior de la clase, ya que ya no es necesaria. Actualice el controlador de eventos RegisterButton_Click_Async para usar AuthService. Este método creará un nuevo UserAccount y, a continuación, probará y actualizará sus detalles de la cuenta. Si Windows Hello no puede crear una clave, la cuenta se quitará como error en el proceso de registro.private async void RegisterButton_Click_Async(object sender, RoutedEventArgs e) { ErrorMessage.Text = ""; //Validate entered credentials are acceptable if (!string.IsNullOrEmpty(UsernameTextBox.Text)) { //Register an Account on the AuthService so that we can get back a userId await AuthService.AuthService.Instance.RegisterAsync(UsernameTextBox.Text); Guid userId = AuthService.AuthService.Instance.GetUserId(UsernameTextBox.Text); if (userId != Guid.Empty) { //Now that the account exists on server try and create the necessary details and add them to the account if (await WindowsHelloHelper.CreateWindowsHelloKeyAsync(userId, UsernameTextBox.Text)) { //Navigate to the Welcome Screen. Frame.Navigate(typeof(Welcome), AuthService.AuthService.Instance.GetUserAccount(userId)); } else { //The Windows Hello account creation failed. //Remove the account from the server as the details were not configured await AuthService.AuthService.Instance.WindowsHelloRemoveUserAsync(userId); ErrorMessage.Text = "Account Creation Failed"; } } } else { ErrorMessage.Text = "Please enter a username"; } }
Compile y ejecute la aplicación. Inicie sesión en la cuenta de usuario de ejemplo con las credenciales "sampleUsername" y "samplePassword". En la pantalla de bienvenida, puede observar que se muestra el botón Olvidar dispositivos, pero no hay dispositivos. Al crear o migrar un usuario para trabajar con Windows Hello, la información de la cuenta no se inserta en AuthService.
Para obtener la información de la cuenta de Windows Hello en AuthService, el WindowsHelloHelper.cs deberá actualizarse. En el método CreateWindowsHelloKeyAsync, en lugar de devolver
true
solo en caso de que se realice correctamente, deberá llamar a un nuevo método que intentará obtener keyAttestation. Aunque este laboratorio práctico no está grabando esta información en AuthService, obtendrá información sobre cómo obtener esta información en el lado cliente. Actualice el método CreateWindowsHelloKeyAsync de la siguiente manera:public static async Task<bool> CreateWindowsHelloKeyAsync(Guid userId, string username) { KeyCredentialRetrievalResult keyCreationResult = await KeyCredentialManager.RequestCreateAsync(username, KeyCredentialCreationOption.ReplaceExisting); switch (keyCreationResult.Status) { case KeyCredentialStatus.Success: Debug.WriteLine("Successfully made key"); await GetKeyAttestationAsync(userId, keyCreationResult); return true; case KeyCredentialStatus.UserCanceled: Debug.WriteLine("User cancelled sign-in process."); break; case KeyCredentialStatus.NotFound: // User needs to setup Windows Hello Debug.WriteLine($"Windows Hello is not set up!{Environment.NewLine}Please go to Windows Settings and set up a PIN to use it."); break; default: break; } return false; }
Cree un método GetKeyAttestationAsync en WindowsHelloHelper.cs. Este método mostrará cómo obtener toda la información necesaria que puede proporcionar Windows Hello para cada cuenta en un dispositivo específico.
using Windows.Storage.Streams; private static async Task GetKeyAttestationAsync(Guid userId, KeyCredentialRetrievalResult keyCreationResult) { KeyCredential userKey = keyCreationResult.Credential; IBuffer publicKey = userKey.RetrievePublicKey(); KeyCredentialAttestationResult keyAttestationResult = await userKey.GetAttestationAsync(); IBuffer keyAttestation = null; IBuffer certificateChain = null; bool keyAttestationIncluded = false; bool keyAttestationCanBeRetrievedLater = false; KeyCredentialAttestationStatus keyAttestationRetryType = 0; if (keyAttestationResult.Status == KeyCredentialAttestationStatus.Success) { keyAttestationIncluded = true; keyAttestation = keyAttestationResult.AttestationBuffer; certificateChain = keyAttestationResult.CertificateChainBuffer; Debug.WriteLine("Successfully made key and attestation"); } else if (keyAttestationResult.Status == KeyCredentialAttestationStatus.TemporaryFailure) { keyAttestationRetryType = KeyCredentialAttestationStatus.TemporaryFailure; keyAttestationCanBeRetrievedLater = true; Debug.WriteLine("Successfully made key but not attestation"); } else if (keyAttestationResult.Status == KeyCredentialAttestationStatus.NotSupported) { keyAttestationRetryType = KeyCredentialAttestationStatus.NotSupported; keyAttestationCanBeRetrievedLater = false; Debug.WriteLine("Key created, but key attestation not supported"); } Guid deviceId = Helpers.GetDeviceId(); //Update the Windows Hello details with the information we have just fetched above. //await UpdateWindowsHelloDetailsAsync(userId, deviceId, publicKey.ToArray(), keyAttestationResult); }
Es posible que haya observado en el método GetKeyAttestationAsync que acaba de agregar la última línea fue comentada. Esta última línea será un nuevo método que cree que enviará toda la información de Windows Hello al servicio AuthService. En el mundo real, tendría que enviar esto a un servidor real a través de una API web.
using System.Runtime.InteropServices.WindowsRuntime; using System.Threading.Tasks; public static async Task<bool> UpdateWindowsHelloDetailsAsync(Guid userId, Guid deviceId, byte[] publicKey, KeyCredentialAttestationResult keyAttestationResult) { //In the real world, you would use an API to add Windows Hello signing info to server for the signed in account. //For this tutorial, we do not implement a Web API for our server and simply mock the server locally. //The CreateWindowsHelloKey method handles adding the Windows Hello account locally to the device using the KeyCredential Manager //Using the userId the existing account should be found and updated. await AuthService.AuthService.Instance.WindowsHelloUpdateDetailsAsync(userId, deviceId, publicKey, keyAttestationResult); return true; }
Quite la marca de comentario de la última línea del método GetKeyAttestationAsync para que la información de Windows Hello se envíe al AuthService.
Compile y ejecute la aplicación e inicie sesión con las credenciales predeterminadas como antes. En la página principal , verá que se muestra el identificador del dispositivo. Si ha iniciado sesión en otro dispositivo que también se mostrará aquí (si tuviera un servicio de autenticación hospedado en la nube). Para este laboratorio práctico, se muestra el identificador real del dispositivo. En una implementación real, le gustaría mostrar un nombre descriptivo que una persona pudiera comprender y usar para identificar cada dispositivo.
Para completar este laboratorio práctico, necesita una solicitud y un desafío para el usuario cuando seleccione en la página de selección de usuario e inicie sesión de nuevo. AuthService tiene dos métodos que creó para solicitar un desafío, uno que usa un desafío firmado. En WindowsHelloHelper.cs, cree un nuevo método denominado RequestSignAsync. Esto solicitará un desafío de AuthService, firmará localmente ese desafío mediante una API de Windows Hello y enviará el desafío firmado a AuthService. En este laboratorio práctico, AuthService recibirá el desafío firmado y devolverá
true
. En una implementación real, tendría que implementar un mecanismo de comprobación para determinar si el usuario correcto firmó el desafío en el dispositivo correcto. Agregue el método siguiente al WindowsHelloHelper.csprivate static async Task<bool> RequestSignAsync(Guid userId, KeyCredentialRetrievalResult openKeyResult) { // Calling userKey.RequestSignAsync() prompts the uses to enter the PIN or use Biometrics (Windows Hello). // The app would use the private key from the user account to sign the sign-in request (challenge) // The client would then send it back to the server and await the servers response. IBuffer challengeMessage = AuthService.AuthService.Instance.WindowsHelloRequestChallenge(); KeyCredential userKey = openKeyResult.Credential; KeyCredentialOperationResult signResult = await userKey.RequestSignAsync(challengeMessage); if (signResult.Status == KeyCredentialStatus.Success) { // If the challenge from the server is signed successfully // send the signed challenge back to the server and await the servers response return AuthService.AuthService.Instance.SendServerSignedChallenge( userId, Helpers.GetDeviceId(), signResult.Result.ToArray()); } else if (signResult.Status == KeyCredentialStatus.UserCanceled) { // User cancelled the Windows Hello PIN entry. } else if (signResult.Status == KeyCredentialStatus.NotFound) { // Must recreate Windows Hello key } else if (signResult.Status == KeyCredentialStatus.SecurityDeviceLocked) { // Can't use Windows Hello right now, remember that hardware failed and suggest restart } else if (signResult.Status == KeyCredentialStatus.UnknownError) { // Can't use Windows Hello right now, try again later } return false; }
En la clase WindowsHelloHelper , llame al método RequestSignAsync desde el método GetWindowsHelloAuthenticationMessageAsync .
public static async Task<bool> GetWindowsHelloAuthenticationMessageAsync(UserAccount account) { KeyCredentialRetrievalResult openKeyResult = await KeyCredentialManager.OpenAsync(account.Username); // Calling OpenAsync will allow the user access to what is available in the app and will not require user credentials again. // If you wanted to force the user to sign in again you can use the following: // var consentResult = await Windows.Security.Credentials.UI.UserConsentVerifier.RequestVerificationAsync(account.Username); // This will ask for the either the password of the currently signed in Microsoft Account or the PIN used for Windows Hello. if (openKeyResult.Status == KeyCredentialStatus.Success) { //If OpenAsync has succeeded, the next thing to think about is whether the client application requires access to backend services. //If it does here you would Request a challenge from the Server. The client would sign this challenge and the server //would check the signed challenge. If it is correct it would allow the user access to the backend. //You would likely make a new method called RequestSignAsync to handle all this //for example, RequestSignAsync(openKeyResult); //Refer to the second Windows Hello sample for information on how to do this. return await RequestSignAsync(account.UserId, openKeyResult); } else if (openKeyResult.Status == KeyCredentialStatus.NotFound) { //If the _account is not found at this stage. It could be one of two errors. //1. Windows Hello has been disabled //2. Windows Hello has been disabled and re-enabled cause the Windows Hello Key to change. //Calling CreateWindowsHelloKeyAsync and passing through the account will attempt to replace the existing Windows Hello Key for that account. //If the error really is that Windows Hello is disabled then the CreateWindowsHelloKeyAsync method will output that error. if (await CreateWindowsHelloKeyAsync(account.UserId, account.Username)) { //If the Windows Hello Key was again successfully created, Windows Hello has just been reset. //Now that the Windows Hello Key has been reset for the _account retry sign in. return await GetWindowsHelloAuthenticationMessageAsync(account); } } // Can't use Windows Hello right now, try again later return false; }
En este ejercicio, ha actualizado la aplicación del lado cliente para usar AuthService. Al hacerlo, ha podido eliminar la necesidad de la clase Account y la clase AccountHelper . Elimine la clase Account , la carpeta Models y la clase AccountHelper en la carpeta Utils . Deberá quitar toda la referencia al
WindowsHelloLogin.Models
espacio de nombres en toda la aplicación antes de que la solución se compile correctamente.Compile y ejecute la aplicación y disfrute de usar Windows Hello con el servicio y la base de datos ficticios.
En este laboratorio práctico, ha aprendido a usar las API de Windows Hello para reemplazar la necesidad de contraseñas al usar la autenticación desde una máquina Windows. Cuando consideres la cantidad de energía que gastan las personas que mantienen contraseñas y admiten contraseñas perdidas en sistemas existentes, deberías ver la ventaja de pasar a este nuevo sistema de autenticación de Windows Hello.
Hemos dejado como ejercicio los detalles de cómo implementará la autenticación en el servicio y el servidor. Se espera que la mayoría de los desarrolladores tengan sistemas existentes que deban migrarse para empezar a trabajar con Windows Hello. Los detalles de cada uno de estos sistemas variarán.