Создание службы входа Windows Hello
Это вторая часть полного пошагового руководства по использованию Windows Hello в качестве альтернативы традиционным системам проверки подлинности пользователей и паролей в упакованных приложениях Windows. В этой статье описывается, где приложение для входа в Windows Hello 1, отключено и расширяет функциональные возможности, чтобы продемонстрировать, как интегрировать Windows Hello в существующее приложение.
Чтобы создать этот проект, вам потребуется некоторый опыт работы с C#и XAML. Вам также потребуется использовать Visual Studio 2022 на компьютере с Windows 10 или Windows 11. Полные инструкции по настройке среды разработки см. в статье "Начало работы с WinUI ".
Упражнение 1. Логика на стороне сервера
В этом упражнении вы начинаете с приложения Windows Hello, встроенного в первую лабораторию, и создадите локальный сервер макета и базу данных. Эта лаборатория предназначена для обучения интеграции Windows Hello в существующую систему. С помощью макетного сервера и макета базы данных много не связанных настроек устраняется. В собственных приложениях необходимо заменить макеты объектов реальными службами и базами данных.
Для начала откройте решение WindowsHelloLogin из первой лаборатории Windows Hello Hands On Lab.
Начните с реализации макетного сервера и макета базы данных. Создайте новую папку с именем AuthService. В Обозреватель решений щелкните правой кнопкой мыши проект WindowsHelloLogin и выберите "Добавить>новую папку".
Создайте классы UserAccount и WindowsHelloDevices , которые будут выступать в качестве моделей для сохранения данных в базе данных макета. UserAccount будет похож на модель пользователя, реализованную на традиционном сервере проверки подлинности. Щелкните правой кнопкой мыши папку AuthService и добавьте новый класс с именем UserAccount.
Измените область класса на общедоступную и добавьте следующие общедоступные свойства для класса UserAccount . Необходимо добавить инструкцию using для
System.ComponentModel.DataAnnotations
пространства имен.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(); } }
Возможно, вы заметили закомментированный список WindowsHelloDevices. Это изменение, необходимо внести в существующую пользовательную модель в текущей реализации. Список WindowsHelloDevices будет содержать идентификатор устройства, открытый ключ, сделанный из Windows Hello, и KeyCredentialAttestationResult. Для этого упражнения необходимо реализовать ключAttestationResult , так как они предоставляются только windows Hello на устройствах с микросхемой доверенного платформенного модуля (доверенные платформенные модули). KeyCredentialAttestationResult — это сочетание нескольких свойств и необходимо разделить их для сохранения и загрузки их с базой данных.
Создайте новый класс в папке AuthService с именем "WindowsHelloDevice.cs". Это модель для устройств Windows Hello, как описано выше. Измените область класса, чтобы она была общедоступной и добавьте следующие свойства.
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; } } }
Вернитесь к UserAccount.cs и раскомментируйте список устройств 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(); } }
При создании модели UserAccount и WindowsHelloDevice необходимо создать другой класс в папке AuthService, которая будет выступать в качестве макетной базы данных, так как это макет базы данных, из которой вы будете сохранять и загружать список учетных записей пользователей локально. В реальном мире это будет реализация базы данных. Создайте новый класс в папке AuthService с именем "MockStore.cs". Измените область класса на общедоступную.
Так как макетное хранилище сохранит и загрузит список учетных записей пользователей локально, вы можете реализовать логику для сохранения и загрузки этого списка с помощью XmlSerializer. Вам также потребуется помнить имя файла и сохранить расположение. В MockStore.cs реализовать следующее:
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 } }
В методе LoadAccountListAsync, возможно, вы заметили, что метод InitializeSampleUserAccountsAsync был закомментирован. Этот метод необходимо создать в MockStore.cs. Этот метод заполняет список учетных записей пользователей, чтобы выполнить вход. В реальном мире пользовательская база данных уже будет заполнена. На этом шаге вы также создадите конструктор, который инициализирует список пользователей и вызовет 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(); } } }
Теперь, когда метод InitializeSampleUserAccountsAsync существует раскомментировать вызов метода в методе 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(); } }
Список учетных записей пользователей в хранилище макетов теперь можно сохранить и загрузить. Другие части приложения должны иметь доступ к этому списку, поэтому для получения этих данных потребуется некоторые методы. Под методом InitializeSampleUserAccountsAsync добавьте следующие методы для получения данных. Они позволяют получить идентификатор пользователя, одного пользователя, список пользователей для определенного устройства Windows Hello, а также получить открытый ключ для пользователя на определенном устройстве.
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; }
Следующие методы для реализации будут обрабатывать простые операции для добавления учетной записи, удаления учетной записи, а также удаления устройства. Удаление устройства необходимо, так как Windows Hello относится к устройству. Для каждого устройства, на котором выполняется вход, будет создана новая пара открытого и закрытого ключей Windows Hello. Это как наличие другого пароля для каждого устройства, на которое вы входите, единственное, что вам не нужно помнить все эти пароли; сервер выполняет. Добавьте следующие методы в 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; }
В классе MockStore добавьте метод, который добавит сведения, связанные с Windows Hello, в существующий UserAccount. Этот метод будет называться "WindowsHelloUpdateDetailsAsync" и будет принимать параметры для идентификации пользователя, а также сведения о Windows Hello. KeyAttestationResult был закомментирован при создании WindowsHelloDevice в реальном мире приложение, которое вам потребуется.
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(); }
Класс MockStore теперь завершен, так как это представляет базу данных, которую следует считать частной. Чтобы получить доступ к MockStore, для управления данными базы данных требуется класс AuthService . В папке AuthService создайте новый класс с именем "AuthService.cs". Измените область класса на общедоступную и добавьте шаблон одноэлементного экземпляра, чтобы убедиться, что только один экземпляр когда-либо создан.
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() { } } }
Класс AuthService должен создать экземпляр класса MockStore и предоставить доступ к свойствам объекта 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); } } }
Вам нужны методы в классе AuthService для доступа к методам добавления, удаления и обновления методов сведений Windows Hello в объекте MockStore . В конце определения класса AuthService добавьте следующие методы.
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); }
Класс AuthService должен предоставить метод для проверки учетных данных. Этот метод принимает имя пользователя и пароль, и убедитесь, что учетная запись существует и пароль действителен. Существующая система будет иметь эквивалентный метод для этого, который проверяет, является ли пользователь авторизованным. Добавьте следующий метод ValidateCredentials в файл 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; }
Класс AuthService нуждается в методе запроса, который возвращает клиенту вызов, чтобы проверить, является ли пользователь, который он утверждает. Затем другой метод необходим в классе AuthService , чтобы получить подписанный запрос обратно от клиента. Для этой лаборатории метод определения завершения подписанного вызова не был завершен. Каждая реализация Windows Hello в существующей системе проверки подлинности будет немного отличаться. Открытый ключ, хранящийся на сервере, должен соответствовать результату, который клиент вернулся на сервер. Добавьте эти два метода в 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; }
Упражнение 2. Логика на стороне клиента
В этом упражнении вы измените клиентские представления и вспомогательные классы из первой лаборатории, чтобы использовать класс AuthService . В реальном мире служба AuthService будет сервером проверки подлинности, и вам потребуется использовать веб-API для отправки и получения данных с сервера. Для этой лаборатории клиент и сервер являются локальными, чтобы обеспечить простоту. Цель состоит в том, чтобы узнать, как использовать API Windows Hello.
В MainPage.xaml.cs можно удалить вызов метода AccountHelper.LoadAccountListAsync в загруженном методе, так как класс AuthService создает экземпляр MockStore для загрузки списка учетных записей. Теперь
Loaded
метод должен выглядеть как фрагмент ниже. Обратите внимание, что определение асинхронного метода удаляется, так как ничего не ожидается.private void MainPage_Loaded(object sender, RoutedEventArgs e) { Frame.Navigate(typeof(UserSelection)); }
Обновите интерфейс страницы входа, чтобы требовать ввода пароля. В этой лаборатории показано, как существующую систему можно перенести для использования Windows Hello и существующих учетных записей с именем пользователя и паролем. Также обновите объяснение в нижней части XAML, чтобы включить пароль по умолчанию. Обновите следующий код XAML в 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>
В файле кода за классом Login необходимо изменить
Account
частную переменную в верхней части класса, чтобы она была.UserAccount
Измените событие,OnNavigateTo
чтобы привести тип к типуUserAccount
. Кроме того, вам потребуется следующая инструкция 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(); } } }
Так как страница входа использует
UserAccount
объект вместо предыдущегоAccount
объекта, WindowsHelloHelper.cs необходимо обновить для использованияUserAccount
в качестве типа параметра для некоторых методов. Вам потребуется изменить следующие параметры для методов CreateWindowsHelloKeyAsync, RemoveWindowsHelloAccountAsync и GetWindowsHelloAuthenticationMessageAsync.UserAccount
Так как класс имеетGuid
идентификатор userId, вы начнете использовать идентификатор в более точных местах.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; }
Метод SignInWindowsHelloAsync в файле Login.xaml.cs необходимо обновить, чтобы использовать AuthService вместо AccountHelper. Проверка учетных данных выполняется через AuthService. Для этой лаборатории только настроенная учетная запись — sampleUsername. Эта учетная запись создается в методе InitializeSampleUserAccountsAsync в MockStore.cs. Обновите метод SignInWindowsHelloAsync в Login.xaml.cs теперь, чтобы отразить приведенный ниже фрагмент кода.
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"; } }
Так как Windows Hello создаст другую пару открытого и закрытого ключей для каждой учетной записи на каждом устройстве , странице приветствия потребуется отобразить список зарегистрированных устройств для учетной записи, вошедшего в систему, и разрешить забыли каждый из них. В файле Welcome.xaml добавьте следующий код XAML под ним
ForgetButton
. При этом будет реализована кнопка забудь об устройстве, текстовая область ошибки и список для отображения всех устройств.<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>
В файле Welcome.xaml.cs необходимо изменить частную
Account
переменную в верхней части класса, чтобы она была частнойUserAccount
переменной. Затем обновитеOnNavigatedTo
метод, чтобы использовать AuthService и получить сведения для текущей учетной записи. Если у вас есть сведения об учетной записи, вы можете задатьItemsSource
список для отображения устройств. Необходимо добавить ссылку на пространство имен 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; } } } } }
Так как вы будете использовать AuthService при удалении учетной записи, можно удалить ссылку на AccountHelper в методе
Button_Forget_User_Click
. Теперь метод должен выглядеть следующим образом.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)); }
Метод WindowsHelloHelper не использует AuthService для удаления учетной записи. Необходимо вызвать AuthService и передать идентификатор пользователя.
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); }
Прежде чем завершить реализацию страницы приветствия , необходимо создать метод в WindowsHelloHelper.cs, который позволит удалить устройство. Создайте новый метод, который вызовет WindowsHelloRemoveDeviceAsync в AuthService.
public static async Task RemoveWindowsHelloDeviceAsync(UserAccount account, Guid deviceId) { await AuthService.AuthService.Instance.WindowsHelloRemoveDeviceAsync(account.UserId, deviceId); }
В Welcome.xaml.cs реализуйте обработчик событий Button_Forget_Device_Click . Это будет использовать выбранное устройство из списка устройств и использовать вспомогательное средство Windows Hello для вызова устройства. Не забудьте сделать асинхронный обработчик событий.
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; } }
На следующей странице будет обновлена страница UserSelection . Страница UserSelection должна использовать AuthService для получения всех учетных записей пользователей для текущего устройства. В настоящее время нет способа передать идентификатор устройства в AuthService , чтобы он смог вернуть учетные записи пользователей для этого устройства. В папке Utils создайте новый класс с именем "Helpers.cs". Измените область класса на общедоступную статическую, а затем добавьте следующий метод, который позволит получить текущий идентификатор устройства.
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; } } }
В классе страницы UserSelection необходимо изменить только код заголовок, а не пользовательский интерфейс. В UserSelection.xaml.cs обновите метод UserSelection_Loaded и метод UserSelectionChanged , чтобы использовать
UserAccount
класс вместоAccount
класса. Вам также потребуется получить всех пользователей для этого устройства через 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); } } } }
На странице WindowsHelloRegister необходимо обновить файл программной части. Пользовательский интерфейс не нуждается в каких-либо изменениях. В WindowsHelloRegister.xaml.cs удалите частную
Account
переменную в верхней части класса, так как она больше не нужна. Обновите обработчик событий RegisterButton_Click_Async, чтобы использовать AuthService. Этот метод создаст новый UserAccount , а затем попытается обновить сведения о своей учетной записи. Если Windows Hello не удается создать ключ, учетная запись будет удалена при сбое процесса регистрации.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"; } }
Выполните сборку приложения и запустите его. Войдите в пример учетной записи пользователя с учетными данными sampleUsername и samplePassword. На экране приветствия вы можете заметить, что отображается кнопка "Забыли устройства", но нет устройств. При создании или переносе пользователя для работы с Windows Hello сведения о учетной записи не отправляются в AuthService.
Чтобы получить сведения об учетной записи Windows Hello в AuthService, необходимо обновить WindowsHelloHelper.cs. В методе CreateWindowsHelloKeyAsync вместо возврата
true
только в случае успешного выполнения необходимо вызвать новый метод, который попытается получить KeyAttestation. Хотя в этой лаборатории эти сведения не записываться в AuthService, вы узнаете, как получить эти сведения на стороне клиента. Обновите метод CreateWindowsHelloKeyAsync следующим образом: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; }
Создайте метод GetKeyAttestationAsync в WindowsHelloHelper.cs. Этот метод демонстрирует, как получить все необходимые сведения, которые могут быть предоставлены Windows Hello для каждой учетной записи на определенном устройстве.
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); }
Возможно, вы заметили в методе GetKeyAttestationAsync , который вы только что добавили последнюю строку, было закомментировано. Последняя строка будет новым методом, который будет отправлять все сведения Windows Hello в AuthService. В реальном мире вам потребуется отправить это на фактический сервер через веб-API.
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; }
Раскомментируйте последнюю строку в методе GetKeyAttestationAsync , чтобы информация Windows Hello отправлялась в AuthService.
Создайте и запустите приложение и войдите с учетными данными по умолчанию, как и раньше. На странице приветствия отобразится идентификатор устройства. Если вы вошли на другое устройство, которое также будет отображаться здесь (если у вас размещена облачная служба проверки подлинности). Для этой лаборатории отображается фактический идентификатор устройства. В реальной реализации вы хотите отобразить понятное имя, которое человек может понять и использовать для идентификации каждого устройства.
Чтобы завершить работу с этой лабораторией, вам потребуется запрос и вызов для пользователя при выборе на странице выбора пользователя и обратном входе. AuthService имеет два метода, созданные для запроса задачи, которая использует подписанный вызов. В WindowsHelloHelper.cs создайте метод с именем RequestSignAsync. Это запросит вызов из AuthService, локально подписав эту проблему с помощью API Windows Hello и отправив подписанный вызов в AuthService. В этой лаборатории AuthService получит подписанный вызов и возврат
true
. В фактической реализации необходимо реализовать механизм проверки, чтобы определить, подписана ли задача правильным пользователем на правильном устройстве. Добавьте приведенный ниже метод в 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; }
В классе WindowsHelloHelper вызовите метод RequestSignAsync из метода 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; }
В рамках этого упражнения вы обновили клиентское приложение, чтобы использовать AuthService. Благодаря этому вы смогли устранить необходимость в классе Account и классе AccountHelper . Удалите класс Account, папку Models и класс AccountHelper в папке Utils. Перед успешной сборкой решения необходимо удалить всю ссылку на
WindowsHelloLogin.Models
пространство имен в приложении.Создайте и запустите приложение и наслаждайтесь использованием Windows Hello со службой макета и базой данных.
В этой лаборатории вы узнали, как использовать API Windows Hello для замены необходимости паролей при использовании проверки подлинности с компьютера Windows. Если вы считаете, сколько энергии тратится людьми, поддерживающими пароли и поддерживая потерянные пароли в существующих системах, вы увидите преимущество перехода на эту новую систему проверки подлинности Windows Hello.
Мы оставили в качестве упражнения сведения о том, как реализовать проверку подлинности на стороне службы и сервера. Ожидается, что большинство разработчиков будут иметь существующие системы, которые необходимо перенести, чтобы начать работу с Windows Hello. Сведения о каждой из этих систем будут отличаться.
См. также
Windows developer