Créer un service de connexion Windows Hello
Il s’agit de la deuxième partie d’une procédure pas à pas complète sur l’utilisation de Windows Hello comme alternative aux systèmes d’authentification de nom d’utilisateur et de mot de passe traditionnels dans les applications Windows empaquetées. Cet article récupère l’emplacement où la partie 1, l’application de connexion Windows Hello, est désactivée et étend les fonctionnalités pour montrer comment intégrer Windows Hello à votre application existante.
Pour générer ce projet, vous aurez besoin d’une expérience avec C# et XAML. Vous devez également utiliser Visual Studio 2022 sur un ordinateur Windows 10 ou Windows 11. Consultez Prise en main de WinUI pour obtenir des instructions complètes sur la configuration de votre environnement de développement.
Exercice 1 : Logique côté serveur
Dans cet exercice, vous commencez par l’application Windows Hello créée dans le premier labo et créez un serveur fictif local et une base de données. Ce laboratoire pratique est conçu pour apprendre comment Windows Hello peut être intégré à un système existant. À l’aide d’un serveur fictif et d’une base de données fictive, une grande partie de la configuration non liée est éliminée. Dans vos propres applications, vous devez remplacer les objets fictifs par les services et bases de données réels.
Pour commencer, ouvrez la solution WindowsHelloLogin à partir du premier laboratoire Windows Hello Hands On Lab.
Vous commencerez par implémenter le serveur fictif et la base de données fictive. Créez un dossier nommé « AuthService ». Dans Explorateur de solutions, cliquez avec le bouton droit sur le projet WindowsHelloLogin, puis sélectionnez Ajouter>un nouveau dossier.
Créez des classes UserAccount et WindowsHelloDevices qui serviront de modèles pour que les données soient enregistrées dans la base de données fictif. UserAccount sera similaire au modèle utilisateur implémenté sur un serveur d’authentification traditionnel. Cliquez avec le bouton droit sur le dossier AuthService et ajoutez une nouvelle classe nommée « UserAccount ».
Modifiez l’étendue de classe pour qu’elle soit publique et ajoutez les propriétés publiques suivantes pour la classe UserAccount . Vous devez ajouter une instruction using pour l’espace
System.ComponentModel.DataAnnotations
de noms.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(); } }
Vous avez peut-être remarqué la liste commentée de WindowsHelloDevices. Il s’agit d’une modification que vous devez apporter à un modèle utilisateur existant dans votre implémentation actuelle. La liste de WindowsHelloDevices contient un deviceID, la clé publique effectuée à partir de Windows Hello et une KeyCredentialAttestationResult. Pour cet exercice, vous devez implémenter la cléAttestationResult , car elles ne sont fournies que par Windows Hello sur les appareils dotés d’une puce TPM (modules de plateforme sécurisée). KeyCredentialAttestationResult est une combinaison de plusieurs propriétés et doit être fractionnée pour les enregistrer et les charger avec une base de données.
Créez une classe dans le dossier AuthService appelé « WindowsHelloDevice.cs ». Il s’agit du modèle des appareils Windows Hello, comme indiqué ci-dessus. Modifiez l’étendue de classe pour qu’elle soit publique et ajoutez les propriétés suivantes.
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; } } }
Revenez à UserAccount.cs et supprimez les marques de commentaire de la liste des appareils 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(); } }
Avec le modèle pour UserAccount et WindowsHelloDevice créé, vous devez créer une autre classe dans le dossier AuthService qui servira de base de données fictif, car il s’agit d’une base de données fictif à partir de laquelle vous allez enregistrer et charger une liste de comptes d’utilisateur localement. Dans le monde réel, il s’agit de votre implémentation de base de données. Créez une classe dans le dossier AuthService nommé « MockStore.cs ». Modifiez l’étendue de la classe en public.
Comme le magasin fictif enregistre et charge une liste de comptes d’utilisateur localement, vous pouvez implémenter la logique pour enregistrer et charger cette liste à l’aide d’un xmlSerializer. Vous devez également mémoriser le nom de fichier et enregistrer l’emplacement. Dans MockStore.cs implémenter les éléments suivants :
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 } }
Dans la méthode LoadAccountListAsync , vous avez peut-être remarqué qu’une méthode InitializeSampleUserAccountsAsync a été commentée. Vous devez créer cette méthode dans le MockStore.cs. Cette méthode remplit la liste des comptes d’utilisateur afin qu’une connexion puisse avoir lieu. Dans le monde réel, la base de données utilisateur serait déjà remplie. Dans cette étape, vous allez également créer un constructeur qui initialise la liste des utilisateurs et appelle 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(); } } }
Maintenant que la méthode InitializeSampleUserAccountsAsync existe, supprimez les marques de commentaire de l’appel de méthode dans la méthode 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 liste des comptes d’utilisateur dans le magasin fictif peut désormais être enregistrée et chargée. D’autres parties de l’application devront avoir accès à cette liste afin qu’il y ait des méthodes pour récupérer ces données. Sous la méthode InitializeSampleUserAccountsAsync , ajoutez les méthodes suivantes pour obtenir des données. Ils vous permettront d’obtenir un ID d’utilisateur, un seul utilisateur, une liste d’utilisateurs pour un appareil Windows Hello spécifique et d’obtenir également la clé publique pour l’utilisateur sur un appareil spécifique.
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; }
Les méthodes suivantes à implémenter gèrent des opérations simples pour ajouter un compte, supprimer un compte et également supprimer un appareil. La suppression d’un appareil est nécessaire, car Windows Hello est spécifique à l’appareil. Pour chaque appareil auquel vous vous connectez, une nouvelle paire de clés publique et privée sera créée par Windows Hello. C’est comme avoir un mot de passe différent pour chaque appareil que vous connectez, la seule chose est que vous n’avez pas besoin de mémoriser tous ces mots de passe ; le serveur le fait. Ajoutez les méthodes suivantes dans la 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; }
Dans la classe MockStore, ajoutez une méthode qui ajoute des informations associées à Windows Hello à un UserAccount existant. Cette méthode sera appelée « WindowsHelloUpdateDetailsAsync » et prendra des paramètres pour identifier l’utilisateur et les détails de Windows Hello. KeyAttestationResult a été commenté lors de la création d’une application WindowsHelloDevice, dans une application réelle dont vous avez besoin.
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 classe MockStore est maintenant terminée, car elle représente la base de données qu’elle doit être considérée comme privée. Pour accéder au MockStore, une classe AuthService est nécessaire pour manipuler les données de base de données. Dans le dossier AuthService , créez une classe appelée « AuthService.cs ». Modifiez l’étendue de classe en public et ajoutez un modèle d’instance singleton pour vous assurer qu’une seule instance n’est jamais créée.
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 classe AuthService doit créer une instance de la classe MockStore et fournir un accès aux propriétés de l’objet 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); } } }
Vous avez besoin de méthodes dans la classe AuthService pour accéder aux méthodes d’ajout, de suppression et de mise à jour des méthodes de détails Windows Hello dans l’objet MockStore . À la fin de la définition de classe AuthService , ajoutez les méthodes suivantes.
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 classe AuthService doit fournir une méthode pour valider les informations d’identification. Cette méthode prend un nom d’utilisateur et un mot de passe et vérifie que le compte existe et que le mot de passe est valide. Un système existant aurait une méthode équivalente à celle-ci qui vérifie que l’utilisateur est autorisé. Ajoutez la méthode ValidateCredentials suivante au fichier 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 classe AuthService a besoin d’une méthode de défi de requête qui retourne un défi au client pour vérifier si l’utilisateur est celui qu’il prétend être. Ensuite, une autre méthode est nécessaire dans la classe AuthService pour recevoir le défi signé du client. Pour ce labo pratique, la méthode de détermination de l’achèvement du défi signé a été laissée incomplète. Chaque implémentation de Windows Hello dans un système d’authentification existant sera légèrement différente. La clé publique stockée sur le serveur doit correspondre au résultat retourné par le client au serveur. Ajoutez ces deux méthodes à 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; }
Exercice 2 : Logique côté client
Dans cet exercice, vous allez modifier les vues côté client et les classes d’assistance du premier laboratoire pour utiliser la classe AuthService . Dans le monde réel, AuthService serait le serveur d’authentification et vous devez utiliser les API web pour envoyer et recevoir des données du serveur. Pour ce labo pratique, le client et le serveur sont tous les deux locaux pour garder les choses simples. L’objectif est d’apprendre à utiliser les API Windows Hello.
Dans le MainPage.xaml.cs, vous pouvez supprimer l’appel de méthode AccountHelper.LoadAccountListAsync dans la méthode chargée, car la classe AuthService crée une instance du MockStore pour charger la liste des comptes. La
Loaded
méthode doit maintenant ressembler à l’extrait de code ci-dessous. Notez que la définition de méthode asynchrone est supprimée, car rien n’est attendu.private void MainPage_Loaded(object sender, RoutedEventArgs e) { Frame.Navigate(typeof(UserSelection)); }
Mettez à jour l’interface de page de connexion pour exiger l’entrée d’un mot de passe. Cette pratique montre comment un système existant peut être migré pour utiliser Windows Hello et les comptes existants aura un nom d’utilisateur et un mot de passe. Mettez également à jour l’explication en bas du code XAML pour inclure le mot de passe par défaut. Mettez à jour le code XAML suivant dans 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>
Dans le fichier code-behind de la classe Login , vous devez modifier la
Account
variable privée en haut de la classe comme étant unUserAccount
. Modifiez l’événementOnNavigateTo
pour convertir le type en unUserAccount
. Vous aurez également besoin de l’instruction using suivante.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(); } } }
Comme la page Login utilise un
UserAccount
objet au lieu de l’objet précédentAccount
, la WindowsHelloHelper.cs doit être mise à jour pour utiliser unUserAccount
type de paramètre comme type de paramètre pour certaines méthodes. Vous devez modifier les paramètres suivants pour les méthodes CreateWindowsHelloKeyAsync, RemoveWindowsHelloAccountAsync et GetWindowsHelloAuthenticationMessageAsync. Étant donné que laUserAccount
classe a unGuid
id utilisateur, vous commencerez à utiliser l’ID dans d’autres endroits pour être plus précis.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; }
La méthode SignInWindowsHelloAsync dans Login.xaml.cs fichier doit être mise à jour pour utiliser AuthService au lieu de AccountHelper. La validation des informations d’identification se produit via AuthService. Pour ce labo, le seul compte configuré est « sampleUsername ». Ce compte est créé dans la méthode InitializeSampleUserAccountsAsync dans MockStore.cs. Mettez à jour la méthode SignInWindowsHelloAsync dans Login.xaml.cs maintenant pour refléter l’extrait de code ci-dessous.
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"; } }
Comme Windows Hello crée une paire de clés publique et privée différente pour chaque compte sur chaque appareil, la page d’accueil doit afficher une liste d’appareils inscrits pour le compte connecté et permettre à chacun d’être oublié. Dans Welcome.xaml, ajoutez le code XAML suivant sous le
ForgetButton
fichier . Cela implémente un bouton oublier l’appareil, une zone de texte d’erreur et une liste pour afficher tous les appareils.<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>
Dans le fichier Welcome.xaml.cs, vous devez modifier la variable privée
Account
en haut de la classe pour qu’elle soit une variable privéeUserAccount
. Ensuite, mettez à jour laOnNavigatedTo
méthode pour utiliser AuthService et récupérer des informations pour le compte actuel. Lorsque vous disposez des informations de compte, vous pouvez définir laItemsSource
liste pour afficher les appareils. Vous devez ajouter une référence à l’espace de noms 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; } } } } }
Comme vous utiliserez AuthService lors de la suppression d’un compte, la référence à AccountHelper dans la
Button_Forget_User_Click
méthode peut être supprimée. La méthode doit maintenant ressembler à ce qui suit.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)); }
La méthode WindowsHelloHelper n’utilise pas AuthService pour supprimer le compte. Vous devez effectuer un appel à AuthService et passer l’id utilisateur.
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); }
Avant de pouvoir terminer l’implémentation de la page d’accueil , vous devez créer une méthode dans WindowsHelloHelper.cs qui permettra à un appareil d’être supprimé. Créez une méthode qui appelleRa WindowsHelloRemoveDeviceAsync dans AuthService.
public static async Task RemoveWindowsHelloDeviceAsync(UserAccount account, Guid deviceId) { await AuthService.AuthService.Instance.WindowsHelloRemoveDeviceAsync(account.UserId, deviceId); }
Dans Welcome.xaml.cs, implémentez le gestionnaire d’événements Button_Forget_Device_Click. Cela utilise l’appareil sélectionné dans la liste des appareils et utilise l’assistance Windows Hello pour appeler supprimer l’appareil. N’oubliez pas de rendre le gestionnaire d’événements asynchrone.
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 page suivante que vous allez mettre à jour est la page UserSelection . La page UserSelection doit utiliser AuthService pour récupérer tous les comptes d’utilisateur de l’appareil actuel. Actuellement, il n’existe aucun moyen pour vous d’obtenir un ID d’appareil à transmettre à AuthService afin qu’il puisse retourner des comptes d’utilisateur pour cet appareil. Dans le dossier Utils , créez une classe appelée « Helpers.cs ». Modifiez l’étendue de classe pour qu’elle soit statique publique, puis ajoutez la méthode suivante qui vous permettra de récupérer l’ID d’appareil actuel.
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; } } }
Dans la classe de page UserSelection , seul le code-behind doit changer, et non l’interface utilisateur. Dans UserSelection.xaml.cs, mettez à jour la méthode UserSelection_Loaded et la méthode UserSelectionChanged pour utiliser la
UserAccount
classe au lieu de laAccount
classe. Vous devez également obtenir tous les utilisateurs pour cet appareil via 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 page WindowsHelloRegister doit mettre à jour le fichier code-behind. L’interface utilisateur n’a pas besoin de modifications. Dans WindowsHelloRegister.xaml.cs, supprimez la variable privée
Account
en haut de la classe, car elle n’est plus nécessaire. Mettez à jour le gestionnaire d’événements RegisterButton_Click_Async pour utiliser AuthService. Cette méthode crée un compte UserAccount , puis tente de mettre à jour ses détails de compte. Si Windows Hello ne parvient pas à créer une clé, le compte est supprimé à mesure que le processus d’inscription a échoué.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"; } }
Générez et exécutez l’application. Connectez-vous à l’exemple de compte d’utilisateur avec les informations d’identification « sampleUsername » et « samplePassword ». Sur l’écran d’accueil, vous remarquerez peut-être que le bouton Oublier les appareils s’affiche, mais il n’y a aucun appareil. Lorsque vous créez ou migrez un utilisateur pour travailler avec Windows Hello, les informations de compte ne sont pas envoyées à AuthService.
Pour obtenir les informations du compte Windows Hello sur AuthService, le WindowsHelloHelper.cs doit être mis à jour. Dans la méthode CreateWindowsHelloKeyAsync, au lieu de retourner
true
uniquement dans le cas où cela réussit, vous devez appeler une nouvelle méthode qui tentera d’obtenir keyAttestation. Bien que cette pratique ne enregistre pas ces informations dans AuthService, vous apprendrez comment obtenir ces informations côté client. Mettez à jour la méthode CreateWindowsHelloKeyAsync comme suit :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; }
Créez une méthode GetKeyAttestationAsync dans WindowsHelloHelper.cs. Cette méthode montre comment obtenir toutes les informations nécessaires qui peuvent être fournies par Windows Hello pour chaque compte sur un appareil spécifique.
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); }
Vous avez peut-être remarqué dans la méthode GetKeyAttestationAsync que vous venez d’ajouter la dernière ligne a été commentée. Cette dernière ligne sera une nouvelle méthode que vous créez qui enverra toutes les informations Windows Hello à AuthService. Dans le monde réel, vous devez l’envoyer à un serveur réel via une 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; }
Supprimez les marques de commentaire de la dernière ligne de la méthode GetKeyAttestationAsync afin que les informations Windows Hello soient envoyées à AuthService.
Générez et exécutez l’application et connectez-vous avec les informations d’identification par défaut comme précédemment. Dans la page d’accueil , vous verrez maintenant que l’ID de l’appareil s’affiche. Si vous vous êtes connecté sur un autre appareil qui s’affiche également ici (si vous aviez un service d’authentification hébergé dans le cloud). Pour ce labo, l’ID d’appareil réel est affiché. Dans une implémentation réelle, vous souhaitez afficher un nom convivial qu’une personne pourrait comprendre et utiliser pour identifier chaque appareil.
Pour effectuer cette tâche pratique, vous avez besoin d’une demande et d’un défi pour l’utilisateur lorsqu’il sélectionne dans la page de sélection de l’utilisateur et reconnectez-vous. AuthService a deux méthodes que vous avez créées pour demander un défi, une qui utilise un défi signé. Dans WindowsHelloHelper.cs, créez une méthode nommée RequestSignAsync. Cela demande un défi à partir de l’AuthService, signe localement ce défi à l’aide d’une API Windows Hello et envoie le défi signé à AuthService. Dans ce labo, AuthService recevra le défi signé et le retour
true
. Dans une implémentation réelle, vous devez implémenter un mécanisme de vérification pour déterminer si le défi a été signé par l’utilisateur correct sur l’appareil approprié. Ajoutez la méthode ci-dessous au 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; }
Dans la classe WindowsHelloHelper , appelez la méthode RequestSignAsync à partir de la méthode 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; }
Tout au long de cet exercice, vous avez mis à jour l’application côté client pour utiliser AuthService. En procédant ainsi, vous avez pu éliminer la nécessité de la classe Account et de la classe AccountHelper . Supprimez la classe Account, le dossier Models et la classe AccountHelper dans le dossier Utils. Vous devez supprimer toutes les références à l’espace
WindowsHelloLogin.Models
de noms dans toute l’application avant que la solution ne se génère correctement.Générez et exécutez l’application et appréciez l’utilisation de Windows Hello avec le service et la base de données fictifs.
Dans ce laboratoire pratique, vous avez appris à utiliser les API Windows Hello pour remplacer le besoin de mots de passe lors de l’utilisation de l’authentification à partir d’un ordinateur Windows. Lorsque vous pensez à combien d’énergie est dépensée par les personnes qui conservent des mots de passe et prennent en charge les mots de passe perdus dans les systèmes existants, vous devez voir l’avantage de passer à ce nouveau système d’authentification Windows Hello.
Nous avons laissé en tant qu’exercice pour vous les détails de la façon dont vous allez implémenter l’authentification côté service et serveur. Il est prévu que la plupart des développeurs disposent de systèmes existants qui devront être migrés pour commencer à utiliser Windows Hello. Les détails de chacun de ces systèmes diffèrent.
Rubriques connexes
Windows developer