建立 Windows Hello 登入服務
這是完整逐步解說的第二個部分,說明如何在封裝的 Windows 應用程式中,使用 Windows Hello 做為傳統使用者名稱和密碼驗證系統的替代方案。 本文繼續第 1 部分 Windows Hello 登入應用程式的內容,並擴展了功能以演示如何將 Windows Hello 整合到現有應用程式中。
若要建置此專案,您需要具備 C# 和 XAML 經驗。 您也必須在 Windows 10 或 Windows 11 計算機上使用 Visual Studio 2022。 如需設定開發環境的完整指示,請參閱 開始使用 WinUI 。
練習 1:伺服器端邏輯
在本練習中,您會從第一個實驗室中建置的 Windows Hello 應用程式開始,並建立本機模擬伺服器和資料庫。 這個實際操作實驗室的設計目的是要教導 Windows Hello 如何整合到現有的系統中。 藉由使用模擬伺服器和模擬資料庫,會消除許多不相關的設定。 在您自己的應用程式中,您必須將模擬物件取代為實際的服務和資料庫。
若要開始,請從第一個 Windows Hello Hands On Lab 開啟 WindowsHelloLogin 解決方案。
您一開始會實作模擬伺服器和模擬資料庫。 建立名為 「AuthService」 的新資料夾。 在 方案總管 中,以滑鼠右鍵按兩下 WindowsHelloLogin 專案,然後選取 [新增>資料夾]。
建立 UserAccount 和 WindowsHelloDevices 類別,以作為要儲存在模擬資料庫中之數據的模型。 UserAccount 會類似於在傳統驗證伺服器上實作的使用者模型。 以 滑鼠右鍵按兩下 AuthService 資料夾,然後新增名為 「UserAccount」 的新類別。
將類別範圍變更為 public,併為 UserAccount 類別新增下列公用屬性。 您必須為
System.ComponentModel.DataAnnotations
命名空間新增using語句。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 列表會包含 deviceID、Windows Hello 所建立的公鑰,以及 KeyCredentialAttestationResult。 在此練習中,您必須實 作keyAttestationResult,因為只有具有TPM(信任平臺模組)晶元的裝置上 Windows Hello 才會提供密鑰AttestationResult 。 KeyCredentialAttestationResult 是多個屬性的組合,需要將其分割以便在資料庫中儲存和載入它們。
在名為 「WindowsHelloDevice.cs」 的 AuthService 資料夾中建立新的類別。 這是如上所述的 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 資料夾中建立另一個新類別,以作為模擬資料庫,因為這是模擬資料庫,而您將從本機儲存和載入用戶帳戶清單。 在真實世界中,這會是您的資料庫實作。 在名為 「MockStore.cs」 的 AuthService 資料夾中建立新的類別。 將類別範圍變更為 public。
當模擬存放區會在本機儲存及載入使用者帳戶清單時,您可以實作邏輯,以使用 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 詳細數據。 在 真實世界應用程式中建立 WindowsHelloDevice 時,KeyAttestationResult 已標記為批註。
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 類別中的方法,才能存取 MockStore 物件中的新增、移除和更新 Windows Hello 詳細數據方法。 在 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 會是驗證伺服器,而您必須使用 Web API 從伺服器傳送和接收數據。 對於這個實際操作實驗室,客戶端和伺服器都是本機,以保持簡單。 目標是學習如何使用 Windows Hello API。
在MainPage.xaml.cs中,您可以在載入的方法中移除 AccountHelper.LoadAccountListAsync 方法呼叫,因為 AuthService 類別會建立 MockStore 的實例以載入帳戶清單。 方法
Loaded
現在看起來應該像下面的代碼段。 請注意,異步方法定義會移除,因為未等候任何專案。private void MainPage_Loaded(object sender, RoutedEventArgs e) { Frame.Navigate(typeof(UserSelection)); }
更新 [ 登入 ] 頁面介面,以要求輸入密碼。 此實作教室示範如何移轉現有系統以使用 Windows Hello,並且現有帳戶將擁有使用者名稱和密碼。 同時更新 XAML 底部的說明,以包含預設密碼。 在 Login.xaml 中更新下列 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; }
Login.xaml.cs 檔案中的 SignInWindowsHelloAsync 方法必須更新,才能使用 AuthService,而不是 AccountHelper。 驗證認證將會透過 AuthService 進行。 對於這個實際操作實驗室,唯一設定的帳戶是 “sampleUsername”。 此帳戶是在 MockStore.cs 的 InitializeSampleUserAccountsAsync 方法中建立。 更新 Login.xaml.cs 中的 SignInWindowsHelloAsync 方法,以反映下列代碼段。
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 中,於下方
ForgetButton
的下列 XAML 中新增。 這將實現一個忘記裝置按鈕、一個錯誤文字區域和一個顯示所有裝置的清單。<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 並傳遞 userId。
public static async void RemoveWindowsHelloAccountAsync(UserAccount account) { //Open the account with Windows Hello KeyCredentialRetrievalResult keyOpenResult = await KeyCredentialManager.OpenAsync(account.Username); if (keyOpenResult.Status == KeyCredentialStatus.Success) { // In the real world you would send key information to server to unregister await AuthService.AuthService.Instance.WindowsHelloRemoveUserAsync(account.UserId); } //Then delete the account from the machines list of Windows Hello Accounts await KeyCredentialManager.DeleteAsync(account.Username); }
在完成實作 歡迎 頁面之前,您必須在 WindowsHelloHelper.cs 中建立方法,以允許移除裝置。 建立新的方法,以在 AuthService 中呼叫 WindowsHelloRemoveDeviceAsync。
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。
若要取得 AuthService 的 Windows Hello 帳戶資訊,必須更新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; }
在 WindowsHelloHelper.cs 中建立 GetKeyAttestationAsync 方法。 此方法將示範如何取得 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。 在真實世界中,您必須透過 Web 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 要求挑戰,並在本機使用 Windows Hello API 簽署該挑戰,並將已簽署的挑戰傳送至 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 類別中,從 GetWindowsHelloAuthenticationMessageAsync 方法呼叫 RequestSignAsync 方法。
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 類別的需求。 刪除 Utils 資料夾中的 Account 類別、Models 資料夾和 AccountHelper 類別。 在解決方案成功建置之前,您必須移除整個應用程式命名空間的所有參考
WindowsHelloLogin.Models
。建置並執行應用程式,並享受使用具有模擬服務和資料庫的 Windows Hello。
在此實際操作實驗室中,您已瞭解如何使用 Windows Hello API 來取代從 Windows 電腦使用驗證時所需的密碼。 當您考慮到人們在現有系統中維護密碼和支援遺失密碼花費了多少精力時,您應該會看到遷移到這個新的 Windows Hello 身分驗證系統的好處。
我們已為您練習如何在服務和伺服器端實作驗證的詳細數據。 預期大部分開發人員會有需要移轉的現有系統,才能開始使用 Windows Hello。 每個系統的詳細數據將會有所不同。