Criar um serviço de logon do Windows Hello
Esta é a segunda parte de um passo a passo completo sobre como usar o Windows Hello como uma alternativa aos sistemas tradicionais de autenticação de nome de usuário e senha em aplicativos Windows empacotados. Este artigo continua de onde a Parte 1, aplicativo de logon do Windows Hello, parou e estende a funcionalidade para demonstrar como você pode integrar o Windows Hello ao seu aplicativo existente.
Para criar esse projeto, você precisará de alguma experiência com C# e XAML. Você também precisará usar o Visual Studio 2022 em um computador Windows 10 ou Windows 11. Consulte Introdução ao WinUI para obter instruções completas sobre como configurar seu ambiente de desenvolvimento.
Exercício 1: Lógica do lado do servidor
Neste exercício, você começa com o aplicativo Windows Hello criado no primeiro laboratório e cria um servidor fictício local e um banco de dados. Este laboratório prático foi projetado para ensinar como o Windows Hello pode ser integrado a um sistema existente. Ao usar um servidor fictício e um banco de dados fictício, muitas configurações não relacionadas são eliminadas. Em seus próprios aplicativos, você precisará substituir os objetos fictícios pelos serviços e bancos de dados reais.
Para começar, abra a solução WindowsHelloLogin do primeiro Laboratório Prático do Windows Hello.
Você começará implementando o servidor fictício e o banco de dados fictício. Crie uma nova pasta chamada "AuthService". No Gerenciador de Soluções, clique com o botão direito do mouse no projeto WindowsHelloLogin e selecione Adicionar>Nova Pasta.
Crie as classes UserAccount e WindowsHelloDevices que atuarão como modelos para os dados a serem salvos no banco de dados fictício. O UserAccount será semelhante ao modelo de usuário implementado em um servidor de autenticação tradicional. Clique com o botão direito do mouse na pasta AuthService e adicione uma nova classe chamada "UserAccount".
Altere o escopo da classe para ser público e adicione as seguintes propriedades públicas para a classe UserAccount . Você precisará adicionar uma instrução using para o
System.ComponentModel.DataAnnotations
namespace.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(); } }
Você deve ter notado a lista comentada de WindowsHelloDevices. Essa é uma modificação que você precisará fazer em um modelo de usuário existente em sua implementação atual. A lista de WindowsHelloDevices conterá um deviceID, a chave pública feita de Windows Hello e um KeyCredentialAttestationResult. Para este exercício, você precisará implementar o keyAttestationResult , pois eles são fornecidos apenas pelo Windows Hello em dispositivos que têm um chip TPM (Trusted Platform Modules). O KeyCredentialAttestationResult é uma combinação de várias propriedades e precisaria ser dividido para salvá-las e carregá-las com um banco de dados.
Crie uma nova classe na pasta AuthService chamada "WindowsHelloDevice.cs". Esse é o modelo para os dispositivos Windows Hello, conforme discutido acima. Altere o escopo da classe para ser público e adicione as propriedades a seguir.
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; } } }
Retorne ao UserAccount.cs e remova o comentário da lista de dispositivos Windows Hello.
using System.Collections.Generic; namespace WindowsHelloLogin.AuthService { public class UserAccount { [Key, Required] public Guid UserId { get; set; } [Required] public string Username { get; set; } public string Password { get; set; } public List<WindowsHelloDevice> WindowsHelloDevices = new(); } }
Com o modelo para o UserAccount e o WindowsHelloDevice criados, você precisa criar outra nova classe na pasta AuthService que atuará como o banco de dados fictício, pois esse é um banco de dados fictício do qual você salvará e carregará uma lista de contas de usuário localmente. No mundo real, essa seria a implementação do seu banco de dados. Crie uma nova classe na pasta AuthService chamada "MockStore.cs". Altere o escopo da classe para público.
Como o repositório fictício salvará e carregará uma lista de contas de usuário localmente, você pode implementar a lógica para salvar e carregar essa lista usando um XmlSerializer. Você também precisará se lembrar do nome do arquivo e do local de salvamento. Em MockStore.cs implemente o seguinte:
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 } }
No método LoadAccountListAsync, você deve ter notado que um método InitializeSampleUserAccountsAsync foi comentado. Você precisará criar esse método no MockStore.cs. Este método preencherá a lista de contas de usuário para que um login possa ocorrer. No mundo real, o banco de dados do usuário já estaria preenchido. Nesta etapa, você também criará um construtor que inicializará a lista de usuários e chamará 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(); } } }
Agora que o método InitializeSampleUserAccountsAsync existe, remova o comentário da chamada de método no método LoadAccountListAsync .
private async Task LoadAccountListAsync() { if (File.Exists(_userAccountListPath)) { StorageFile accountsFile = await StorageFile.GetFileFromPathAsync(_userAccountListPath); string accountsXml = await FileIO.ReadTextAsync(accountsFile); DeserializeXmlToAccountList(accountsXml); } // If the UserAccountList does not contain the sampleUser Initialize the sample users // This is only needed as it in a Hand on Lab to demonstrate a user migrating // In the real world user accounts would just be in a database if (!_mockDatabaseUserAccountsList.Any(f = > f.Username.Equals("sampleUsername"))) { //If the list is empty InitializeSampleUserAccountsAsync and return the list await InitializeSampleUserAccountsAsync(); } }
A lista de contas de usuário no repositório fictício agora pode ser salva e carregada. Outras partes do aplicativo precisarão ter acesso a essa lista, portanto, será necessário haver alguns métodos para recuperar esses dados. Abaixo do método InitializeSampleUserAccountsAsync , adicione os métodos a seguir para obter dados. Eles permitirão que você obtenha uma ID de usuário, um único usuário, uma lista de usuários para um dispositivo Windows Hello específico e também obtenha a chave pública para o usuário em um dispositivo específico.
public Guid GetUserId(string username) { if (_mockDatabaseUserAccountsList.Any()) { UserAccount account = _mockDatabaseUserAccountsList.FirstOrDefault(f => f.Username.Equals(username)); if (account != null) { return account.UserId; } } return Guid.Empty; } public UserAccount GetUserAccount(Guid userId) { return _mockDatabaseUserAccountsList.FirstOrDefault(f => f.UserId.Equals(userId)); } public List<UserAccount> GetUserAccountsForDevice(Guid deviceId) { var usersForDevice = new List<UserAccount>(); foreach (UserAccount account in _mockDatabaseUserAccountsList) { if (account.WindowsHelloDevices.Any(f => f.DeviceId.Equals(deviceId))) { usersForDevice.Add(account); } } return usersForDevice; } public byte[] GetPublicKey(Guid userId, Guid deviceId) { UserAccount account = _mockDatabaseUserAccountsList.FirstOrDefault(f => f.UserId.Equals(userId)); if (account != null) { if (account.WindowsHelloDevices.Any()) { return account.WindowsHelloDevices.FirstOrDefault(p => p.DeviceId.Equals(deviceId)).PublicKey; } } return null; }
Os próximos métodos a serem implementados lidarão com operações simples para adicionar uma conta, remover uma conta e também remover um dispositivo. A remoção de um dispositivo é necessária, pois o Windows Hello é específico do dispositivo. Para cada dispositivo no qual você fizer login, um novo par de chaves pública e privada será criado pelo Windows Hello. É como ter uma senha diferente para cada dispositivo em que você faz login, a única coisa é que você não precisa se lembrar de todas essas senhas; o servidor faz. Adicione os seguintes métodos ao 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; }
Na classe MockStore, adicione um método que adicionará informações relacionadas ao Windows Hello a uma UserAccount existente. Esse método será chamado de "WindowsHelloUpdateDetailsAsync" e usará parâmetros para identificar o usuário e os detalhes do Windows Hello. O KeyAttestationResult foi comentado ao criar um WindowsHelloDevice, em um aplicativo do mundo real, você precisaria disso.
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(); }
A classe MockStore agora está completa, pois representa o banco de dados que deve ser considerado privado. Para acessar o MockStore, uma classe AuthService é necessária para manipular os dados do banco de dados. Na pasta AuthService, crie uma nova classe chamada "AuthService.cs". Altere o escopo da classe para public e adicione um padrão de instância singleton para garantir que apenas uma instância seja criada.
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() { } } }
A classe AuthService precisa criar uma instância da classe MockStore e fornecer acesso às propriedades do objeto MockStore .
namespace WindowsHelloLogin.AuthService { public class AuthService { //Singleton instance of the AuthService //The AuthService is a mock of what a real world server and database implementation would be private static AuthService _instance; public static AuthService Instance { get { if (null == _instance) { _instance = new AuthService(); } return _instance; } } private AuthService() { } private MockStore _mockStore = new(); public Guid GetUserId(string username) { return _mockStore.GetUserId(username); } public UserAccount GetUserAccount(Guid userId) { return _mockStore.GetUserAccount(userId); } public List<UserAccount> GetUserAccountsForDevice(Guid deviceId) { return _mockStore.GetUserAccountsForDevice(deviceId); } } }
Você precisa de métodos na classe AuthService para acessar métodos de detalhes adicionar, remover e atualizar Windows Hello no objeto MockStore . No final da definição da classe AuthService , adicione os métodos a seguir.
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); }
A classe AuthService precisa fornecer um método para validar credenciais. Este método pegará um nome de usuário e senha e garantirá que a conta exista e que a senha seja válida. Um sistema existente teria um método equivalente a este que verifica se o usuário está autorizado. Adicione o método ValidateCredentials a seguir ao arquivo 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; }
A classe AuthService precisa de um método de desafio de solicitação que retorne um desafio ao cliente para validar se o usuário é quem afirma ser. Em seguida, outro método é necessário na classe AuthService para receber o desafio assinado de volta do cliente. Para este laboratório prático, o método de como você determina se o desafio assinado foi concluído foi deixado incompleto. Cada implementação do Windows Hello em um sistema de autenticação existente será um pouco diferente. A chave pública armazenada no servidor precisa corresponder ao resultado que o cliente retornou ao servidor. Adicione esses dois métodos a AuthService.cs.
using Windows.Security.Cryptography; using Windows.Storage.Streams; public IBuffer WindowsHelloRequestChallenge() { return CryptographicBuffer.ConvertStringToBinary("ServerChallenge", BinaryStringEncoding.Utf8); } public bool SendServerSignedChallenge(Guid userId, Guid deviceId, byte[] signedChallenge) { // Depending on your company polices and procedures this step will be different // It is at this point you will need to validate the signedChallenge that is sent back from the client. // Validation is used to ensure the correct user is trying to access this account. // The validation process will use the signedChallenge and the stored PublicKey // for the username and the specific device signin is called from. // Based on the validation result you will return a bool value to allow access to continue or to block the account. // For this sample validation will not happen as a best practice solution does not apply and will need to // be configured for each company. // Simply just return true. // You could get the User's Public Key with something similar to the following: byte[] userPublicKey = _mockStore.GetPublicKey(userId, deviceId); return true; }
Exercício 2: Lógica do lado do cliente
Neste exercício, você alterará as exibições do lado do cliente e as classes auxiliares do primeiro laboratório para usar a classe AuthService . No mundo real, o AuthService seria o servidor de autenticação e você precisaria usar APIs da Web para enviar e receber dados do servidor. Para este laboratório prático, o cliente e o servidor são locais para manter as coisas simples. O objetivo é aprender a usar as APIs do Windows Hello.
No MainPage.xaml.cs, você pode remover a chamada do método AccountHelper.LoadAccountListAsync no método loaded, pois a classe AuthService cria uma instância do MockStore para carregar a lista de contas. O
Loaded
método agora deve ser semelhante ao snippet abaixo. Observe que a definição do método assíncrono é removida, pois nada está sendo aguardado.private void MainPage_Loaded(object sender, RoutedEventArgs e) { Frame.Navigate(typeof(UserSelection)); }
Atualize a interface da página de login para exigir que uma senha seja inserida. Este laboratório prático demonstra como um sistema existente pode ser migrado para usar o Windows Hello e as contas existentes terão um nome de usuário e uma senha. Atualize também a explicação na parte inferior do XAML para incluir a senha padrão. Atualize o XAML a seguir em 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>
No arquivo code-behind da classe Login , você precisará alterar a
Account
variável privada na parte superior da classe para ser umUserAccount
. Altere oOnNavigateTo
evento para converter o tipo para ser umUserAccount
. Você também precisará da seguinte instrução using.using WindowsHelloLogin.AuthService; namespace WindowsHelloLogin.Views { public sealed partial class Login : Page { private UserAccount _account; private bool _isExistingAccount; public Login() { this.InitializeComponent(); } protected override async void OnNavigatedTo(NavigationEventArgs e) { //Check Windows Hello is setup and available on this machine if (await WindowsHelloHelper.WindowsHelloAvailableCheckAsync()) { if (e.Parameter != null) { _isExistingAccount = true; //Set the account to the existing account being passed in _account = (UserAccount)e.Parameter; UsernameTextBox.Text = _account.Username; await SignInWindowsHelloAsync(); } } } private async void LoginButton_Click(object sender, RoutedEventArgs e) { ErrorMessage.Text = ""; await SignInWindowsHelloAsync(); } } }
Como a página de logon está usando um
UserAccount
objeto em vez do objeto anteriorAccount
, o WindowsHelloHelper.cs precisará ser atualizado para usar aUserAccount
como um tipo de parâmetro para alguns métodos. Você precisará alterar os parâmetros a seguir para os métodos CreateWindowsHelloKeyAsync, RemoveWindowsHelloAccountAsync e GetWindowsHelloAuthenticationMessageAsync . Como aUserAccount
classe tem umGuid
para um UserId, você começará a usar o Id em mais lugares para ser mais preciso.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; }
O método SignInWindowsHelloAsync em Login.xaml.cs arquivo precisará ser atualizado para usar o AuthService em vez do AccountHelper. A validação das credenciais ocorrerá por meio do AuthService. Para este laboratório prático, a única conta configurada é "sampleUsername". Essa conta é criada no método InitializeSampleUserAccountsAsync no MockStore.cs. Atualize o método SignInWindowsHelloAsync no Login.xaml.cs agora para refletir o snippet de código abaixo.
private async Task SignInWindowsHelloAsync() { if (_isExistingAccount) { if (await WindowsHelloHelper.GetWindowsHelloAuthenticationMessageAsync(_account)) { Frame.Navigate(typeof(Welcome), _account); } } else if (AuthService.AuthService.Instance.ValidateCredentials(UsernameTextBox.Text, PasswordBox.Password)) { Guid userId = AuthService.AuthService.Instance.GetUserId(UsernameTextBox.Text); if (userId != Guid.Empty) { //Now that the account exists on server try and create the necessary details and add them to the account if (await WindowsHelloHelper.CreateWindowsHelloKeyAsync(userId, UsernameTextBox.Text)) { Debug.WriteLine("Successfully signed in with Windows Hello!"); //Navigate to the Welcome Screen. _account = AuthService.AuthService.Instance.GetUserAccount(userId); Frame.Navigate(typeof(Welcome), _account); } else { //The Windows Hello account creation failed. //Remove the account from the server as the details were not configured await AuthService.AuthService.Instance.WindowsHelloRemoveUserAsync(userId); ErrorMessage.Text = "Account Creation Failed"; } } } else { ErrorMessage.Text = "Invalid Credentials"; } }
Como o Windows Hello criará um par de chaves pública e privada diferente para cada conta em cada dispositivo, a página de boas-vindas precisará exibir uma lista de dispositivos registrados para a conta conectada e permitir que cada um seja esquecido. Em Welcome.xaml, adicione o seguinte XAML abaixo do
ForgetButton
. Isso implementará um botão esquecer dispositivo, uma área de texto de erro e uma lista para exibir todos os dispositivos.<Grid> <StackPanel> <TextBlock x:Name="Title" Text="Welcome" FontSize="40" TextAlignment="Center"/> <TextBlock x:Name="UserNameText" FontSize="28" TextAlignment="Center"/> <Button x:Name="BackToUserListButton" Content="Back to User List" Click="Button_Restart_Click" HorizontalAlignment="Center" Margin="0,20" Foreground="White" Background="DodgerBlue"/> <Button x:Name="ForgetButton" Content="Forget Me" Click="Button_Forget_User_Click" Foreground="White" Background="Gray" HorizontalAlignment="Center"/> <Button x:Name="ForgetDeviceButton" Content="Forget Device" Click="Button_Forget_Device_Click" Foreground="White" Background="Gray" Margin="0,40,0,20" HorizontalAlignment="Center"/> <TextBlock x:Name="ForgetDeviceErrorTextBlock" Text="Select a device first" TextWrapping="Wrap" Width="300" Foreground="Red" TextAlignment="Center" VerticalAlignment="Center" FontSize="16" Visibility="Collapsed"/> <ListView x:Name="UserListView" MaxHeight="500" MinWidth="350" Width="350" HorizontalAlignment="Center"> <ListView.ItemTemplate> <DataTemplate> <Grid Background="Gray" Height="50" Width="350" HorizontalAlignment="Center" VerticalAlignment="Stretch" > <TextBlock Text="{Binding DeviceId}" HorizontalAlignment="Center" TextAlignment="Center" VerticalAlignment="Center" Foreground="White"/> </Grid> </DataTemplate> </ListView.ItemTemplate> </ListView> </StackPanel> </Grid>
No arquivo Welcome.xaml.cs, você precisa alterar a variável privada
Account
na parte superior da classe para ser uma variável privadaUserAccount
. Em seguida, atualize oOnNavigatedTo
método para usar o AuthService e recuperar informações para a conta atual. Quando você tiver as informações da conta, poderá definir aItemsSource
lista para exibir os dispositivos. Você precisará adicionar uma referência ao namespace AuthService .using WindowsHelloLogin.AuthService; namespace WindowsHelloLogin.Views { public sealed partial class Welcome : Page { private UserAccount _activeAccount; public Welcome() { InitializeComponent(); } protected override void OnNavigatedTo(NavigationEventArgs e) { _activeAccount = (UserAccount)e.Parameter; if (_activeAccount != null) { UserAccount account = AuthService.AuthService.Instance.GetUserAccount(_activeAccount.UserId); if (account != null) { UserListView.ItemsSource = account.WindowsHelloDevices; UserNameText.Text = account.Username; } } } } }
Como você usará o AuthService ao remover uma conta, a referência ao AccountHelper no
Button_Forget_User_Click
método pode ser removida. O método agora deve ter a aparência abaixo.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)); }
O método WindowsHelloHelper não está usando o AuthService para remover a conta. Você precisa fazer uma chamada para o AuthService e passar o 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); }
Antes de concluir a implementação da página de boas-vindas , você precisa criar um método no WindowsHelloHelper.cs que permitirá que um dispositivo seja removido. Crie um novo método que chamará WindowsHelloRemoveDeviceAsync em AuthService.
public static async Task RemoveWindowsHelloDeviceAsync(UserAccount account, Guid deviceId) { await AuthService.AuthService.Instance.WindowsHelloRemoveDeviceAsync(account.UserId, deviceId); }
Em Welcome.xaml.cs, implemente o manipulador de eventos Button_Forget_Device_Click . Isso usará o dispositivo selecionado na lista de dispositivos e usará o auxiliar do Windows Hello para chamar remover dispositivo. Lembre-se de tornar o manipulador de eventos assíncrono.
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; } }
A próxima página que você atualizará é a página UserSelection . A página UserSelection precisará usar o AuthService para recuperar todas as contas de usuário do dispositivo atual. Atualmente, não há como você obter uma ID de dispositivo para passar para o AuthService para que ele possa retornar contas de usuário para esse dispositivo. Na pasta Utils, crie uma nova classe chamada "Helpers.cs". Altere o escopo da classe para ser public static e adicione o método a seguir que permitirá que você recupere a ID do dispositivo atual.
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; } } }
Na classe de página UserSelection, apenas o code-behind precisa ser alterado, não a interface do usuário. Em UserSelection.xaml.cs, atualize o método UserSelection_Loaded e o método UserSelectionChanged para usar a
UserAccount
classe em vez daAccount
classe. Você também precisará obter todos os usuários desse dispositivo por meio do 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); } } } }
A página WindowsHelloRegister precisa ter o arquivo code-behind atualizado. A interface do usuário não precisa de alterações. Em WindowsHelloRegister.xaml.cs, remova a variável privada
Account
na parte superior da classe, pois ela não é mais necessária. Atualize o manipulador de eventos RegisterButton_Click_Async para usar o AuthService. Esse método criará uma nova UserAccount e, em seguida, tentará atualizar os detalhes da conta. Se o Windows Hello não conseguir criar uma chave, a conta será removida porque o processo de registro falhou.private async void RegisterButton_Click_Async(object sender, RoutedEventArgs e) { ErrorMessage.Text = ""; //Validate entered credentials are acceptable if (!string.IsNullOrEmpty(UsernameTextBox.Text)) { //Register an Account on the AuthService so that we can get back a userId await AuthService.AuthService.Instance.RegisterAsync(UsernameTextBox.Text); Guid userId = AuthService.AuthService.Instance.GetUserId(UsernameTextBox.Text); if (userId != Guid.Empty) { //Now that the account exists on server try and create the necessary details and add them to the account if (await WindowsHelloHelper.CreateWindowsHelloKeyAsync(userId, UsernameTextBox.Text)) { //Navigate to the Welcome Screen. Frame.Navigate(typeof(Welcome), AuthService.AuthService.Instance.GetUserAccount(userId)); } else { //The Windows Hello account creation failed. //Remove the account from the server as the details were not configured await AuthService.AuthService.Instance.WindowsHelloRemoveUserAsync(userId); ErrorMessage.Text = "Account Creation Failed"; } } } else { ErrorMessage.Text = "Please enter a username"; } }
Compile e execute o aplicativo. Entre na conta de usuário de exemplo com as credenciais "sampleUsername" e "samplePassword". Na tela de boas-vindas, você pode notar que o botão Esquecer dispositivos é exibido, mas não há dispositivos. Quando você está criando ou migrando um usuário para trabalhar com o Windows Hello, as informações da conta não estão sendo enviadas por push para o AuthService.
Para obter as informações da conta do Windows Hello para o AuthService, o WindowsHelloHelper.cs precisará ser atualizado. No método CreateWindowsHelloKeyAsync, em vez de retornar
true
apenas no caso bem-sucedido, você precisará chamar um novo método que tentará obter o KeyAttestation. Embora este laboratório prático não esteja registrando essas informações no AuthService, você aprenderá como obteria essas informações no lado do cliente. Atualize o método CreateWindowsHelloKeyAsync da seguinte maneira: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; }
Crie um método GetKeyAttestationAsync no WindowsHelloHelper.cs. Este método demonstrará como obter todas as informações necessárias que podem ser fornecidas pelo Windows Hello para cada conta em um dispositivo específico.
using Windows.Storage.Streams; private static async Task GetKeyAttestationAsync(Guid userId, KeyCredentialRetrievalResult keyCreationResult) { KeyCredential userKey = keyCreationResult.Credential; IBuffer publicKey = userKey.RetrievePublicKey(); KeyCredentialAttestationResult keyAttestationResult = await userKey.GetAttestationAsync(); IBuffer keyAttestation = null; IBuffer certificateChain = null; bool keyAttestationIncluded = false; bool keyAttestationCanBeRetrievedLater = false; KeyCredentialAttestationStatus keyAttestationRetryType = 0; if (keyAttestationResult.Status == KeyCredentialAttestationStatus.Success) { keyAttestationIncluded = true; keyAttestation = keyAttestationResult.AttestationBuffer; certificateChain = keyAttestationResult.CertificateChainBuffer; Debug.WriteLine("Successfully made key and attestation"); } else if (keyAttestationResult.Status == KeyCredentialAttestationStatus.TemporaryFailure) { keyAttestationRetryType = KeyCredentialAttestationStatus.TemporaryFailure; keyAttestationCanBeRetrievedLater = true; Debug.WriteLine("Successfully made key but not attestation"); } else if (keyAttestationResult.Status == KeyCredentialAttestationStatus.NotSupported) { keyAttestationRetryType = KeyCredentialAttestationStatus.NotSupported; keyAttestationCanBeRetrievedLater = false; Debug.WriteLine("Key created, but key attestation not supported"); } Guid deviceId = Helpers.GetDeviceId(); //Update the Windows Hello details with the information we have just fetched above. //await UpdateWindowsHelloDetailsAsync(userId, deviceId, publicKey.ToArray(), keyAttestationResult); }
Você deve ter notado no método GetKeyAttestationAsync que acabou de adicionar que a última linha foi comentada. Essa última linha será um novo método que você criará e enviará todas as informações do Windows Hello para o AuthService. No mundo real, você precisaria enviar isso para um servidor real por meio de uma 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; }
Remova o comentário da última linha no método GetKeyAttestationAsync para que as informações do Windows Hello sejam enviadas para o AuthService.
Compile e execute o aplicativo e entre com as credenciais padrão como antes. Na página de boas-vindas , você verá que o ID do dispositivo é exibido. Se você fez login em outro dispositivo, isso também será exibido aqui (se você tiver um serviço de autenticação hospedado na nuvem). Para este laboratório prático, o ID real do dispositivo está sendo exibido. Em uma implementação real, você gostaria de exibir um nome amigável que uma pessoa pudesse entender e usar para identificar cada dispositivo.
Para concluir este laboratório prático, você precisa de uma solicitação e um desafio para o usuário quando ele selecionar na página de seleção de usuários e entrar novamente. O AuthService tem dois métodos que você criou para solicitar um desafio, um que usa um desafio assinado. Em WindowsHelloHelper.cs, crie um novo método chamado RequestSignAsync. Isso solicitará um desafio do AuthService, assinará localmente esse desafio usando uma API do Windows Hello e enviará o desafio assinado para o AuthService. Neste laboratório prático, o AuthService receberá o desafio assinado e retornará
true
. Em uma implementação real, você precisaria implementar um mecanismo de verificação para determinar se o desafio foi assinado pelo usuário correto no dispositivo correto. Adicione o método abaixo ao 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; }
Na classe WindowsHelloHelper, chame o método RequestSignAsync do método GetWindowsHelloAuthenticationMessageAsync.
public static async Task<bool> GetWindowsHelloAuthenticationMessageAsync(UserAccount account) { KeyCredentialRetrievalResult openKeyResult = await KeyCredentialManager.OpenAsync(account.Username); // Calling OpenAsync will allow the user access to what is available in the app and will not require user credentials again. // If you wanted to force the user to sign in again you can use the following: // var consentResult = await Windows.Security.Credentials.UI.UserConsentVerifier.RequestVerificationAsync(account.Username); // This will ask for the either the password of the currently signed in Microsoft Account or the PIN used for Windows Hello. if (openKeyResult.Status == KeyCredentialStatus.Success) { //If OpenAsync has succeeded, the next thing to think about is whether the client application requires access to backend services. //If it does here you would Request a challenge from the Server. The client would sign this challenge and the server //would check the signed challenge. If it is correct it would allow the user access to the backend. //You would likely make a new method called RequestSignAsync to handle all this //for example, RequestSignAsync(openKeyResult); //Refer to the second Windows Hello sample for information on how to do this. return await RequestSignAsync(account.UserId, openKeyResult); } else if (openKeyResult.Status == KeyCredentialStatus.NotFound) { //If the _account is not found at this stage. It could be one of two errors. //1. Windows Hello has been disabled //2. Windows Hello has been disabled and re-enabled cause the Windows Hello Key to change. //Calling CreateWindowsHelloKeyAsync and passing through the account will attempt to replace the existing Windows Hello Key for that account. //If the error really is that Windows Hello is disabled then the CreateWindowsHelloKeyAsync method will output that error. if (await CreateWindowsHelloKeyAsync(account.UserId, account.Username)) { //If the Windows Hello Key was again successfully created, Windows Hello has just been reset. //Now that the Windows Hello Key has been reset for the _account retry sign in. return await GetWindowsHelloAuthenticationMessageAsync(account); } } // Can't use Windows Hello right now, try again later return false; }
Ao longo deste exercício, você atualizou o aplicativo do lado do cliente para usar o AuthService. Ao fazer isso, você conseguiu eliminar a necessidade da classe Account e da classe AccountHelper. Exclua a classe Account , a pasta Models e a classe AccountHelper na pasta Utils . Você precisará remover todas as referências ao
WindowsHelloLogin.Models
namespace em todo o aplicativo antes que a solução seja compilada com êxito.Crie e execute o aplicativo e aproveite o uso do Windows Hello com o serviço fictício e o banco de dados.
Neste laboratório prático, você aprendeu a usar as APIs do Windows Hello para substituir a necessidade de senhas ao usar a autenticação de um computador Windows. Quando você considera quanta energia é gasta por pessoas que mantêm senhas e dão suporte a senhas perdidas em sistemas existentes, você deve ver o benefício de mudar para esse novo sistema de autenticação do Windows Hello.
Deixamos como exercício para você os detalhes de como você implementará a autenticação no lado do serviço e do servidor. Espera-se que a maioria dos desenvolvedores tenha sistemas existentes que precisarão ser migrados para começar a trabalhar com o Windows Hello. Os detalhes de cada um desses sistemas serão diferentes.
Tópicos relacionados
Windows developer