チュートリアル: WPF デスクトップ アプリケーションのユーザーを認証する
このチュートリアルは、Windows Presentation Form (WPF) デスクトップ アプリの構築と、Microsoft Entra 管理センターを使用した認証のためのその準備を見ていくシリーズの、最後のパートです。 このシリーズのパート 1 では、外部テナントでアプリケーションを登録し、ユーザー フローを構成しました。 このチュートリアルでは、.NET WPF デスクトップ アプリを構築し、Microsoft Entra 外部 ID を使用してユーザーをサインインおよびサインアウトさせる方法について説明します。
このチュートリアルでは、次のことについて説明します。
- WPF デスクトップ アプリを構成して、そのアプリの登録の詳細を使用します。
- ユーザーをサインインさせ、ユーザーの代わりにトークンを取得するデスクトップ アプリをビルドします。
前提条件
- チュートリアル: .NET WPF アプリケーションでユーザーをサインインさせるように外部テナントを準備する。
- .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
トークンを取得するために必要なすべての主要なコンポーネントを含む Microsoft Authentication Library (MSAL) をインストールします。 また、デスクトップ認証ブローカーとの対話操作を処理する 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
プロパティのゲッターとセッターを定義します。 次のコードを追加します。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 がわからない場合は、テナントの詳細を読み取る方法を確認してください。- [Enter_the_Custom_Domain_Here] という値を持つ
knownAuthorities
プロパティを追加します。
カスタム URL ドメインが login.contoso.com、テナント ID が aaaabbbb-0000-cccc-1111-dddd2222eeee の場合、appsettings.json ファイルに変更を加えた後には、ファイルは次のスニペットのようになるはずです。
{
"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 ファイルに移動します。
このファイルでは、次の 2 つの手順を実行します。
- sign-in-dotnet-wpf.csproj ファイルを変更して、プロジェクトのコンパイル時に appsettings.json ファイルを出力ディレクトリにコピーするようにアプリに指示します。 以下のコードを 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 を作成する
MainWindow.xaml ファイルを変更して、アプリの UI 要素を追加します。 アプリのルート フォルダーにある 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
インターフェイスは 1 つのアカウントに関する情報を表します。トークンを取得するために、アプリは
AcquireTokenSilent
メソッドを使用してトークンを警告なしで取得し、受け入れ可能なトークンがキャッシュ内にあるかどうかを確認します。 たとえば、ユーザーがサインアウトしたため、AcquireTokenSilent
メソッドが失敗する可能性があります。ユーザーの操作によって解決できる問題が MSAL によって検出された場合、MSAL はMsalUiRequiredException
例外をスローします。 この例外により、アプリは対話操作でトークンを取得します。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
を実行してアプリを実行します。サンプルを起動すると、サインイン ボタンを含むウィンドウが表示されます。 [サインイン] ボタンを選択します。
サインイン ページで、アカウントのメール アドレスを入力します。 アカウントをお持ちでない場合は、[アカウントをお持ちではない場合、作成できます] を選択します。これで、サインアップ フローが開始されます。 このフローに従って、新しいアカウントを作成してサインインします。
サインインすると、正常なサインインと、取得したトークンに保存されているユーザー アカウントに関する基本情報を表示する画面が表示されます。 基本的な情報は、サインイン画面の [トークン情報] セクションに表示されます。