次の方法で共有


チュートリアル: WPF デスクトップ アプリケーションのユーザーを認証する

このチュートリアルは、Windows Presentation Form (WPF) デスクトップ アプリの構築と、Microsoft Entra 管理センターを使用した認証のためのその準備を見ていくシリーズの、最後のパートです。 このシリーズのパート 1 では、外部テナントでアプリケーションを登録し、ユーザー フローを構成しました。 このチュートリアルでは、.NET WPF デスクトップ アプリを構築し、Microsoft Entra 外部 ID を使用してユーザーをサインインおよびサインアウトさせる方法について説明します。

このチュートリアルでは、次のことについて説明します。

  • WPF デスクトップ アプリを構成して、そのアプリの登録の詳細を使用します。
  • ユーザーをサインインさせ、ユーザーの代わりにトークンを取得するデスクトップ アプリをビルドします。

前提条件

WPF デスクトップ アプリケーションを作成する

  1. ターミナルを開き、プロジェクトを公開するフォルダーに移動します。

  2. 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 ファイルを作成する

  1. アプリのルート フォルダーに appsettings.json ファイルを作成します。

  2. アプリの登録の詳細を 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 に置き換えます。
  3. アプリ設定ファイルを作成したら、アプリ設定ファイルから構成を読み取るのに役立つ AzureAdConfig.cs という名前の別のファイルを作成します。 アプリのルート フォルダーで AzureAdConfig.cs ファイルを作成します。

  4. 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 ドメイン名にリダイレクトされず、あなたのドメインにとどまります。

カスタム ドメインを使用するには、次の手順に従います。

  1. 外部テナントのアプリに対してカスタム URL ドメインを有効にする 」の手順を使用して、外部テナントに対してカスタム URL ドメインを有効にします。

  2. appsettings.json ファイルを開きます。

    1. 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 がわからない場合は、テナントの詳細を読み取る方法を確認してください。
    2. [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"]
    }
}

プロジェクト ファイルを変更する

  1. アプリのルート フォルダーにある sign-in-dotnet-wpf.csproj ファイルに移動します。

  2. このファイルでは、次の 2 つの手順を実行します。

    1. sign-in-dotnet-wpf.csproj ファイルを変更して、プロジェクトのコンパイル時に appsettings.json ファイルを出力ディレクトリにコピーするようにアプリに指示します。 以下のコードを sign-in-dotnet-wpf.csproj ファイルに追加します。
    2. トークン キャッシュ ヘルパー クラスに表示されるように、ターゲット フレームワークをターゲット 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>
    

トークン キャッシュ ヘルパー クラスを作成する

トークン キャッシュを初期化するトークン キャッシュ ヘルパー クラスを作成します。 アプリケーションは、新しいトークンの取得を試みる前に、キャッシュからトークンの読み取りを試みます。 キャッシュでトークンが見つからない場合、アプリケーションは新しいトークンを取得します。 サインアウトすると、すべてのアカウントと対応するすべてのアクセス トークンのキャッシュがクリアされます。

  1. アプリのルート フォルダーで TokenCacheHelper.cs ファイルを作成します。

  2. TokenCacheHelper.cs ファイルを開きます。 ファイルにパッケージと名前空間を追加します。 次の手順では、関連するロジックを TokenCacheHelper クラスに追加して、このファイルにコード ロジックを設定します。

    using System.IO;
    using System.Security.Cryptography;
    using Microsoft.Identity.Client;
    
    namespace sign_in_dotnet_wpf
    {
        static class TokenCacheHelper{}
    }
    
  3. キャッシュ ファイルパスを定義する 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();
        }
    }
    
    
  4. トークン キャッシュのシリアル化を処理するコードを追加します。 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 要素の動作のランタイム ロジックを提供するコードが含まれています。

  1. アプリのルート フォルダーにある MainWindow.xaml.cs ファイルを開きます。

  2. ファイルに次のコードを追加してパッケージをインポートし、作成するメソッド向けのプレースホルダーを定義します。

    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){...}
        }
    }
    
  3. 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 はトークンの取得に使用する通常のメソッドになります

  4. 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 メソッドは、すべてのアクセス トークンと対応するすべてのアカウントのキャッシュをクリアします。 次回ユーザーがサインインしようとすると、対話操作でサインインする必要があります。

  5. 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 メソッドを使用することで、トークン キャッシュをシリアル化できます。

アプリを実行する

アプリを実行し、サインインしてアプリケーションをテストする

  1. ターミナルで、WPF アプリのルート フォルダーに移動し、ターミナルでコマンド dotnet run を実行してアプリを実行します。

  2. サンプルを起動すると、サインイン ボタンを含むウィンドウが表示されます。 [サインイン] ボタンを選択します。

    WPF デスクトップ アプリケーションのサインイン画面のスクリーンショット。

  3. サインイン ページで、アカウントのメール アドレスを入力します。 アカウントをお持ちでない場合は、[アカウントをお持ちではない場合、作成できます] を選択します。これで、サインアップ フローが開始されます。 このフローに従って、新しいアカウントを作成してサインインします。

  4. サインインすると、正常なサインインと、取得したトークンに保存されているユーザー アカウントに関する基本情報を表示する画面が表示されます。 基本的な情報は、サインイン画面の [トークン情報] セクションに表示されます。

関連項目