Windows Hello 로그인 앱 만들기

이 문서는 기존 사용자 이름 및 암호 인증 시스템 대신 Windows Hello를 사용해서 Windows 10 그리고 11 UWP(유니버설 Windows 플랫폼) 앱을 만드는 방법을 안내하는 안내서의 첫번째 파트입니다. 앱은 사용자 이름으로 로그인하고 각 계정에 Hello 키를 만듭니다. 이러한 계정은 Windows Hello 구성의 Windows 설정에 설정된 PIN의 보호를 받습니다.

이 안내서는 앱을 만드는 파트 그리고 백 엔드 서비스를 연결하는 파트, 이렇게 2가지 파트로 나누어져 있습니다. 이 문서를 마치면 제2부: Windows Hello 로그인 서비스를 계속 진행하세요.

시작하기 전에 Windows Hello 작동 방식을 전반적으로 이해하기 위해 Windows Hello 개요를 읽으셔야 합니다.

시작하기

이 프로젝트를 만드려면 C#과 XAML에 대한 약간의 경험이 필요합니다. 또한 Windows 10 또는 Windows 11 이 설치된 컴퓨터에서 Visual Studio 2015(Community Edition 이상) 또는 근래 버전의 Visual Studio를 사용해야 합니다. Visual Studio 2015가 최소 필수 버전이지만 최신 개발자 및 보안 업데이트를 위해 최신 버전의 Visual Studio를 사용하는 것이 좋습니다.

  • Visual Studio를 열고, [파일 > 새로 만들기 > 프로젝트]를 차례로 선택합니다.
  • 그러면 "새 프로잭트" 창이 열립니다. [템플릿 > Visual C#]으로 차례로 이동합니다.
  • 빈(블랭크) 앱(유니버설 Windows)을 선택하고 애플리케이션 이름을 "PassportLogin"으로 지정합니다.
  • 새 애플리케이션을 만들고 실행(F5)하면 화면에 빈 창이 나타납니다. 애플리케이션을 닫습니다.

Windows Hello new project

예제 1: Microsoft Passport로 로그인하기

이 연습에서는 컴퓨터에 Windows Hello가 설정되어 있는지 확인하는 방법과 Windows Hello를 사용하여 계정에 로그인하는 방법을 알아봅니다.

  • 새 프로젝트에서 솔루션 안에 "Views"라는 새 폴더를 만듭니다. 이 폴더에는 이 예시에서 탐색할 페이지들을 담게 됩니다. 솔루션 탐색기에서 프로젝트를 마우스 오른쪽 단추로 클릭하고, [추가 > 새 폴더]를 차례로 선택한 다음, 폴더 이름을 Views로 바꿉니다.

    Windows Hello add folder

  • 새 Views 폴더를 마우스 오른쪽 단추로 클릭하고, [추가 > 새 항목]을 차례로 선택하고, [빈 페이지]를 선택합니다. 이 페이지의 이름을 "Login.xaml"로 지정하세요.

    Windows Hello add blank page

  • 새 로그인 페이지에 대한 사용자 인터페이스를 설정하려면 다음 XAML을 추가합니다. 이 XAML은 다음 선택자를 정렬하는 스택패널을 정의합니다.

    • 제목이 들어갈 텍스트블록입니다.
    • 오류 메시지가 들어갈 텍스트블록입니다.
    • 입력될 사용자 이름이 들어갈 텍스트박스입니다.
    • 등록 페이지로 이동하는 버튼입니다.
    • Windows Hello의 상태를 포함하는 TextBlock
    • 로그인 페이지에서 백 엔드 또는 구성된 사용자가 없다는 설명이 들어갈 텍스트블록입니다.
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
      <StackPanel Orientation="Vertical">
        <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 username below" Margin="0,0,0,20"
                   TextWrapping="Wrap" Width="300"
                   TextAlignment="Center" VerticalAlignment="Center" FontSize="16"/>
        <TextBox x:Name="UsernameTextBox" Margin="4" Width="250"/>
        <Button x:Name="PassportSignInButton" Content="Login" Background="DodgerBlue" Foreground="White"
            Click="PassportSignInButton_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="PassportStatus" Background="#22B14C"
                   Margin="0,20" Height="100" >
          <TextBlock x:Name="PassportStatusText" Text="Microsoft Passport is ready to use!"
                 Margin="4" TextAlignment="Center" VerticalAlignment="Center" FontSize="20"/>
        </Border>
        <TextBlock x:Name="LoginExplaination" FontSize="24" TextAlignment="Center" TextWrapping="Wrap" 
            Text="Please Note: To demonstrate a login, validation will only occur using the default username 'sampleUsername'"/>
      </StackPanel>
    </Grid>
    
  • 솔루션 빌드를 진행하려면 코드 숨김방식에 몇 가지 메서드가 추가되야 합니다. F7 키를 누르거나 솔루션 탐색기를 사용하여 Login.xaml.cs 을 찾으세요. 다음 두 이벤트 메서드를 추가하여 로그인 및 등록 이벤트를 처리하도록 합니다. 지금은 이러한 메서드는 ErrorMessage.Text를 빈 스트링으로 설정하게 됩니다.

    namespace PassportLogin.Views
    {
        public sealed partial class Login : Page
        {
            public Login()
            {
                this.InitializeComponent();
            }
    
            private void PassportSignInButton_Click(object sender, RoutedEventArgs e)
            {
                ErrorMessage.Text = "";
            }
            private void RegisterButtonTextBlock_OnPointerPressed(object sender, PointerRoutedEventArgs e)
            {
                ErrorMessage.Text = "";
            }
        }
    }
    
  • 로그인 페이지를 렌더링하려면 MainPage 코드를 편집하여 MainPage가 로드될 때 로그인 페이지로 이동하게 합니다. MainPage.xaml.cs 파일을 엽니다. 솔루션 탐색기에서 MainPage.xaml.cs을 더블 클릭합니다. 찾을 수 없는 경우 MainPage.xaml 옆에 있는 작은 화살표를 클릭하여 숨겨진 코드를 나타나게 하세요. 로드된 이벤트 처리기 메서드를 만들어 로그인 페이지로 이동되로록 합니다. Views 네임스페이스에 대한 참조를 추가해야 합니다.

    using PassportLogin.Views;
    
    namespace PassportLogin
    {
        public sealed partial class MainPage : Page
        {
            public MainPage()
            {
                this.InitializeComponent();
                Loaded += MainPage_Loaded;
            }
    
            private void MainPage_Loaded(object sender, RoutedEventArgs e)
            {
                Frame.Navigate(typeof(Login));
            }
        }
    }
    
  • 로그인 페이지에서 Windows Hello를 이 컴퓨터에서 사용할 수 있는지 확인하는 OnNavigatedTo 이벤트를 처리해야 합니다. Login.xaml.cs 에서 다음을 실행합니다. MicrosoftPassportHelper 개체가 오류 플래그를 지정하는 것을 볼 수 있습니다. 이는 Microsoft가 이를 아직 구현하지 않았기 때문입니다.

    public sealed partial class Login : Page
    {
        public Login()
        {
            this.InitializeComponent();
        }
    
        protected override async void OnNavigatedTo(NavigationEventArgs e)
        {
            // Check Microsoft Passport is setup and available on this machine
            if (await MicrosoftPassportHelper.MicrosoftPassportAvailableCheckAsync())
            {
            }
            else
            {
                // Microsoft Passport is not setup so inform the user
                PassportStatus.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 50, 170, 207));
                PassportStatusText.Text = "Microsoft Passport is not setup!\n" + 
                    "Please go to Windows Settings and set up a PIN to use it.";
                PassportSignInButton.IsEnabled = false;
            }
        }
    }
    
  • MicrosoftPassportHelper 클래스를 만들려면 마우스 오른쪽 단추로 PassportLogin(유니버설 Windows) 솔루션을 클릭하고, [추가 > 새 폴더]를 차례로 클릭합니다. 이 폴더 이름을 Utils로 지정하세요.

    passport create helper class

  • 마우스 오른쪽 단추로 Utils 폴더를 클릭하고, [추가 >클래스]를 차례로 클릭합니다. 이 클래스의 이름을 "MicrosoftPassportHelper.cs"로 지정하세요.

  • MicrosoftPassportHelper의 클래스 정의를 public static으로 변경한 다음, Windows Hello를 사용할 준비가 되었는지 여부를 사용자에게 알려주는 다음 메서드를 추가합니다. 필요한 네임스페이스를 추가해야 합니다.

    using System;
    using System.Diagnostics;
    using System.Threading.Tasks;
    using Windows.Security.Credentials;
    
    namespace PassportLogin.Utils
    {
        public static class MicrosoftPassportHelper
        {
            /// <summary>
            /// Checks to see if Passport is ready to be used.
            /// 
            /// Passport has dependencies on:
            ///     1. Having a connected Microsoft Account
            ///     2. Having a Windows PIN set up for that _account on the local machine
            /// </summary>
            public static async Task<bool> MicrosoftPassportAvailableCheckAsync()
            {
                bool keyCredentialAvailable = await KeyCredentialManager.IsSupportedAsync();
                if (keyCredentialAvailable == false)
                {
                    // Key credential is not enabled yet as user 
                    // needs to connect to a Microsoft Account and select a PIN in the connecting flow.
                    Debug.WriteLine("Microsoft Passport is not setup!\nPlease go to Windows Settings and set up a PIN to use it.");
                    return false;
                }
    
                return true;
            }
        }
    }
    
  • Login.xaml.cs 에서 Utils 네임스페이스에 대한 참조를 추가하세요. 그러면 OnNavigatedTo 메서드의 오류가 해결될겁니다.

    using PassportLogin.Utils;
    
  • 애플리케이션을 빌드 그리고 실행합니다 (F5). 로그인 페이지로 이동하면 Hello를 사용할 준비가 되었을 때 Windows Hello 배너가 표시됩니다. 컴퓨터에 Windows Hello 상태를 나타내는 녹색 또는 파란색 배너가 표시됩니다.

    Windows Hello login screen ready

    Windows Hello login screen not setup

  • 다음으로 해야 될 일은 로그인을 위한 로직을 빌드하는 겁니다. NonClaims라는 새 폴더를 만드세요.

  • Models 폴더에 "Account.cs"라는 새 클래스를 만듭니다. 이 클래스는 계정 모델 역할을 하게 됩니다. 이건 샘플이므로 사용자 이름만 가지게 됩니다. 클래스 정의를 public으로 변경하고 Username 속성을 추가하세요.

    namespace PassportLogin.Models
    {
        public class Account
        {
            public string Username { get; set; }
        }
    }
    
  • 계정들을 처리할 방법이 필요합니다. 이 실습의 경우 서버 또는 데이터베이스가 없기 때문에 사용자 목록이 로컬에 저장되고 로드됩니다. Utils 폴더를 마우스 우 클릭하고 "AccountHelper.cs"라는 새 클래스를 추가합니다. 클래스 정의를 public static으로 변경하세요. AccountHelper는 계정 목록을 로컬로 저장하고 로드하는 데 필요한 모든 메서드를 포함하는 스태틱 클래스입니다. 저장 및 불러오기는 XmlSerializer를 사용하여 작동됩니다. 저장한 파일과 저장한 위치 또한 기억해야 합니다.

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Text;
    using System.Threading.Tasks;
    using System.Xml.Serialization;
    using Windows.Storage;
    using PassportLogin.Models;
    
    namespace PassportLogin.Utils
    {
        public static class AccountHelper
        {
            // In the real world this would not be needed as there would be a server implemented that would host a user account database.
            // For this tutorial we will just be storing accounts locally.
            private const string USER_ACCOUNT_LIST_FILE_NAME = "accountlist.txt";
            private static string _accountListPath = Path.Combine(ApplicationData.Current.LocalFolder.Path, USER_ACCOUNT_LIST_FILE_NAME);
            public static List<Account> AccountList = new List<Account>();
    
            /// <summary>
            /// Create and save a useraccount list file. (Updating the old one)
            /// </summary>
            private static async void SaveAccountListAsync()
            {
                string accountsXml = SerializeAccountListToXml();
    
                if (File.Exists(_accountListPath))
                {
                    StorageFile accountsFile = await StorageFile.GetFileFromPathAsync(_accountListPath);
                    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>
            public static async Task<List<Account>> LoadAccountListAsync()
            {
                if (File.Exists(_accountListPath))
                {
                    StorageFile accountsFile = await StorageFile.GetFileFromPathAsync(_accountListPath);
    
                    string accountsXml = await FileIO.ReadTextAsync(accountsFile);
                    DeserializeXmlToAccountList(accountsXml);
                }
    
                return AccountList;
            }
    
            /// <summary>
            /// Uses the local list of accounts and returns an XML formatted string representing the list
            /// </summary>
            /// <returns>XML formatted list of accounts</returns>
            public static string SerializeAccountListToXml()
            {
                XmlSerializer xmlizer = new XmlSerializer(typeof(List<Account>));
                StringWriter writer = new StringWriter();
                xmlizer.Serialize(writer, AccountList);
    
                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>
            public static List<Account> DeserializeXmlToAccountList(string listAsXml)
            {
                XmlSerializer xmlizer = new XmlSerializer(typeof(List<Account>));
                TextReader textreader = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(listAsXml)));
    
                return AccountList = (xmlizer.Deserialize(textreader)) as List<Account>;
            }
        }
    }
    
  • 다음으로, 로컬 계정 목록에서 계정을 추가하고 제거하는 방법을 구현하세요. 이러한 작업은 각각 목록을 저장하게 됩니다. 이 실습 랩에 필요한 마지막 메서드는 유효성 검사 메서드입니다. 인증 서버 또는 사용자 데이터베이스가 없으므로 하드 코딩된 단일 사용자에 대해서 유효성을 검사하게 됩니다. 이러한 메서드들은 AccountHelper 클래스에 추가되어야 합니다.

    public static Account AddAccount(string username)
            {
                // Create a new account with the username
                Account account = new Account() { Username = username };
                // Add it to the local list of accounts
                AccountList.Add(account);
                // SaveAccountList and return the account
                SaveAccountListAsync();
                return account;
            }
    
            public static void RemoveAccount(Account account)
            {
                // Remove the account from the accounts list
                AccountList.Remove(account);
                // Re save the updated list
                SaveAccountListAsync();
            }
    
            public static bool ValidateAccountCredentials(string username)
            {
                // In the real world, this method would call the server to authenticate that the account exists and is valid.
                // For this tutorial however we will just have a existing sample user that is just "sampleUsername"
                // If the username is null or does not match "sampleUsername" it will fail validation. In which case the user should register a new passport user
    
                if (string.IsNullOrEmpty(username))
                {
                    return false;
                }
    
                if (!string.Equals(username, "sampleUsername"))
                {
                    return false;
                }
    
                return true;
            }
    
  • 다음으로 해야 할 일은 사용자의 로그인 요청을 처리하는 겁니다. Login.xaml.cs 에 현재 계정 로그인을 보유할 새 프라이빗 변수를 만듭니다. 그런 다음 SignInPassport를 호출하는 새로운 메서드를 추가 합니다. 그러면 AccountHelper.ValidateAccountCredentials 메서드를 사용하여 계정 자격 증명의 유효성을 검사하게 됩니다. 입력한 사용자 이름이 이전 단계에서 설정한 하드 코딩된 스트링 값과 동일한 경우 이 메서드는 불값을 출력합니다. 이 샘플의 하드 코딩 값은 "sampleUsername"입니다.

    using PassportLogin.Models;
    using PassportLogin.Utils;
    using System.Diagnostics;
    
    namespace PassportLogin.Views
    {
        public sealed partial class Login : Page
        {
            private Account _account;
    
            public Login()
            {
                this.InitializeComponent();
            }
    
            protected override async void OnNavigatedTo(NavigationEventArgs e)
            {
                // Check Microsoft Passport is setup and available on this machine
                if (await MicrosoftPassportHelper.MicrosoftPassportAvailableCheckAsync())
                {
                }
                else
                {
                    // Microsoft Passport is not setup so inform the user
                    PassportStatus.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 50, 170, 207));
                    PassportStatusText.Text = "Microsoft Passport is not setup!\nPlease go to Windows Settings and set up a PIN to use it.";
                    PassportSignInButton.IsEnabled = false;
                }
            }
    
            private void PassportSignInButton_Click(object sender, RoutedEventArgs e)
            {
                ErrorMessage.Text = "";
                SignInPassport();
            }
    
            private void RegisterButtonTextBlock_OnPointerPressed(object sender, PointerRoutedEventArgs e)
            {
                ErrorMessage.Text = "";
            }
    
            private async void SignInPassport()
            {
                if (AccountHelper.ValidateAccountCredentials(UsernameTextBox.Text))
                {
                    // Create and add a new local account
                    _account = AccountHelper.AddAccount(UsernameTextBox.Text);
                    Debug.WriteLine("Successfully signed in with traditional credentials and created local account instance!");
    
                    //if (await MicrosoftPassportHelper.CreatePassportKeyAsync(UsernameTextBox.Text))
                    //{
                    //    Debug.WriteLine("Successfully signed in with Microsoft Passport!");
                    //}
                }
                else
                {
                    ErrorMessage.Text = "Invalid Credentials";
                }
            }
        }
    }
    
  • MicrosoftPassportHelper의 메서드를 참조하는 주석 처리된 코드를 볼수 있을겁니다. MicrosoftPassportHelper.cs 에 CreatePassportKeyAsync라는 새 메서드를 추가하세요. 이 메서드는 KeyCredentialManager에서 Windows Hello API를 사용합니다. RequestCreateAsync를 호출하면 accountId 와 로컬 컴퓨터에 특정된 패스포트 키가 만들어집니다. 실사용 상황에서 이를 구현하는대 관심이 있는 경우 switch 문의 주석을 확인 하세요.

    /// <summary>
    /// Creates a Passport key on the machine using the _account id passed.
    /// </summary>
    /// <param name="accountId">The _account id associated with the _account that we are enrolling into Passport</param>
    /// <returns>Boolean representing if creating the Passport key succeeded</returns>
    public static async Task<bool> CreatePassportKeyAsync(string accountId)
    {
        KeyCredentialRetrievalResult keyCreationResult = await KeyCredentialManager.RequestCreateAsync(accountId, KeyCredentialCreationOption.ReplaceExisting);
    
        switch (keyCreationResult.Status)
        {
            case KeyCredentialStatus.Success:
                Debug.WriteLine("Successfully made key");
    
                // In the real world authentication would take place on a server.
                // So every time a user migrates or creates a new Microsoft Passport account Passport details should be pushed to the server.
                // The details that would be pushed to the server include:
                // The public key, keyAttesation if available, 
                // certificate chain for attestation endorsement key if available,  
                // status code of key attestation result: keyAttestationIncluded or 
                // keyAttestationCanBeRetrievedLater and keyAttestationRetryType
                // As this sample has no concept of a server it will be skipped for now
                // for information on how to do this refer to the second Passport sample
    
                //For this sample just return true
                return true;
            case KeyCredentialStatus.UserCanceled:
                Debug.WriteLine("User cancelled sign-in process.");
                break;
            case KeyCredentialStatus.NotFound:
                // User needs to setup Microsoft Passport
                Debug.WriteLine("Microsoft Passport is not setup!\nPlease go to Windows Settings and set up a PIN to use it.");
                break;
            default:
                break;
        }
    
        return false;
    }
    
  • 이제 CreatePassportKeyAsync 메서드를 만들었으니 Login.xaml.cs 파일로 돌아가 SignInPassport 메서드의 코드 주석 처리를 제거합니다.

    private async void SignInPassport()
    {
        if (AccountHelper.ValidateAccountCredentials(UsernameTextBox.Text))
        {
            //Create and add a new local account
            _account = AccountHelper.AddAccount(UsernameTextBox.Text);
            Debug.WriteLine("Successfully signed in with traditional credentials and created local account instance!");
    
            if (await MicrosoftPassportHelper.CreatePassportKeyAsync(UsernameTextBox.Text))
            {
                Debug.WriteLine("Successfully signed in with Microsoft Passport!");
            }
        }
        else
        {
            ErrorMessage.Text = "Invalid Credentials";
        }
    }
    
  • 애플리케이션을 빌드 및 실행합니다. 로그인 페이지로 이동하게 됩니다. "sampleUsername"을 입력하고 로그인을 클릭하세요. PIN을 입력하라는 Windows Hello 프롬프트가 표시됩니다. PIN을 올바르게 입력하면 CreatePassportKeyAsync 메서드가 Windows Hello 키를 만들 수 있습니다. 성공 메시지가 표시되는지 출력 창을 모니터링 하세요.

    Windows Hello login pin prompt

