적용 대상: 근로자 테넌트
외부 임차인(자세히 알아보기)
이 자습서에서는 WPF(Windows Presentation Form) 데스크톱 앱을 빌드하고 Microsoft Entra 관리 센터를 사용하여 인증을 준비하는 방법을 보여 줍니다.
이 자습서에서는 다음을 수행합니다.
- 앱 등록 세부 정보를 사용하도록 WPF 데스크톱 앱을 구성합니다.
- 사용자를 로그인하고 사용자를 대신하여 토큰을 획득하는 데스크톱 앱을 빌드합니다.
필수 조건
-
모든 조직 디렉터리 및 개인 Microsoft 계정의 계정에 대해 구성된 Microsoft Entra 관리 센터에 새 앱을 등록합니다. 자세한 내용은 Register an application을 참조하십시오. 응용 프로그램 개요 페이지에서 다음 값을 나중에 사용할 수 있도록 기록하십시오.
- 애플리케이션(클라이언트) ID
- 디렉터리(테넌트) ID
- 디렉터리(테넌트) 도메인 이름(예: contoso.onmicrosoft.com 또는 contoso.com).
-
모바일 및 데스크톱 애플리케이션 플랫폼 구성을 사용하여 다음 리디렉션 URI를 추가합니다. 자세한 내용은 애플리케이션에서 리디렉션 URI를 추가하는 방법을 참조하세요 .
-
리디렉션 URI:
https://login.microsoftonline.com/common/oauth2/nativeclient
-
리디렉션 URI:
- Microsoft Entra 관리 센터의 사용자 흐름과 앱을 연결합니다. 이 사용자 흐름은 여러 애플리케이션에서 사용할 수 있습니다. 자세한 내용은 외부 테넌트 앱에 대한 셀프 서비스 등록 사용자 흐름 만들기 및 사용자 흐름애플리케이션 추가를 참조하세요.
- .NET 7.0 SDK 이상.
- React 애플리케이션을 지원하는 모든 IDE(통합 개발 환경)를 사용할 수 있지만 이 자습서에서는 Visual Studio Code를 사용합니다.
WPF 데스크톱 애플리케이션 만들기
터미널을 열고 프로젝트를 사용할 폴더로 이동합니다.
WPF 데스크톱 앱을 초기화하고 루트 폴더로 이동합니다.
dotnet new wpf --language "C#" --name sign-in-dotnet-wpf cd sign-in-dotnet-wpf
패키지 설치
앱 설정 파일의 키-값 쌍에서 앱이 구성 데이터를 읽는 데 도움이 되는 구성 공급자를 설치합니다. 이렇게 구성을 추상화하면 구성 값을 .NET 개체 인스턴스에 바인딩할 수 있게 됩니다.
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.Binder
토큰을 획득하는 데 필요한 모든 주요 구성 요소가 포함된 MSAL(Microsoft 인증 라이브러리)을 설치합니다. 또한 데스크톱 인증 브로커와의 상호 작용을 처리하는 MSAL 브로커 라이브러리를 설치합니다.
dotnet add package Microsoft.Identity.Client
dotnet add package Microsoft.Identity.Client.Broker
appsettings.json 파일 만들기 및 등록 구성 추가
앱의 루트 폴더에 appsettings.json 파일을 만듭니다.
appsettings.json 파일에 앱 등록 세부 정보를 추가합니다.
{ "AzureAd": { "Authority": "https://<Enter_the_Tenant_Subdomain_Here>.ciamlogin.com/", "ClientId": "<Enter_the_Application_Id_Here>" } }
-
Enter_the_Tenant_Subdomain_Here
를 디렉터리(테넌트) 하위 도메인으로 바꿉니다. -
Enter_the_Application_Id_Here
를 이전에 등록한 앱의 애플리케이션(클라이언트) ID로 바꿉니다.
-
앱 설정 파일을 만든 후 앱 설정 파일에서 구성을 읽는 데 도움이 되는 AzureAdConfig.cs라는 또 다른 파일을 만듭니다. 앱의 루트 폴더에 AzureAdConfig.cs 파일을 만듭니다.
AzureAdConfig.js 파일에서
ClientId
및Authority
속성에 대한 getter 및 setter를 정의합니다. 다음 코드를 추가합니다.namespace sign_in_dotnet_wpf { public class AzureAdConfig { public string Authority { get; set; } public string ClientId { get; set; } } }
사용자 지정 URL 도메인 사용(선택 사항)
사용자 지정 도메인을 사용하여 인증 URL을 완전히 브랜드화합니다. 사용자 관점에서 볼 때, 사용자는 인증 과정 동안 ciamlogin.com 도메인 이름으로 리디렉션되는 것이 아니라 도메인에 남아 있습니다.
사용자 지정 도메인을 사용하려면 다음 단계를 수행합니다.
외부 테넌트 앱에 대한 사용자 지정 URL 도메인을 활성화하는 단계를 사용하여 외부 테넌트에 사용자 지정 URL 도메인을 활성화하십시오.
appsettings.json 파일을 엽니다.
-
Authority
속성의 값을 https://Enter_the_Custom_Domain_Here/Enter_the_Tenant_ID_Here로 업데이트합니다.Enter_the_Custom_Domain_Here
를 사용자 지정 URL 도메인으로,Enter_the_Tenant_ID_Here
를 테넌트 ID로 바꿉니다. 테넌트 ID가 없는 경우 테넌트 세부 정보를읽는 방법을 알아봅니다. -
knownAuthorities
값을 갖는 속성을 추가합니다.
-
appsettings.json 파일을 변경한 후 사용자 지정 URL 도메인이 login.contoso.com이고 테넌트 ID가 aaaabbbb-0000-cccc-1111-dddd2222eeee인 경우 파일은 다음 코드 조각과 유사해야 합니다.
{
"AzureAd": {
"Authority": "https://login.contoso.com/aaaabbbb-0000-cccc-1111-dddd2222eeee",
"ClientId": "Enter_the_Application_Id_Here",
"KnownAuthorities": ["login.contoso.com"]
}
}
프로젝트 파일 수정
앱의 루트 폴더에 있는 sign-in-dotnet-wpf.csproj 파일로 이동합니다.
이 파일에서 다음 두 단계를 수행합니다.
- 프로젝트가 컴파일될 때 appsettings.json 파일을 출력 디렉터리에 복사하도록 앱에 지시하도록 sign-in-dotnet-wpf.csproj 파일을 수정합니다. sign-in-dotnet-wpf.csproj 파일에 다음 코드 조각을 추가합니다.
- 토큰 캐시 도우미 클래스에서 볼 수 있듯이 토큰 캐시에서 캐시된 토큰을 읽는 데 도움이 되도록 대상 프레임워크를 windows10.0.19041.0 빌드를 대상으로 설정합니다.
<Project Sdk="Microsoft.NET.Sdk"> ... <!-- Set target framework to target windows10.0.19041.0 build --> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net7.0-windows10.0.19041.0</TargetFramework> <!-- target framework --> <RootNamespace>sign_in_dotnet_wpf</RootNamespace> <Nullable>enable</Nullable> <UseWPF>true</UseWPF> </PropertyGroup> <!-- Copy appsettings.json file to output folder. --> <ItemGroup> <None Remove="appsettings.json" /> </ItemGroup> <ItemGroup> <EmbeddedResource Include="appsettings.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </EmbeddedResource> </ItemGroup> </Project>
토큰 캐시 도우미 클래스 만들기
토큰 캐시를 초기화하는 토큰 캐시 도우미 클래스를 만듭니다. 애플리케이션은 새 토큰을 획득하려고 시도하기 전에 캐시에서 토큰을 읽으려고 시도합니다. 캐시에서 토큰을 찾을 수 없으면 애플리케이션은 새 토큰을 획득합니다. 로그아웃하면 캐시에서 모든 계정과 해당 액세스 토큰이 모두 지워집니다.
앱의 루트 폴더에 TokenCacheHelper.cs 파일을 만듭니다.
TokenCacheHelper.cs 파일을 엽니다. 파일에 패키지와 네임스페이스를 추가합니다. 다음 단계에서는
TokenCacheHelper
클래스에 관련 논리를 추가하여 코드 논리로 이 파일을 채웁니다.using System.IO; using System.Security.Cryptography; using Microsoft.Identity.Client; namespace sign_in_dotnet_wpf { static class TokenCacheHelper{} }
캐시 파일 경로를 정의하는
TokenCacheHelper
클래스에 생성자를 추가합니다. 패키지된 데스크톱 앱(MSIX 패키지, 데스크톱 브리지라고도 함)의 경우 실행 중인 어셈블리 폴더는 읽기 전용입니다. 이 경우 패키지된 앱에 대한 앱별 읽기/쓰기 폴더인Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path + "\msalcache.bin"
을 사용해야 합니다.namespace sign_in_dotnet_wpf { static class TokenCacheHelper { static TokenCacheHelper() { try { CacheFilePath = Path.Combine(Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path, ".msalcache.bin3"); } catch (System.InvalidOperationException) { CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalcache.bin3"; } } public static string CacheFilePath { get; private set; } private static readonly object FileLock = new object(); } }
토큰 캐시 직렬화를 처리하는 코드를 추가합니다.
ITokenCache
인터페이스는 캐시 작업에 대한 공용 액세스를 구현합니다.ITokenCache
인터페이스에는 캐시 직렬화 이벤트를 구독하는 메서드가 포함되어 있는 반면, 인터페이스ITokenCacheSerializer
는 캐시를 직렬화/역직렬화하기 위해 캐시 직렬화 이벤트에 사용해야 하는 메서드를 노출합니다.TokenCacheNotificationArgs
에는 캐시에 액세스하는Microsoft.Identity.Client
(MSAL) 호출에 사용되는 매개 변수가 포함되어 있습니다.ITokenCacheSerializer
인터페이스는TokenCacheNotificationArgs
콜백에서 사용할 수 있습니다.TokenCacheHelper
클래스에 다음 코드를 추가합니다.static class TokenCacheHelper { static TokenCacheHelper() {...} public static string CacheFilePath { get; private set; } private static readonly object FileLock = new object(); public static void BeforeAccessNotification(TokenCacheNotificationArgs args) { lock (FileLock) { args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath) ? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath), null, DataProtectionScope.CurrentUser) : null); } } public static void AfterAccessNotification(TokenCacheNotificationArgs args) { if (args.HasStateChanged) { lock (FileLock) { File.WriteAllBytes(CacheFilePath, ProtectedData.Protect(args.TokenCache.SerializeMsalV3(), null, DataProtectionScope.CurrentUser) ); } } } } internal static void EnableSerialization(ITokenCache tokenCache) { tokenCache.SetBeforeAccess(BeforeAccessNotification); tokenCache.SetAfterAccess(AfterAccessNotification); }
BeforeAccessNotification
메서드에서는 파일 시스템에서 캐시를 읽고, 캐시가 비어 있지 않으면 캐시를 역직렬화하고 로드합니다.AfterAccessNotification
메서드는Microsoft.Identity.Client
(MSAL)이 캐시에 액세스한 후에 호출됩니다. 캐시가 변경된 경우 캐시를 직렬화하고 변경 내용을 캐시에 유지합니다.EnableSerialization
에는ITokenCache.SetBeforeAccess()
및ITokenCache.SetAfterAccess()
메서드가 포함되어 있습니다.-
ITokenCache.SetBeforeAccess()
는 라이브러리 메서드가 캐시에 액세스하기 전에 알림을 받도록 대리자를 설정합니다. 이는TokenCacheNotificationArgs
에 지정된 애플리케이션 및 계정에 대한 캐시 항목을 역직렬화하는 옵션을 대리자에게 제공합니다. -
ITokenCache.SetAfterAccess()
는 라이브러리 메서드가 캐시에 액세스한 후 알림을 받도록 대리자를 설정합니다. 이는TokenCacheNotificationArgs
에 지정된 애플리케이션 및 계정에 대한 캐시 항목을 직렬화하는 옵션을 대리자에게 제공합니다.
-
WPF 데스크톱 앱 UI 만들기
앱의 UI 요소를 추가하려면 MainWindow.xaml 파일을 수정합니다. 앱의 루트 폴더에서 MainWindow.xaml 파일을 열고 <Grid></Grid>
제어 섹션에 다음 코드 조각을 추가합니다.
<StackPanel Background="Azure">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="SignInButton" Content="Sign-In" HorizontalAlignment="Right" Padding="5" Click="SignInButton_Click" Margin="5" FontFamily="Segoe Ui"/>
<Button x:Name="SignOutButton" Content="Sign-Out" HorizontalAlignment="Right" Padding="5" Click="SignOutButton_Click" Margin="5" Visibility="Collapsed" FontFamily="Segoe Ui"/>
</StackPanel>
<Label Content="Authentication Result" Margin="0,0,0,-5" FontFamily="Segoe Ui" />
<TextBox x:Name="ResultText" TextWrapping="Wrap" MinHeight="120" Margin="5" FontFamily="Segoe Ui"/>
<Label Content="Token Info" Margin="0,0,0,-5" FontFamily="Segoe Ui" />
<TextBox x:Name="TokenInfoText" TextWrapping="Wrap" MinHeight="70" Margin="5" FontFamily="Segoe Ui"/>
</StackPanel>
이 코드는 주요 UI 요소를 추가합니다. UI 요소의 기능을 처리하는 메서드와 개체는 다음 단계에서 만드는 MainWindow.xaml.cs 파일에 정의됩니다.
- 사용자를 로그인시키는 단추입니다. 사용자가 이 단추를 선택하면
SignInButton_Click
메서드가 호출됩니다. - 사용자를 로그아웃시키는 단추입니다. 사용자가 이 단추를 선택하면
SignOutButton_Click
메서드가 호출됩니다. - 사용자가 로그인을 시도한 후 인증 결과 세부 정보를 표시하는 텍스트 상자입니다. 여기에 표시된 정보는
ResultText
개체에 의해 반환됩니다. - 사용자가 성공적으로 로그인한 후 토큰 세부 정보를 표시하는 텍스트 상자입니다. 여기에 표시된 정보는
TokenInfoText
개체에 의해 반환됩니다.
MainWindow.xaml.cs 파일에 코드 추가
MainWindow.xaml.cs 파일에는 MainWindow.xaml 파일의 UI 요소 동작에 대한 런타임 논리를 제공하는 코드가 포함되어 있습니다.
앱의 루트 폴더에서 MainWindow.xaml.cs 파일을 엽니다.
파일에 다음 코드를 추가하여 패키지를 가져오고, 우리가 생성할 메서드에 대한 자리 표시자를 정의합니다.
using Microsoft.Identity.Client; using System; using System.Linq; using System.Windows; using System.Windows.Interop; namespace sign_in_dotnet_wpf { public partial class MainWindow : Window { string[] scopes = new string[] { }; public MainWindow() { InitializeComponent(); } private async void SignInButton_Click(object sender, RoutedEventArgs e){...} private async void SignOutButton_Click(object sender, RoutedEventArgs e){...} private void DisplayBasicTokenInfo(AuthenticationResult authResult){...} } }
SignInButton_Click
메서드에 다음 코드를 추가합니다. 이 메서드는 사용자가 로그인 단추를 선택할 때 호출됩니다.private async void SignInButton_Click(object sender, RoutedEventArgs e) { AuthenticationResult authResult = null; var app = App.PublicClientApp; ResultText.Text = string.Empty; TokenInfoText.Text = string.Empty; IAccount firstAccount; var accounts = await app.GetAccountsAsync(); firstAccount = accounts.FirstOrDefault(); try { authResult = await app.AcquireTokenSilent(scopes, firstAccount) .ExecuteAsync(); } catch (MsalUiRequiredException ex) { try { authResult = await app.AcquireTokenInteractive(scopes) .WithAccount(firstAccount) .WithParentActivityOrWindow(new WindowInteropHelper(this).Handle) .WithPrompt(Prompt.SelectAccount) .ExecuteAsync(); } catch (MsalException msalex) { ResultText.Text = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}"; } catch (Exception ex) { ResultText.Text = $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}"; return; } if (authResult != null) { ResultText.Text = "Sign in was successful."; DisplayBasicTokenInfo(authResult); this.SignInButton.Visibility = Visibility.Collapsed; this.SignOutButton.Visibility = Visibility.Visible; } } }
GetAccountsAsync()
는 앱의 사용자 토큰 캐시에서 사용 가능한 모든 계정을 반환합니다.IAccount
인터페이스는 단일 계정에 대한 정보를 나타냅니다.토큰을 획득하기 위해 앱은 허용 가능한 토큰이 캐시에 있는지 확인하기 위해
AcquireTokenSilent
메서드를 사용하여 자동으로 토큰 획득을 시도합니다. 예를 들어 사용자가 로그아웃했기 때문에AcquireTokenSilent
메서드가 실패할 수 있습니다. MSAL이 대화형 작업을 요구해 이 문제를 해결할 수 있다고 감지하면MsalUiRequiredException
예외를 throw합니다. 이 예외로 인해 앱은 대화형으로 토큰을 획득합니다.AcquireTokenInteractive
메서드를 호출하면 사용자에게 로그인하라는 창이 표시됩니다. 앱에서는 일반적으로 사용자가 처음 인증해야 할 때 대화형으로 로그인하도록 요구합니다. 토큰을 획득하는 조용한 작업 중에 로그인해야 할 수도 있습니다.AcquireTokenInteractive
가 처음으로 실행된 후에는AcquireTokenSilent
가 토큰을 가져오는 데 사용하는 일반적인 방법이 됩니다.SignOutButton_Click
메서드에 다음 코드를 추가합니다. 이 메서드는 사용자가 로그아웃 단추를 선택할 때 호출됩니다.private async void SignOutButton_Click(object sender, RoutedEventArgs e) { var accounts = await App.PublicClientApp.GetAccountsAsync(); if (accounts.Any()) { try { await App.PublicClientApp.RemoveAsync(accounts.FirstOrDefault()); this.ResultText.Text = "User has signed-out"; this.TokenInfoText.Text = string.Empty; this.SignInButton.Visibility = Visibility.Visible; this.SignOutButton.Visibility = Visibility.Collapsed; } catch (MsalException ex) { ResultText.Text = $"Error signing-out user: {ex.Message}"; } } }
SignOutButton_Click
메서드는 모든 계정의 캐시와 해당 액세스 토큰을 모두 지웁니다. 사용자가 다음에 로그인을 시도할 때는 대화형으로 로그인해야 합니다.DisplayBasicTokenInfo
메서드에 다음 코드를 추가합니다. 이 메서드는 토큰에 대한 기본 정보를 표시합니다.private void DisplayBasicTokenInfo(AuthenticationResult authResult) { TokenInfoText.Text = ""; if (authResult != null) { TokenInfoText.Text += $"Username: {authResult.Account.Username}" + Environment.NewLine; TokenInfoText.Text += $"{authResult.Account.HomeAccountId}" + Environment.NewLine; } }
App.xaml.cs 파일에 코드 추가
App.xaml은 앱 전체에서 사용되는 리소스를 선언하는 파일입니다. 이는 앱의 진입점입니다. App.xaml.cs는 App.xaml의 코드 숨김 파일입니다. App.xaml.cs는 애플리케이션의 시작 창도 정의합니다.
앱의 루트 폴더에서 App.xaml.cs 파일을 열고 다음 코드를 추가합니다.
using System.Windows;
using System.Reflection;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Broker;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;
namespace sign_in_dotnet_wpf
{
public partial class App : Application
{
static App()
{
CreateApplication();
}
public static void CreateApplication()
{
var assembly = Assembly.GetExecutingAssembly();
using var stream = assembly.GetManifestResourceStream("sign_in_dotnet_wpf.appsettings.json");
AppConfiguration = new ConfigurationBuilder()
.AddJsonStream(stream)
.Build();
AzureAdConfig azureADConfig = AppConfiguration.GetSection("AzureAd").Get<AzureAdConfig>();
var builder = PublicClientApplicationBuilder.Create(azureADConfig.ClientId)
.WithAuthority(azureADConfig.Authority)
.WithDefaultRedirectUri();
_clientApp = builder.Build();
TokenCacheHelper.EnableSerialization(_clientApp.UserTokenCache);
}
private static IPublicClientApplication _clientApp;
private static IConfiguration AppConfiguration;
public static IPublicClientApplication PublicClientApp { get { return _clientApp; } }
}
}
이 단계에서는 appsettings.json 파일을 로드합니다. 구성 빌더는 appsettings.json 파일에 정의된 앱 구성을 읽는 데 도움이 됩니다. 또한 WPF 앱은 데스크톱 앱이므로 공용 클라이언트 앱으로 정의합니다.
TokenCacheHelper.EnableSerialization
메서드는 토큰 캐시 직렬화를 사용하도록 설정합니다.
앱 실행
앱을 실행하고 로그인하여 애플리케이션을 테스트합니다.
터미널에서 WPF 앱의 루트 폴더로 이동하고 터미널에서
dotnet run
명령을 실행하여 앱을 실행합니다.샘플을 실행하면 로그인 단추가 있는 창이 표시됩니다. 로그인 단추를 선택합니다.
로그인 페이지에서 계정 이메일 주소를 입력합니다. 계정이 없으면 계정이 없나요? 새로 만드세요를 선택하여 등록 흐름을 시작합니다. 이 흐름에 따라 새 계정을 만들고 로그인합니다.
로그인하면 성공적인 로그인과 검색된 토큰에 저장된 사용자 계정에 대한 기본 정보를 표시하는 화면이 표시됩니다. 기본 정보는 로그인 화면의 토큰 정보 섹션에 표시됩니다.