Membuat layanan masuk Windows Hello
Ini adalah bagian kedua dari panduan lengkap tentang cara menggunakan Windows Hello sebagai alternatif untuk sistem autentikasi nama pengguna dan kata sandi tradisional di aplikasi Windows yang dikemas. Artikel ini mengambil di mana Bagian 1, aplikasi masuk Windows Hello, meninggalkan dan memperluas fungsionalitas untuk menunjukkan bagaimana Anda dapat mengintegrasikan Windows Hello ke dalam aplikasi yang ada.
Untuk membangun proyek ini, Anda memerlukan pengalaman dengan C#, dan XAML. Anda juga harus menggunakan Visual Studio 2022 pada komputer Windows 10 atau Windows 11. Lihat Mulai menggunakan WinUI untuk instruksi lengkap tentang menyiapkan lingkungan pengembangan Anda.
Latihan 1: Logika Sisi Server
Dalam latihan ini, Anda mulai dengan aplikasi Windows Hello yang dibangun di lab pertama dan membuat server dan database tiruan lokal. Tangan di lab ini dirancang untuk mengajarkan bagaimana Windows Hello dapat diintegrasikan ke dalam sistem yang ada. Dengan menggunakan server tiruan dan database tiruan, banyak pengaturan yang tidak terkait dihilangkan. Dalam aplikasi Anda sendiri, Anda harus mengganti objek tiruan dengan layanan dan database nyata.
Untuk memulai, buka solusi WindowsHelloLogin dari Windows Hello Hands On Lab pertama.
Anda akan mulai dengan menerapkan server tiruan dan database tiruan. Buat folder baru bernama "AuthService". Di Penjelajah Solusi, klik kanan proyek WindowsHelloLogin dan pilih Tambahkan>Folder Baru.
Buat kelas UserAccount dan WindowsHelloDevices yang akan bertindak sebagai model agar data disimpan dalam database tiruan. UserAccount akan mirip dengan model pengguna yang diterapkan pada server autentikasi tradisional. Klik kanan folder AuthService dan tambahkan kelas baru bernama "UserAccount".
Ubah cakupan kelas menjadi publik dan tambahkan properti publik berikut untuk kelas UserAccount . Anda harus menambahkan pernyataan penggunaan untuk
System.ComponentModel.DataAnnotations
namespace layanan.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(); } }
Anda mungkin telah melihat daftar WindowsHelloDevices yang dikomentari. Ini adalah modifikasi yang perlu Anda lakukan pada model pengguna yang ada dalam implementasi Anda saat ini. Daftar WindowsHelloDevices akan berisi deviceID, kunci publik yang dibuat dari Windows Hello, dan KeyCredentialAttestationResult. Untuk latihan ini, Anda harus menerapkan keyAttestationResult karena hanya disediakan oleh Windows Hello pada perangkat yang memiliki chip TPM (Modul Platform Tepercaya). KeyCredentialAttestationResult adalah kombinasi dari beberapa properti dan perlu dipisahkan untuk menyimpan dan memuatnya dengan database.
Buat kelas baru di folder AuthService yang disebut "WindowsHelloDevice.cs". Ini adalah model untuk perangkat Windows Hello seperti yang dibahas di atas. Ubah cakupan kelas menjadi publik dan tambahkan properti berikut.
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; } } }
Kembali ke UserAccount.cs dan batalkan komentar daftar perangkat 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(); } }
Dengan model untuk UserAccount dan WindowsHelloDevice dibuat, Anda perlu membuat kelas baru lain di folder AuthService yang akan bertindak sebagai database tiruan, karena ini adalah database tiruan tempat Anda akan menyimpan dan memuat daftar akun pengguna secara lokal. Di dunia nyata, ini akan menjadi implementasi database Anda. Buat kelas baru di folder AuthService bernama "MockStore.cs". Ubah cakupan kelas menjadi publik.
Karena penyimpanan tiruan akan menyimpan dan memuat daftar akun pengguna secara lokal, Anda dapat menerapkan logika untuk menyimpan dan memuat daftar tersebut menggunakan XmlSerializer. Anda juga perlu mengingat nama file dan menyimpan lokasi. Dalam MockStore.cs terapkan hal-hal berikut:
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 } }
Dalam metode LoadAccountListAsync, Anda mungkin telah memperhatikan bahwa metode InitializeSampleUserAccountsAsync telah dikomentari. Anda harus membuat metode ini di MockStore.cs. Metode ini akan mengisi daftar akun pengguna sehingga proses masuk dapat dilakukan. Di dunia nyata, database pengguna sudah akan diisi. Dalam langkah ini, Anda juga akan membuat konstruktor yang akan menginisialisasi daftar pengguna dan memanggil 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(); } } }
Sekarang setelah metode InitializeSampleUserAccountsAsync ada membatalkan komentar panggilan metode dalam metode 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(); } }
Daftar akun pengguna di penyimpanan tiruan sekarang dapat disimpan dan dimuat. Bagian lain dari aplikasi harus memiliki akses ke daftar ini sehingga perlu ada beberapa metode untuk mengambil data ini. Di bawah metode InitializeSampleUserAccountsAsync , tambahkan metode berikut untuk mendapatkan data. Mereka akan memungkinkan Anda untuk mendapatkan ID pengguna, satu pengguna, daftar pengguna untuk perangkat Windows Hello tertentu, dan juga mendapatkan kunci publik untuk pengguna pada perangkat tertentu.
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; }
Metode berikutnya yang akan diterapkan akan menangani operasi sederhana untuk menambahkan akun, menghapus akun, dan juga menghapus perangkat. Menghapus perangkat diperlukan karena Windows Hello khusus untuk perangkat. Untuk setiap perangkat tempat Anda masuk, pasangan kunci publik dan privat baru akan dibuat oleh Windows Hello. Ini seperti memiliki kata sandi yang berbeda untuk setiap perangkat yang Anda masuki, satu-satunya hal adalah Anda tidak perlu mengingat semua kata sandi tersebut; server tidak. Tambahkan metode berikut ke dalam 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; }
Di kelas MockStore tambahkan metode yang akan menambahkan informasi terkait Windows Hello ke UserAccount yang ada. Metode ini akan disebut "WindowsHelloUpdateDetailsAsync" dan akan mengambil parameter untuk mengidentifikasi pengguna, dan detail Windows Hello. KeyAttestationResult telah dikomentari saat membuat WindowsHelloDevice, dalam aplikasi dunia nyata Anda akan memerlukan ini.
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(); }
Kelas MockStore sekarang selesai, karena ini mewakili database yang harus dianggap privat. Untuk mengakses MockStore, kelas AuthService diperlukan untuk memanipulasi data database. Di folder AuthService, buat kelas baru yang disebut "AuthService.cs". Ubah cakupan kelas menjadi publik dan tambahkan pola instans singleton untuk memastikan hanya satu instans yang pernah dibuat.
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() { } } }
Kelas AuthService perlu membuat instans kelas MockStore dan menyediakan akses ke properti objek 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); } } }
Anda memerlukan metode di kelas AuthService untuk mengakses metode menambahkan, menghapus, dan memperbarui detail Windows Hello di objek MockStore . Di akhir definisi kelas AuthService , tambahkan metode berikut.
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); }
Kelas AuthService perlu menyediakan metode untuk memvalidasi kredensial. Metode ini akan mengambil nama pengguna dan kata sandi dan memastikan bahwa akun ada dan kata sandi valid. Sistem yang ada akan memiliki metode yang setara dengan ini yang memeriksa pengguna diotorisasi. Tambahkan metode ValidateCredentials berikut ke file 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; }
Kelas AuthService memerlukan metode tantangan permintaan yang mengembalikan tantangan kepada klien untuk memvalidasi apakah pengguna adalah siapa yang mereka klaim. Kemudian metode lain diperlukan di kelas AuthService untuk menerima tantangan yang ditandatangani kembali dari klien. Untuk tangan di lab ini, metode bagaimana Anda menentukan apakah tantangan yang ditandatangani telah selesai telah dibiarkan tidak lengkap. Setiap implementasi Windows Hello ke dalam sistem autentikasi yang ada akan sedikit berbeda. Kunci publik yang disimpan di server perlu dicocokkan dengan hasil yang dikembalikan klien ke server. Tambahkan dua metode ini ke 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; }
Latihan 2: Logika Sisi Klien
Dalam latihan ini, Anda akan mengubah tampilan sisi klien dan kelas pembantu dari lab pertama untuk menggunakan kelas AuthService . Di dunia nyata, AuthService akan menjadi server autentikasi dan Anda harus menggunakan Api Web untuk mengirim dan menerima data dari server. Untuk tangan ini di lab, klien dan server keduanya lokal untuk menjaga hal-hal sederhana. Tujuannya adalah untuk mempelajari cara menggunakan WINDOWS Hello API.
Dalam MainPage.xaml.cs, Anda dapat menghapus panggilan metode AccountHelper.LoadAccountListAsync dalam metode yang dimuat karena kelas AuthService membuat instans MockStore untuk memuat daftar akun. Metode
Loaded
ini sekarang akan terlihat seperti cuplikan di bawah ini. Perhatikan bahwa definisi metode asinkron dihapus karena tidak ada yang sedang ditunggu.private void MainPage_Loaded(object sender, RoutedEventArgs e) { Frame.Navigate(typeof(UserSelection)); }
Perbarui antarmuka halaman Masuk untuk mengharuskan kata sandi dimasukkan. Tangan di lab ini menunjukkan bagaimana sistem yang ada dapat dimigrasikan untuk menggunakan Windows Hello dan akun yang ada akan memiliki nama pengguna dan kata sandi. Perbarui juga penjelasan di bagian bawah XAML untuk menyertakan kata sandi default. Perbarui XAML berikut di 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>
Dalam file code-behind untuk kelas Login, Anda harus mengubah
Account
variabel privat di bagian atas kelas menjadi .UserAccount
OnNavigateTo
Ubah peristiwa untuk melemparkan jenis menjadi .UserAccount
Anda juga akan memerlukan pernyataan penggunaan berikut.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(); } } }
Karena halaman Masuk menggunakan
UserAccount
objek alih-alih objek sebelumnyaAccount
, WindowsHelloHelper.cs perlu diperbarui untuk menggunakanUserAccount
sebagai jenis parameter untuk beberapa metode. Anda harus mengubah parameter berikut untuk metode CreateWindowsHelloKeyAsync, RemoveWindowsHelloAccountAsync, dan GetWindowsHelloAuthenticationMessageAsync.UserAccount
Karena kelas memilikiGuid
untuk UserId, Anda akan mulai menggunakan Id di lebih banyak tempat agar lebih tepat.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; }
Metode SignInWindowsHelloAsync dalam file Login.xaml.cs perlu diperbarui untuk menggunakan AuthService alih-alih AccountHelper. Validasi kredensial akan terjadi melalui AuthService. Untuk tangan ini di lab, satu-satunya akun yang dikonfigurasi adalah "sampleUsername". Akun ini dibuat dalam metode InitializeSampleUserAccountsAsync di MockStore.cs. Perbarui metode SignInWindowsHelloAsync di Login.xaml.cs sekarang untuk mencerminkan cuplikan kode di bawah ini.
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"; } }
Karena Windows Hello akan membuat pasangan kunci publik dan privat yang berbeda untuk setiap akun di setiap perangkat , halaman Selamat Datang perlu menampilkan daftar perangkat terdaftar untuk akun yang masuk dan memungkinkan masing-masing akun dilupakan. Di Welcome.xaml tambahkan XAML berikut di bawah
ForgetButton
. Ini akan menerapkan tombol lupa perangkat, area teks kesalahan, dan daftar untuk menampilkan semua perangkat.<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>
Dalam file Welcome.xaml.cs, Anda perlu mengubah variabel privat
Account
di bagian atas kelas menjadi variabel privatUserAccount
. Kemudian perbaruiOnNavigatedTo
metode untuk menggunakan AuthService dan ambil informasi untuk akun saat ini. Ketika Anda memiliki informasi akun, Anda dapat mengaturItemsSource
daftar untuk menampilkan perangkat. Anda harus menambahkan referensi ke namespace layanan 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; } } } } }
Karena Anda akan menggunakan AuthService saat menghapus akun, referensi ke AccountHelper dalam
Button_Forget_User_Click
metode dapat dihapus. Metode ini sekarang harus terlihat seperti di bawah ini.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)); }
Metode WindowsHelloHelper tidak menggunakan AuthService untuk menghapus akun. Anda perlu melakukan panggilan ke AuthService dan meneruskan 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); }
Sebelum dapat menyelesaikan penerapan halaman Selamat Datang , Anda perlu membuat metode di WindowsHelloHelper.cs yang akan memungkinkan perangkat dihapus. Buat metode baru yang akan memanggil WindowsHelloRemoveDeviceAsync di AuthService.
public static async Task RemoveWindowsHelloDeviceAsync(UserAccount account, Guid deviceId) { await AuthService.AuthService.Instance.WindowsHelloRemoveDeviceAsync(account.UserId, deviceId); }
Di Welcome.xaml.cs, terapkan penanganan aktivitas Button_Forget_Device_Click . Ini akan menggunakan perangkat yang dipilih dari daftar perangkat dan menggunakan pembantu Windows Hello untuk memanggil hapus perangkat. Ingatlah untuk membuat asinkron penanganan aktivitas.
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; } }
Halaman berikutnya yang akan Anda perbarui adalah halaman UserSelection . Halaman UserSelection harus menggunakan AuthService untuk mengambil semua akun pengguna untuk perangkat saat ini. Saat ini, tidak ada cara bagi Anda untuk mendapatkan ID perangkat untuk diteruskan ke AuthService sehingga dapat mengembalikan akun pengguna untuk perangkat tersebut. Di folder Utils, buat kelas baru yang disebut "Helpers.cs". Ubah cakupan kelas menjadi statis publik lalu tambahkan metode berikut yang akan memungkinkan Anda mengambil id perangkat saat ini.
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; } } }
Di kelas halaman UserSelection, hanya kode di belakang yang perlu diubah, bukan antarmuka pengguna. Di UserSelection.xaml.cs, perbarui metode UserSelection_Loaded dan metode UserSelectionChanged untuk menggunakan
UserAccount
kelas alih-alihAccount
kelas. Anda juga perlu mendapatkan semua pengguna untuk perangkat ini melalui 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); } } } }
Halaman WindowsHelloRegister harus memperbarui file code-behind. Antarmuka pengguna tidak memerlukan perubahan apa pun. Dalam WindowsHelloRegister.xaml.cs, hapus variabel privat
Account
di bagian atas kelas, karena tidak lagi diperlukan. Perbarui penanganan aktivitas RegisterButton_Click_Async untuk menggunakan AuthService. Metode ini akan membuat UserAccount baru lalu mencoba dan memperbarui detail akunnya. Jika Windows Hello gagal membuat kunci, akun akan dihapus karena proses pendaftaran gagal.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"; } }
Membuat dan menjalankan aplikasi. Masuk ke contoh akun pengguna dengan kredensial "sampleUsername" dan "samplePassword". Pada layar selamat datang, Anda mungkin melihat tombol Lupakan perangkat ditampilkan, tetapi tidak ada perangkat. Saat Anda membuat atau memigrasikan pengguna untuk bekerja dengan Windows Hello, informasi akun tidak didorong ke AuthService.
Untuk mendapatkan informasi akun Windows Hello ke AuthService, WindowsHelloHelper.cs perlu diperbarui. Dalam metode CreateWindowsHelloKeyAsync, alih-alih hanya kembali
true
dalam kasus yang berhasil, Anda harus memanggil metode baru yang akan mencoba mendapatkan KeyAttestation. Meskipun tangan di lab ini tidak merekam informasi ini di AuthService, Anda akan mempelajari bagaimana Anda akan mendapatkan informasi ini di sisi klien. Perbarui metode CreateWindowsHelloKeyAsync sebagai berikut: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; }
Buat metode GetKeyAttestationAsync di WindowsHelloHelper.cs. Metode ini akan menunjukkan cara mendapatkan semua informasi yang diperlukan yang dapat disediakan oleh Windows Hello untuk setiap akun pada perangkat tertentu.
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); }
Anda mungkin telah memperhatikan dalam metode GetKeyAttestationAsync bahwa Anda baru saja menambahkan baris terakhir yang dikomentari. Baris terakhir ini akan menjadi metode baru yang Anda buat yang akan mengirim semua informasi Windows Hello ke AuthService. Di dunia nyata, Anda harus mengirim ini ke server aktual melalui 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; }
Batalkan komentar baris terakhir dalam metode GetKeyAttestationAsync sehingga informasi Windows Hello dikirim ke AuthService.
Buat dan jalankan aplikasi dan masuk dengan kredensial default seperti sebelumnya. Pada halaman Selamat Datang , Anda sekarang akan melihat bahwa Id perangkat ditampilkan. Jika Anda masuk di perangkat lain yang juga akan ditampilkan di sini (jika Anda memiliki layanan autentikasi yang dihosting cloud). Untuk tangan ini di lab, Id perangkat aktual sedang ditampilkan. Dalam implementasi nyata, Anda ingin menampilkan nama yang ramah yang dapat dipahami dan digunakan seseorang untuk mengidentifikasi setiap perangkat.
Untuk menyelesaikan tangan ini di lab, Anda memerlukan permintaan dan tantangan bagi pengguna ketika mereka memilih dari halaman pilihan pengguna dan masuk kembali. AuthService memiliki dua metode yang Anda buat untuk meminta tantangan, yang menggunakan tantangan yang ditandatangani. Di WindowsHelloHelper.cs, buat metode baru bernama RequestSignAsync. Ini akan meminta tantangan dari AuthService, secara lokal menandatangani tantangan itu menggunakan Windows Hello API dan mengirim tantangan yang ditandatangani ke AuthService. Di tangan lab ini, AuthService akan menerima tantangan yang ditandatangani dan mengembalikan
true
. Dalam implementasi aktual, Anda perlu menerapkan mekanisme verifikasi untuk menentukan apakah tantangan ditandatangani oleh pengguna yang benar pada perangkat yang benar. Tambahkan metode di bawah ini ke 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; }
Di kelas WindowsHelloHelper, panggil metode RequestSignAsync dari metode 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; }
Sepanjang latihan ini, Anda telah memperbarui aplikasi sisi klien untuk menggunakan AuthService. Dengan melakukan ini, Anda telah dapat menghilangkan kebutuhan akan kelas Akun dan kelas AccountHelper . Hapus kelas Akun, folder Model, dan kelas AccountHelper di folder Utils. Anda harus menghapus semua referensi ke
WindowsHelloLogin.Models
namespace layanan di seluruh aplikasi sebelum solusi berhasil dibuat.Bangun dan jalankan aplikasi dan nikmati penggunaan Windows Hello dengan layanan tiruan dan database.
Di lab ini, Anda telah mempelajari cara menggunakan WINDOWS Hello API untuk menggantikan kebutuhan akan kata sandi saat menggunakan autentikasi dari komputer Windows. Ketika Anda mempertimbangkan berapa banyak energi yang dikeluarkan oleh orang-orang yang mempertahankan kata sandi dan mendukung kata sandi yang hilang dalam sistem yang ada, Anda akan melihat manfaat pindah ke sistem autentikasi Windows Hello baru ini.
Kami telah meninggalkan sebagai latihan untuk Anda detail tentang bagaimana Anda akan menerapkan autentikasi di sisi layanan dan server. Diharapkan bahwa sebagian besar pengembang akan memiliki sistem yang ada yang perlu dimigrasikan untuk mulai bekerja dengan Windows Hello. Detail masing-masing sistem ini akan berbeda.
Topik terkait
Windows developer