예제 2: 시작 및 사용자 선택 페이지

이 예제에서 당신은 이전 예제를 이어서 진행하게 됩니다. 성공적으로 로그인하면 사용자는 계정을 로그아웃하거나 삭제할 수 있는 시작 페이지로 이동되어야 합니다. Windows Hello는 모든 컴퓨터에 대해 키를 만들므로 해당 컴퓨터에 로그인한 모든 사용자를 표시하는 사용자 선택 화면을 만들 수 있습니다. 그러면 사용자는 기기 사용 인증이 이미 된 상황이므로 이러한 계정 중 하나를 선택하여 암호를 다시 입력할 필요 없이 시작 화면으로 바로 이동할 수 있습니다.

  • Views 폴더에 "Welcome.xaml"이라는 새 빈 페이지를 추가하세요. 다음 XAML을 추가하여 사용자 인터페이스를 완성하세요. 그러면 제목, 로그인한 사용자 이름 그리고 두 개의 버튼이 표시됩니다. 버튼 중 하나는 사용자 목록(나중에 만들 예정)으로 다시 이동하고 다른 버튼은 이 사용자를 포겟 처리합니다.

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
      <StackPanel Orientation="Vertical">
        <TextBlock x:Name="Title" Text="Welcome" FontSize="40" TextAlignment="Center"/>
        <TextBlock x:Name="UserNameText" FontSize="28" TextAlignment="Center" Foreground="Black"/>
    
        <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"/>
      </StackPanel>
    </Grid>
    
  • Welcome.xaml.cs 코드 숨김 파일에 로그인된 계정을 보유할 새 프라이빗 변수를 추가합니다. OnNavigateTo 이벤트를 중단시키는 메서드를 구현하세요. 그러면 시작 페이지로 이동된 계정이 저장됩니다. 또한 XAML에 정의된 두 버튼에 대한 클릭 이벤트도 구현해야 합니다. Models 및 Utils 폴더에 대한 참조가 필요합니다.

    using PassportLogin.Models;
    using PassportLogin.Utils;
    using System.Diagnostics;
    
    namespace PassportLogin.Views
    {
        public sealed partial class Welcome : Page
        {
            private Account _activeAccount;
    
            public Welcome()
            {
                InitializeComponent();
            }
    
            protected override void OnNavigatedTo(NavigationEventArgs e)
            {
                _activeAccount = (Account)e.Parameter;
                if (_activeAccount != null)
                {
                    UserNameText.Text = _activeAccount.Username;
                }
            }
    
            private void Button_Restart_Click(object sender, RoutedEventArgs e)
            {
            }
    
            private void Button_Forget_User_Click(object sender, RoutedEventArgs e)
            {
                // Remove it from Microsoft Passport
                // MicrosoftPassportHelper.RemovePassportAccountAsync(_activeAccount);
    
                // Remove it from the local accounts list and resave the updated list
                AccountHelper.RemoveAccount(_activeAccount);
    
                Debug.WriteLine("User " + _activeAccount.Username + " deleted.");
            }
        }
    }
    
  • 사용자 클릭 포겟 이벤트에서 코멘트 아웃된 라인을 찾을 수 있을 겁니다. 로컬 목록에서 제거할 계정이지만 현재로서는 Windows Hello에서 제거할 방법이 없습니다. MicrosoftPassportHelper.cs에 Windows Hello 사용자를 제거할 새 메서드를 구현해야 합니다. 이 메서드는 다른 Windows Hello API를 사용하여 계정을 열고 삭제합니다. 실사용 환경에서 계정을 삭제할 때는 사용자 데이터베이스 유효성이 지켜지도록 서버 또는 데이터베이스에 알림을 보내야 합니다. Models 폴더에 대한 참조가 필요합니다.

    using PassportLogin.Models;
    
    /// <summary>
    /// Function to be called when user requests deleting their account.
    /// Checks the KeyCredentialManager to see if there is a Passport for the current user
    /// Then deletes the local key associated with the Passport.
    /// </summary>
    public static async void RemovePassportAccountAsync(Account account)
    {
        // Open the account with Passport
        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
            //for example, RemovePassportAccountOnServer(account);
        }
    
        // Then delete the account from the machines list of Passport Accounts
        await KeyCredentialManager.DeleteAsync(account.Username);
    }
    
  • Welcome.xaml.cs 로 돌아와서 RemovePassportAccountAsync를 호출하는 라인의 주석 처리를 제거하세요.

    private void Button_Forget_User_Click(object sender, RoutedEventArgs e)
    {
        // Remove it from Microsoft Passport
        MicrosoftPassportHelper.RemovePassportAccountAsync(_activeAccount);
    
        // Remove it from the local accounts list and resave the updated list
        AccountHelper.RemoveAccount(_activeAccount);
    
        Debug.WriteLine("User " + _activeAccount.Username + " deleted.");
    }
    
  • Login.xaml.cs의 SignInPassport 메서드에서 CreatePassportKeyAsync가 성공하면 시작 화면으로 이동하여 계정을 전달해야 합니다.

    private async void SignInPassport()
    {
        if (AccountHelper.ValidateAccountCredentials(UsernameTextBox.Text))
        {
            // Create and add a new local account
            _account = AccountHelper.AddAccount(UsernameTextBox.Text);
            Debug.WriteLine("Successfully signed in with traditional credentials and created local account instance!");
    
            if (await MicrosoftPassportHelper.CreatePassportKeyAsync(UsernameTextBox.Text))
            {
                Debug.WriteLine("Successfully signed in with Microsoft Passport!");
                Frame.Navigate(typeof(Welcome), _account);
            }
        }
        else
        {
            ErrorMessage.Text = "Invalid Credentials";
        }
    }
    
  • 애플리케이션을 빌드 및 실행합니다. "sampleUsername"으로 로그인하세요, 로그인을 클릭하세요. PIN을 입력하고 성공하면 시작 화면으로 이동되야 합니다. 사용자 포겟을 클릭하고 출력 창을 모니터링하여 사용자가 삭제되었는지 확인합니다. 사용자가 삭제되어도 당신은 시작 페이지에 머물러야 합니다. 앱이 탐색할 수 있는 사용자 선택 페이지를 만드세요.

    Windows Hello welcome screen

  • Views 폴더에서 "UserSelection.xaml"이라는 새 빈 페이지를 만들고 다음 XAML을 추가하여 사용자 인터페이스를 설정합니다. 이 페이지에는 로컬 계정 목록의 모든 사용자를 표시하는 ListView 와 사용자가 다른 계정을 추가할 수 있도록 로그인 페이지로 이동하는 버튼이 있습니다.

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
      <StackPanel Orientation="Vertical">
        <TextBlock x:Name="Title" Text="Select a User" FontSize="36" Margin="4" TextAlignment="Center" HorizontalAlignment="Center"/>
    
        <ListView x:Name="UserListView" Margin="4" MaxHeight="200" MinWidth="250" Width="250" HorizontalAlignment="Center">
          <ListView.ItemTemplate>
            <DataTemplate>
              <Grid Background="DodgerBlue" Height="50" Width="250" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                <TextBlock Text="{Binding Username}" HorizontalAlignment="Center" TextAlignment="Center" VerticalAlignment="Center" Foreground="White"/>
              </Grid>
            </DataTemplate>
          </ListView.ItemTemplate>
        </ListView>
    
        <Button x:Name="AddUserButton" Content="+" FontSize="36" Width="60" Click="AddUserButton_Click" HorizontalAlignment="Center"/>
      </StackPanel>
    </Grid>
    
  • UserSelection.xaml.cs 에 로컬 목록에 계정이 존재하지 않는 경우 로그인 페이지로 이동하는 로드된 메서드를 구현합니다. 또한 ListView에 대한 SelectionChanged 이벤트와 버튼에 대한 클릭 이벤트를 구현합니다.

    using System.Diagnostics;
    using PassportLogin.Models;
    using PassportLogin.Utils;
    
    namespace PassportLogin.Views
    {
        public sealed partial class UserSelection : Page
        {
            public UserSelection()
            {
                InitializeComponent();
                Loaded += UserSelection_Loaded;
            }
    
            private void UserSelection_Loaded(object sender, RoutedEventArgs e)
            {
                if (AccountHelper.AccountList.Count == 0)
                {
                    //If there are no accounts navigate to the LoginPage
                    Frame.Navigate(typeof(Login));
                }
    
    
                UserListView.ItemsSource = AccountHelper.AccountList;
                UserListView.SelectionChanged += UserSelectionChanged;
            }
    
            /// <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)
                {
                    Account account = (Account)((ListView)sender).SelectedValue;
                    if (account != null)
                    {
                        Debug.WriteLine("Account " + account.Username + " selected!");
                    }
                    Frame.Navigate(typeof(Login), account);
                }
            }
    
            /// <summary>
            /// Function called when the "+" button is clicked to add a new user.
            /// Navigates to the Login page with nothing filled out
            /// </summary>
            private void AddUserButton_Click(object sender, RoutedEventArgs e)
            {
                Frame.Navigate(typeof(Login));
            }
        }
    }
    
  • 앱에는 UserSelection 페이지로 이동해야하는 몇몇 곳이 있습니다. MainPage.xaml.cs 에서 로그인 페이지 대신 UserSelection 페이지로 이동해야 합니다. MainPage에서 로드된 이벤트에 있는 동안엔 계정 목록을 로드해서 UserSelection 페이지가 계정의 유무를 확인할수 있게 해야 합니다. 이렇게 하려면 로드된 메서드를 비동기로 변경해야 하고 Utils 폴더에 대한 참조 또한 추가해야 합니다.

    using PassportLogin.Utils;
    
    private async void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        // Load the local Accounts List before navigating to the UserSelection page
        await AccountHelper.LoadAccountListAsync();
        Frame.Navigate(typeof(UserSelection));
    }
    
  • 다음으로 시작 페이지에서 UserSelection 페이지로 이동하세요. 두 클릭 이벤트 모두에서 UserSelection 페이지로 다시 이동되야 합니다.

    private void Button_Restart_Click(object sender, RoutedEventArgs e)
    {
        Frame.Navigate(typeof(UserSelection));
    }
    
    private void Button_Forget_User_Click(object sender, RoutedEventArgs e)
    {
        // Remove it from Microsoft Passport
        MicrosoftPassportHelper.RemovePassportAccountAsync(_activeAccount);
    
        // Remove it from the local accounts list and resave the updated list
        AccountHelper.RemoveAccount(_activeAccount);
    
        Debug.WriteLine("User " + _activeAccount.Username + " deleted.");
    
        // Navigate back to UserSelection page.
        Frame.Navigate(typeof(UserSelection));
    }
    
  • 로그인 페이지에서 UserSelection 페이지의 목록에서 선택한 계정에 로그인 하려면 코드가 필요합니다. OnNavigatedTo 이벤트에서 탐색에 전송된 계정을 저장합니다. 먼저 계정이 존재하는 계정인지 식별하는 새 프라이빗 변수를 추가합니다. 그런 다음 OnNavigatedTo 이벤트를 처리하세요.

    namespace PassportLogin.Views
    {
        public sealed partial class Login : Page
        {
            private Account _account;
            private bool _isExistingAccount;
    
            public Login()
            {
                InitializeComponent();
            }
    
            /// <summary>
            /// Function called when this frame is navigated to.
            /// Checks to see if Microsoft Passport is available and if an account was passed in.
            /// If an account was passed in set the "_isExistingAccount" flag to true and set the _account
            /// </summary>
            protected override async void OnNavigatedTo(NavigationEventArgs e)
            {
                // Check Microsoft Passport is setup and available on this machine
                if (await MicrosoftPassportHelper.MicrosoftPassportAvailableCheckAsync())
                {
                    if (e.Parameter != null)
                    {
                        _isExistingAccount = true;
                        // Set the account to the existing account being passed in
                        _account = (Account)e.Parameter;
                        UsernameTextBox.Text = _account.Username;
                        SignInPassport();
                    }
                }
                else
                {
                    // Microsoft Passport is not setup so inform the user
                    PassportStatus.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 50, 170, 207));
                    PassportStatusText.Text = "Microsoft Passport is not setup!\n" + 
                        "Please go to Windows Settings and set up a PIN to use it.";
                    PassportSignInButton.IsEnabled = false;
                }
            }
        }
    }
    
  • 선택한 계정에 로그인하려면 SignInPassport 메서드를 업데이트해야 합니다. 계정에 이미 Passport 키가 생성되어 있으므로 MicrosoftPassportHelper는 Passport를 사용하여 계정을 여는 다른 메서드가 필요합니다. MicrosoftPassportHelper.cs에서 새 메서드를 구현하여 passport를 사용하여 기존 사용자를 로그인 시킵니다. 코드 각 부분에 대한 자세한 내용은 코드 주석을 참조하세요.

    /// <summary>
    /// Attempts to sign a message using the Passport key on the system for the accountId passed.
    /// </summary>
    /// <returns>Boolean representing if creating the Passport authentication message succeeded</returns>
    public static async Task<bool> GetPassportAuthenticationMessageAsync(Account 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 Microsoft Passport.
    
        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 Microsoft Passport 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. Microsoft Passport has been disabled
            // 2. Microsoft Passport has been disabled and re-enabled cause the Microsoft Passport Key to change.
            // Calling CreatePassportKey and passing through the account will attempt to replace the existing Microsoft Passport Key for that account.
            // If the error really is that Microsoft Passport is disabled then the CreatePassportKey method will output that error.
            if (await CreatePassportKeyAsync(account.Username))
            {
                // If the Passport Key was again successfully created, Microsoft Passport has just been reset.
                // Now that the Passport Key has been reset for the _account retry sign in.
                return await GetPassportAuthenticationMessageAsync(account);
            }
        }
    
        // Can't use Passport right now, try again later
        return false;
    }
    
  • 기존 계정을 처리하도록 Login.xaml.cs의 SignInPassport 메서드를 업데이트합니다. 이것은 MicrosoftPassportHelper.cs의 새 메서드를 사용하게 됩니다. 성공하면 계정이 로그인되고 사용자가 시작 화면으로 이동됩니다.

    private async void SignInPassport()
    {
        if (_isExistingAccount)
        {
            if (await MicrosoftPassportHelper.GetPassportAuthenticationMessageAsync(_account))
            {
                Frame.Navigate(typeof(Welcome), _account);
            }
        }
        else if (AccountHelper.ValidateAccountCredentials(UsernameTextBox.Text))
        {
            //Create and add a new local account
            _account = AccountHelper.AddAccount(UsernameTextBox.Text);
            Debug.WriteLine("Successfully signed in with traditional credentials and created local account instance!");
    
            if (await MicrosoftPassportHelper.CreatePassportKeyAsync(UsernameTextBox.Text))
            {
                Debug.WriteLine("Successfully signed in with Microsoft Passport!");
                Frame.Navigate(typeof(Welcome), _account);
            }
        }
        else
        {
            ErrorMessage.Text = "Invalid Credentials";
        }
    }
    
  • 애플리케이션을 빌드 및 실행합니다. "sampleUsername"을 사용하여 로그인합니다. PIN을 입력하면 성공하면 시작 화면으로 이동됩니다. 클릭하여 사용자 목록으로 돌아갑니다. 이제 목록에 사용자가 표시되는걸 볼 수 있습니다. 이 Passport를 클릭하면 암호 등을 다시 입력하지 않고도 다시 로그인할 수 있습니다.

    Windows Hello select user list

연습 3: 새 Windows Hello 사용자 등록

이 연습에서는 Windows Hello가 있는 새 계정을 만드는 새 페이지를 작성합니다. 이는 로그인 페이지의 방식과 유사하게 작동됩니다. 로그인 페이지는 Windows Hello를 사용하도록 마이그레이션하는 기존 사용자를 위해 구현되었습니다. PassportRegister 페이지는 새 사용자를 위한 Windows Hello 등록을 만듭니다.

  • views 폴더에서 "PassportRegister.xaml"이라는 새 빈 페이지를 만듭니다. XAML에서 다음을 추가하여 사용자 인터페이스를 만듭니다. 여기에 있는 인터페이스는 로그인 페이지와 흡사합니다.

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
      <StackPanel Orientation="Vertical">
        <TextBlock x:Name="Title" Text="Register New Passport User" FontSize="24" Margin="4" TextAlignment="Center"/>
    
        <TextBlock x:Name="ErrorMessage" Text="" FontSize="20" Margin="4" Foreground="Red" TextAlignment="Center"/>
    
        <TextBlock Text="Enter your new username below" Margin="0,0,0,20"
                   TextWrapping="Wrap" Width="300"
                   TextAlignment="Center" VerticalAlignment="Center" FontSize="16"/>
    
        <TextBox x:Name="UsernameTextBox" Margin="4" Width="250"/>
    
        <Button x:Name="PassportRegisterButton" Content="Register" Background="DodgerBlue" Foreground="White"
            Click="RegisterButton_Click_Async" Width="80" HorizontalAlignment="Center" Margin="0,20"/>
    
        <Border x:Name="PassportStatus" Background="#22B14C"
                   Margin="4" Height="100">
          <TextBlock x:Name="PassportStatusText" Text="Microsoft Passport is ready to use!" FontSize="20"
                 Margin="4" TextAlignment="Center" VerticalAlignment="Center"/>
        </Border>
      </StackPanel>
    </Grid>
    
  • PassportRegister.xaml.cs 숨김 코드 파일에서 등록 버튼에 대한 private Account 변수 및 클릭 이벤트를 구현합니다. 그러면 새 로컬 계정이 추가되고 Passport 키가 생성됩니다.

    using PassportLogin.Models;
    using PassportLogin.Utils;
    
    namespace PassportLogin.Views
    {
        public sealed partial class PassportRegister : Page
        {
            private Account _account;
    
            public PassportRegister()
            {
                InitializeComponent();
            }
    
            private async void RegisterButton_Click_Async(object sender, RoutedEventArgs e)
            {
                ErrorMessage.Text = "";
    
                //In the real world you would normally validate the entered credentials and information before 
                //allowing a user to register a new account. 
                //For this sample though we will skip that step and just register an account if username is not null.
    
                if (!string.IsNullOrEmpty(UsernameTextBox.Text))
                {
                    //Register a new account
                    _account = AccountHelper.AddAccount(UsernameTextBox.Text);
                    //Register new account with Microsoft Passport
                    await MicrosoftPassportHelper.CreatePassportKeyAsync(_account.Username);
                    //Navigate to the Welcome Screen. 
                    Frame.Navigate(typeof(Welcome), _account);
                }
                else
                {
                    ErrorMessage.Text = "Please enter a username";
                }
            }
        }
    }
    
  • 등록을 클릭하면 로그인 페이지에서 이 페이지로 이동되어야 합니다.

    private void RegisterButtonTextBlock_OnPointerPressed(object sender, PointerRoutedEventArgs e)
    {
        ErrorMessage.Text = "";
        Frame.Navigate(typeof(PassportRegister));
    }
    
  • 애플리케이션을 빌드 및 실행합니다. 새 사용자를 등록해 보세요. 그런 다음, 사용자 목록으로 돌아가서 해당 사용자가 유효한지 확인하고 로그인 시킵니다.

    Windows Hello register new user

이 실습에서는 새로운 Windows Hello API를 사용하여 기존 사용자를 인증하고 신규 사용자의 계정을 만드는 데 필요한 필수 기술을 알아보았습니다. 이 새로운 지식을 통해 사용자가 애플리케이션 암호를 기억할 필요성을 줄일 수 있습니다, 그리고 사용자 인증으로 보호되는 만큼 걱정할 필요는 없습니다. Windows 10 및 Windows 11은 Windows Hello의 새로운 인증 기술을 사용하여 생체 인식 로그인 옵션을 지원합니다.