WinForms/WPFアプリにEntra ID認証を組み込む ── MSAL.NETとWAMブローカーの実務構成

· · Windows, C#, .NET, WinForms, WPF, Entra ID, 認証, セキュリティ, 技術相談

「社内の業務アプリごとにログイン画面を作って、パスワードを自前のデータベースで管理している。退職者が出るたびにアプリごとにアカウントを止めて回るのがつらい」「Microsoft 365 は全社で入れているので、そのアカウントでそのままログインさせられないか」。デスクトップアプリの改修相談で、ここ数年着実に増えているテーマです。パスワードの漏えい事故やゼロトラスト対応の文脈で情報システム部門から「自前のパスワード管理をやめてほしい」と言われた、という形で来ることもあります。

結論から言えば、Microsoft 365 を使っている組織なら、WinForms / WPF の社内アプリのログインを Entra ID(旧 Azure AD) に寄せるのは筋の良い投資です。アプリはパスワードを一切預からなくなり、多要素認証・条件付きアクセス・サインインログといったテナント側の防御と監査がそのまま社内アプリにも効くようになります。実装も、MSAL.NET というライブラリと数十行のコードで済む範囲です。

ただし、デスクトップアプリならではの落とし穴がいくつかあります。「ユーザー名とパスワードをテキストボックスで受けて裏で認証する」昔ながらの作り(ROPC)は公式に廃止の方向で、新規に採用してはいけません。トークンキャッシュを永続化しないと起動のたびにサインイン画面が出ますし、Windows ではブローカー(WAM)を使うかどうかで体験とセキュリティが大きく変わります。この記事では、概念の最小限の整理から、アプリ登録、MSAL.NET の実装、WAM、キャッシュ、導入判断、運用の罠までを一通りまとめます。

1. まず結論

  • ID とパスワードの自前管理をやめて Entra ID に寄せると、パスワード保管・リセット対応・退職者のアカウント停止・サインイン監査が全部テナント側の仕事になります。アプリ側の責任範囲が劇的に小さくなるのが最大の利点です。
  • デスクトップアプリは パブリッククライアントです。exe は配布先で解析できるため、クライアントシークレットを持てません(持たせてはいけません)。アプリ登録もパブリッククライアントとして構成します。1
  • ユーザー名とパスワードをアプリが直接預かる ROPC(Resource Owner Password Credentials)は公式に「廃止(deprecated)」と明記され、移行ガイドが出ています。MFA・条件付きアクセスと両立せず、実質的に使えなくなる方向です。新規採用は禁止と考えてください。23
  • 実装は MSAL.NET(Microsoft.Identity.Client) で、まず AcquireTokenSilentMsalUiRequiredException が出たら AcquireTokenInteractive という呼び出しパターンが唯一の基本形です。4
  • Windows では WAM(Web Account Manager)ブローカー経由の認証が推奨です。Windows サインイン済みアカウントとの SSO、条件付きアクセス・Windows Hello・FIDO キー対応、リフレッシュトークンのデバイスバインドが、WithBroker の 1 行で手に入ります。5
  • トークンキャッシュの永続化を忘れると、アプリを再起動するたびにサインイン画面が出ますMicrosoft.Identity.Client.Extensions.Msal の暗号化キャッシュを最初から組み込んでください。6
  • Entra 認証はネットワーク前提の仕組みです。完全オフラインで動く必要がある現場アプリでは成立しないので、8 章の判断表で導入可否を先に見極めてください。

2. 全体像 ── 自前パスワード管理をやめるとはどういうことか

2.1 自前管理の何が問題か

業務アプリが自前のユーザーテーブルでパスワードを管理していると、次の責任がすべてアプリ(=開発した私たち)に乗ります。

  • 保管: ハッシュ方式の選定と実装(ソルトなし MD5 のまま放置された 15 年物のテーブル、いまだに見かけます)
  • 運用: パスワードリセットの問い合わせ対応、ロックアウト、初期パスワードの配布
  • ライフサイクル: 退職・異動時のアカウント停止。アプリが 5 本あれば 5 回止めて回る
  • 監査: 誰がいつログインしたかの記録と保全。多要素認証は事実上実装不可能

Entra ID に認証を委譲すると、この 4 つがアプリのコードから消えて、テナントの管理に一本化されます。退職者は Entra ID のアカウントを無効化すれば全アプリで即座にログイン不能になり、サインインログも自動で残ります。Microsoft 365 導入済みの組織で自前認証を続ける理由は、ほとんどありません。なお Google Workspace の組織で Windows ログオン自体を Google アカウントに寄せる対の仕組みは「GCPWとは」で書いています。

2.2 最小限の概念 ── パブリッククライアントとトークン

OAuth 2.0 / OpenID Connect の教科書的な説明は割愛して、デスクトップアプリの実装に必要な概念だけ並べます。

概念 デスクトップアプリでの意味
パブリッククライアント exe・モバイルアプリなど、秘密(クライアントシークレット)を安全に保持できないアプリ。ユーザーの代理でのみトークンを取得できる
機密クライアント(confidential client) Web サーバーやデーモンなど、シークレットや証明書を保持できるアプリ。デスクトップアプリはこちらではない
ID トークン 「この人が誰か」を表す JWT。ログイン機能だけ欲しい場合はこれで足りる
アクセストークン 特定の API(Microsoft Graph や自社 Web API)を呼ぶための通行証。宛先(audience)とスコープが焼き込まれている
リフレッシュトークン 上記 2 つを対話なしで更新するためのトークン。MSAL がキャッシュ内で自動管理し、アプリから直接は見えない

重要なのは 1 行目です。exe は配布先で解析・逆コンパイルできるため、埋め込んだ「秘密」は秘密になりません。だからシークレットなしで動くパブリッククライアントとして登録し、認証そのもの(パスワード入力や MFA)はブラウザまたは OS のブローカーに委ね、アプリはトークンだけを受け取る設計になります。アプリがユーザーのパスワードに触れないこと自体が、この仕組みの根幹です。

2.3 ROPCは終わった方式 ── 裏取りの結果

昔ながらの発想だと「自前のログイン画面でユーザー名とパスワードを受けて、裏で Entra ID に検証してもらえばいい」となりがちです。これが ROPC(ユーザー名パスワード直渡し)で、MSAL.NET にも AcquireTokenByUsernamePassword として残ってはいますが、公式ドキュメントの現在の記述は明確です。

  • パブリッククライアント向けの ROPC は「セキュリティリスクのため廃止(deprecated)された」と明記され、より安全なフローへの移行ガイドが公開されています。3
  • ROPC は MFA・条件付きアクセスと非互換です。テナントで MFA が必須化されているユーザーは、このフローではブロックされてサインインできません2
  • SSO が効かず、個人 Microsoft アカウントも使えず、パスワードレス(FIDO、Authenticator)のアカウントもサインインできません。2
  • Microsoft の Web API 側でも MFA 済みトークンしか受け付けない動きが進んでおり、公式ドキュメント自身が「ROPC に依存するアプリは締め出される(locked out)。デスクトップアプリはブローカーベースの認証へ移行せよ」と書いています。2

MFA の必須化はテナント側の設定でいつでも起こるので、「今は動いているから」で ROPC を採用すると、ある日突然全ユーザーがログインできなくなります。既存アプリが ROPC で動いている場合も移行を前提に計画を立ててください。デスクトップアプリで使ってよい取得方法は実質次の 2 つです。

フロー 使いどころ
対話型(ブローカー / ブラウザ) 通常の GUI アプリ。本命
デバイスコードフロー ブラウザを表示できない環境(SSH 先のコンソールなど)。URL とコードを表示し、別デバイスのブラウザでサインインしてもらう

3. アプリ登録 ── Entra管理センターでの設定

コードを書く前に、テナントにアプリを登録します。開発者が自分でできない場合は、この節の内容をそのまま情報システム部門への依頼書にしてください。

3.1 登録本体

Microsoft Entra 管理センター(entra.microsoft.com)の [アプリの登録] → [新規登録] で作成します。7

  • 名前: 同意画面やサインインログに表示されるので、「在庫管理システム」など業務側に通じる名前にします。
  • サポートされているアカウントの種類: 社内アプリなら「この組織ディレクトリのみのアカウント」(シングルテナント)一択です。マルチテナントは複数組織へ配布する製品の場合だけです。
  • 登録後に表示されるアプリケーション (クライアント) IDディレクトリ (テナント) ID を控え、アプリの設定に埋め込みます(どちらも秘密情報ではありません)。

3.2 リダイレクトURI ── プラットフォームは「モバイルアプリケーションとデスクトップアプリケーション」

認証後にトークンを受け取る場所の宣言です。[認証] → [プラットフォームを追加] → [モバイル アプリケーションとデスクトップ アプリケーション] を選び、認証方式に応じた URI を登録します。1

認証方式 登録するリダイレクトURI
WAM ブローカー(本命、5 章) ms-appx-web://microsoft.aad.brokerplugin/{クライアントID}
システムブラウザー http://localhost
埋め込みブラウザー https://login.microsoftonline.com/common/oauth2/nativeclient

WAM 用の ms-appx-web://... は MSAL 側のコードには書きませんが、アプリ登録側には必須です。8 WAM が使えない環境でのブラウザーフォールバック(5 章)を考えると、表の 3 つを最初から全部登録しておくのが実務的です。特に注意したいのが WithDefaultRedirectUri() の挙動で、解決先はプラットフォーム依存です。.NET Framework では https://login.microsoftonline.com/common/oauth2/nativeclient、.NET(Core 以降)では http://localhost に解決されます9 ms-appx-webhttp://localhost しか登録していない .NET Framework アプリが WAM からブラウザーへフォールバックすると、nativeclient との不一致で認証エラーになります。3 つとも登録するか、WithRedirectUri(...) で明示的に固定してください。「Web」プラットフォームの方へ登録してしまい認証エラーになるのも定番のつまずきです。

3.3 APIのアクセス許可と管理者の同意

[API のアクセス許可] で、アプリが呼ぶ API の委任されたアクセス許可(delegated permission)を追加します。ログインとプロフィール表示だけなら、既定で付与されている Microsoft Graph の User.Read で足ります。

追加したら [(テナント名)に管理者の同意を与えます] を実行してもらいます。7 これで初回サインイン時のユーザーごとの同意ダイアログが出なくなります。ユーザー自身の同意が無効化されたテナントでは、管理者の同意なしだと初回サインインが「管理者の承認が必要です」で止まるため、社内配布アプリでは配布前に管理者の同意まで済ませておくのが原則です。

3.4 「パブリック クライアント フローを許可する」フラグ

[認証] の詳細設定にある「パブリック クライアント フローを許可する」は、デバイスコードフローや統合 Windows 認証のようにリダイレクト URI を使わないフローを使う場合に「はい」にします。1 対話型(ブラウザー / ブローカー)だけなら必須ではありません。なお、このアプリ登録にはクライアントシークレットも証明書も作りません。「証明書とシークレット」欄が空なのがパブリッククライアントの正しい状態です(混同の相談が多いので 9 章で再度触れます)。

4. MSAL.NETでの実装 ── Silent→Interactiveの基本形

NuGet で Microsoft.Identity.Client を追加します。実装パターンは 1 つだけ覚えれば足ります。必ず AcquireTokenSilent を先に呼び、MsalUiRequiredException を受けたときだけ対話型にフォールバックします。AcquireTokenInteractive はキャッシュを一切見ない設計なので、いきなり呼ぶと毎回サインイン画面が出ます。4

using Microsoft.Identity.Client;

public sealed class AuthService
{
    private const string ClientId = "アプリケーション(クライアント)ID";
    private const string TenantId = "ディレクトリ(テナント)ID";
    private static readonly string[] Scopes = { "User.Read" };

    private readonly IPublicClientApplication _app;

    public AuthService()
    {
        _app = PublicClientApplicationBuilder.Create(ClientId)
            .WithAuthority(AzureCloudInstance.AzurePublic, TenantId)
            .WithRedirectUri("http://localhost")  // システムブラウザー用
            .Build();
        // 実運用ではここでトークンキャッシュの永続化を登録する(6章)
    }

    public async Task<AuthenticationResult> SignInAsync(IntPtr ownerHwnd)
    {
        // 1. キャッシュ済みアカウントでのサイレント取得を必ず先に試す
        var accounts = await _app.GetAccountsAsync();
        var account = accounts.FirstOrDefault();
        try
        {
            return await _app.AcquireTokenSilent(Scopes, account)
                             .ExecuteAsync();
        }
        catch (MsalUiRequiredException)
        {
            // 2. 対話が必要なときだけサインイン画面を出す。
            // .NET Framework の既定は旧式の埋め込み WebView のため、
            // http://localhost リダイレクト=システムブラウザーを明示する
            // (.NET 6+ はもともとシステムブラウザーのみ)
            return await _app.AcquireTokenInteractive(Scopes)
                             .WithAccount(account)
                             .WithParentActivityOrWindow(ownerHwnd)
                             .WithUseEmbeddedWebView(false)
                             .ExecuteAsync();
        }
    }
}

呼び出し側でオーナーウィンドウのハンドルを渡します。認証ダイアログがアプリの背面に隠れる事故を防ぐためで、WAM では必須です。5 もう 1 点、WithUseEmbeddedWebView(false) は .NET Framework アプリでは省略しないでください。.NET Framework の対話認証の既定は埋め込み WebView で、http://localhost リダイレクトはシステムブラウザー用のものだからです(組み合わせがずれると、条件付きアクセスや Windows Hello / FIDO が動かない旧式の埋め込みブラウザーに落ちたり、リダイレクト URI 不一致になったりします)。10 .NET 6 以降は埋め込み WebView 自体がなく常にシステムブラウザーなので、この指定は冗長ですが害はありません。

// WinForms (Form のメソッド内)
var result = await _authService.SignInAsync(this.Handle);

// WPF
var hwnd = new System.Windows.Interop.WindowInteropHelper(this).Handle;
var result = await _authService.SignInAsync(hwnd);

this.Text = $"ログイン中: {result.Account.Username}";

押さえておくポイントを補足します。

  • IPublicClientApplication はアプリで 1 インスタンスを使い回します。インスタンスごとにキャッシュを持つため、呼び出しのたびに Create するとサイレント取得が効きません。
  • MsalUiRequiredException は「異常」ではなく「対話が必要」という通常の制御フローです。初回起動、リフレッシュトークンの失効、条件付きアクセスの要求変更などで発生します。
  • API を呼ぶ直前に毎回 AcquireTokenSilent を呼ぶのが正しい使い方です。キャッシュに有効なトークンがあれば即座に返り、期限が近ければ自動更新されます。4 アクセストークンを自前で保持して寿命管理してはいけません。
  • UI スレッドで .Result.Wait() で待つとデッドロックします(「WPF/WinFormsのasyncとUIスレッドを一枚で整理」参照)。

5. WAMブローカー ── Windowsでの推奨構成

4 章のコードはブラウザーを開く構成ですが、Windows ではもう一段良い方法があります。WAM(Web Account Manager)は Windows 10(1703 以降)と Windows Server 2019 以降に組み込まれた認証ブローカーで、公式ドキュメントが挙げる利点は次の 4 つです。5

  • セキュリティ強化: リフレッシュトークンがデバイスにバインドされ、盗み出しても他の端末で使えなくなります(トークン保護)。セキュリティ改善が OS 側の更新で継続的に入ります。
  • 機能サポート: Windows Hello、条件付きアクセス、FIDO キーといった OS・サービス連携の認証機能が、追加コードなしで使えます。
  • システム統合: Windows にサインイン済みのアカウントが組み込みのアカウントピッカーに出るため、多くの場合パスワード入力なしでサインインが完了します。実質的な SSO です。
  • トークン保護: 条件付きアクセスのトークン保護ポリシーに対応できます。

社内 PC が Entra 参加(または Hybrid Join)していれば、「アプリを起動→ Windows のアカウントを選ぶ→即ログイン完了」という体験になり、パスワードをどこにも入力しません。

5.1 実装 ── WithBrokerとパッケージ

WAM を使うには MSAL.NET 4.52.0 以降と、追加パッケージ Microsoft.Identity.Client.Broker が必要です。5 4 章のビルダーに WithBroker を足します。

using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Broker;  // WithBroker(BrokerOptions) 用

var brokerOptions = new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
{
    Title = "在庫管理システム"  // アカウントピッカーに表示されるタイトル
};

_app = PublicClientApplicationBuilder.Create(ClientId)
    .WithAuthority(AzureCloudInstance.AzurePublic, TenantId)
    .WithDefaultRedirectUri()
    .WithParentActivityOrWindow(() => _ownerHwnd)  // WAMでは必須
    .WithBroker(brokerOptions)
    .Build();

サイレント取得側も 1 行強化できます。キャッシュにアカウントがないとき、PublicClientApplication.OperatingSystemAccount を渡すと「今 Windows にサインインしているアカウント」でのサイレントサインインを試せます。初回起動からダイアログなしでログインが決まる、公式推奨のパターンです。8

var accounts = await _app.GetAccountsAsync();
var account = accounts.FirstOrDefault()
              ?? PublicClientApplication.OperatingSystemAccount;
try
{
    return await _app.AcquireTokenSilent(Scopes, account).ExecuteAsync();
}
catch (MsalUiRequiredException)
{
    return await _app.AcquireTokenInteractive(Scopes).ExecuteAsync();
}

あわせて、3.2 節のとおりアプリ登録側に ms-appx-web://microsoft.aad.brokerplugin/{クライアントID} を「モバイル アプリケーションとデスクトップ アプリケーション」プラットフォームで登録しておきます。5 これを忘れるとブローカーエラーで対話認証が失敗します。もう 1 点、上のサンプルの WithDefaultRedirectUri()WAM が使えずブラウザーへフォールバックしたときのリダイレクト URI を決めるもので、解決先がプラットフォーム依存です(.NET Framework → nativeclient、.NET → http://localhost)。9 3.2 節の表の 3 つを登録済みならどちらでも通りますが、登録を絞りたい場合は WithRedirectUri(...) で明示してください。

5.2 WAMの制約 ── 知らないとハマる

制約 内容
OS Windows 10 (1703)+ / Windows Server 2019+。それ以前・Mac・Linux では自動的にブラウザーへフォールバックする5
ID プロバイダー Entra ID 専用。Azure AD B2C・AD FS の authority は非対応(ブラウザーへフォールバック)5
実行コンテキスト 対話的なユーザーセッションで UI を出せることが前提。Windows サービス、タスクスケジューラ(ユーザーセッション外)、runas での別ユーザー実行では設計上エラーになる5

3 行目は特に重要です。「画面ありアプリでは動くのに、同じコードを夜間バッチに流用したら失敗する」のは仕様です。無人実行はユーザー委任ではなくアプリケーション権限(機密クライアント)で設計を分けるべき領域になります。フォールバックが仕様として組み込まれているため、「WAM を第一候補、ダメならブラウザー」をコード 1 本で実現できるのが MSAL の良いところです。

6. トークンキャッシュの永続化 ── 再起動のたびにログイン画面を出さない

MSAL.NET のトークンキャッシュは既定ではメモリ上のみで、デスクトップアプリでは永続化の実装がアプリ側の責任です。永続化しないと、プロセスを再起動するたびに AcquireTokenSilent が失敗して対話サインインになります。4 「導入テストでは良かったのに、毎朝ログイン画面が出ると現場から苦情が来た」という相談の原因はほぼこれです。

公式の推奨は、クロスプラットフォームキャッシュライブラリ Microsoft.Identity.Client.Extensions.Msal(NuGet)を使うことです。6

using Microsoft.Identity.Client.Extensions.Msal;

var storageProperties = new StorageCreationPropertiesBuilder(
        "msal_cache.dat",
        Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "KomuraSoft", "InventoryApp"))
    .Build();

var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties);
cacheHelper.RegisterCache(_app.UserTokenCache);  // Build() 直後に1回登録

Windows ではキャッシュが暗号化されて保存されます。公式ドキュメントに載っている自前実装の例は ProtectedDataDPAPI、DataProtectionScope.CurrentUser)でトークンを暗号化してファイル保存する形で、Extensions.Msal はそれを製品品質に仕上げたライブラリという位置づけです。6 「ユーザー単位の秘密はユーザースコープの DPAPI で守る」原則は、「Windowsアプリの機密情報保存 - DPAPIで平文設定を避ける」で書いた設定ファイルの話と同じです。トークンキャッシュを平文 JSON で保存する自作実装は、接続文字列の平文保存と同じ深刻度で扱ってください。

運用面の注意を 3 点。

  • WAM を使う場合でもキャッシュ永続化は必要です。MSAL は ID トークンとアカウントのメタデータを引き続き自分のキャッシュに保存するためです。8
  • 保存先は %LOCALAPPDATA%\会社名\アプリ名 が基本です。DPAPI の紐付けの関係で別 PC・別ユーザーでは復号できませんが、サイレント取得に失敗して再サインインになるだけで実害はありません。
  • 「ログアウト」は GetAccountsAsync で列挙したアカウントを RemoveAsync で消して実装します。キャッシュファイルの削除は不要です。ただし RemoveAsync が消すのは MSAL のローカルキャッシュだけで、WAM・ブラウザー・Windows サインイン側のセッションはそのまま残ります。次の対話サインインで同じアカウントがサイレントに再サインインされ得るため、共有 PC でアカウントの切り替えを成立させたい場合は、AcquireTokenInteractiveWithPrompt(Prompt.SelectAccount) を付けて必ずアカウント選択画面を出す、あるいは要件次第でテナントのログアウトエンドポイントも併用するなど、「ローカルキャッシュの削除」と「本当のサインアウト」を区別して設計してください。

7. 取得したトークンで何をするか ── 3つの構成

認証が通った後の使い道は 3 パターンに分かれます。どこまでやるかで、必要な設定も変わります。

構成 使うトークン 追加で必要なもの
(1) ログインだけ ID トークン(AuthenticationResult.Account / ClaimsPrincipal なし(User.Read のみ)
(2) Microsoft Graph を呼ぶ Graph 向けアクセストークン 呼びたい API に応じた Graph のアクセス許可と同意
(3) 自社 Web API を守る 自社 API 向けアクセストークン API 側のアプリ登録とスコープ公開、API 側でのトークン検証

7.1 ログインだけの構成 ── 一番小さく始める

「自前のパスワード照合を置き換えたいだけで、クラウドの API は呼ばない」なら、サインイン結果のアカウント情報をアプリ内の権限テーブルと突き合わせるだけで成立します。users テーブルのキーを Entra のオブジェクト ID(姓の変更などで UPN が変わっても不変)に置き換え、パスワード列を削除します。ローカル DB の設計は変えずに認証だけ差し替えられるので、最初の一歩として一番おすすめしやすい構成です。

7.2 Microsoft Graphを呼ぶ

User.Read のアクセストークンをそのまま Microsoft Graph に投げれば、サインインユーザーのプロフィールや写真が取れます。

var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", result.AccessToken);
var me = await http.GetStringAsync("https://graph.microsoft.com/v1.0/me");

予定表・メール送信・Teams 通知などに広げる場合は、対応するアクセス許可(Mail.Send など)を追加して管理者の同意を取り直します。社内アプリからの通知メールを Graph に寄せる設計は「中小企業向けの一斉メール配信を、特定サービスに縛られず設計する方法」で扱った流れとも噛み合います。

7.3 自社Web APIを守る ── audienceとスコープの検証まで

デスクトップアプリが自社の Web API を呼ぶ構成なら、API 側にも別のアプリ登録を作り、api://{APIのクライアントID}/access_as_user のようなスコープを公開して、デスクトップ側はそのスコープでトークンを要求します。API 側で重要なのは、[Authorize] を付けるだけでは不十分だと公式に明記されている点です。11 検証すべきは次の 3 段です。

  1. 署名と発行者: 正しいテナントの Entra ID が発行した JWT か(ASP.NET Core + Microsoft.Identity.Web ならミドルウェアが処理)
  2. audience(aud: トークンの宛先がこの API 自身か。Graph 用のトークンを自社 API に流用させない
  3. スコープ(scp クレーム): 期待するスコープが入っているか。Microsoft.Identity.Web なら [RequiredScope("access_as_user")] 属性で宣言できる11

2 と 3 を省くと「Entra のトークンっぽければ誰でも通る API」ができあがります。「Windowsアプリ開発のセキュリティ最低限チェックリスト」の通信・入力検証の項目とあわせて設計レビューの対象にしてください。

8. 導入判断 ── 完全社内ツールにEntra認証を入れるべきか

すべての社内アプリに入れるべきかというと、そうでもありません。判断表を置きます。

状況 推奨 理由
Microsoft 365 / Entra ID を全社導入済み+アプリにログイン概念がある 導入する 自前パスワード管理の負債が丸ごと消える。実装コストは小さい
アプリが自社 Web API やクラウド資源を呼ぶ 導入する API 保護に認証基盤は必須。自前トークンを発明するより確実
監査要件がある(誰がいつ使ったかの記録、MFA 必須化) 導入する サインインログ・条件付きアクセスがテナント側で一元化される
ログイン概念がない単機能ツール(変換ツール、ビューアー等) 不要 認証を足す動機がない。Windows ログオンで足りている
完全オフライン環境(閉域の製造ライン、持ち出し PC)で動く 不可または要設計 初回サインインとトークン更新にネットワークが必須
Entra ID 未導入(オンプレ AD のみ、Google Workspace のみ) 別解を検討 前者は AD 認証(Windows 統合認証)、後者は Google 側の仕組みが自然

オフライン要件は特に注意してください。AcquireTokenSilent はキャッシュ内のアクセストークンが有効な間(経験則でおおむね 1 時間強)はオフラインでも返せますが、期限が切れれば更新にネットワークが要ります。導入前に、この寿命の前提で業務が回るのかを現場の利用パターンと突き合わせる必要があります。

9. 運用の罠 ── 導入後に来る問い合わせ

導入して終わりではなく、運用フェーズで定番の問い合わせがあります。先回りして書いておきます。

  • 「昨日まで使えたのに突然ログインできない」: 第一容疑者は条件付きアクセスポリシーの変更です。情報システム部門が「未登録デバイスをブロック」などを有効にすると、アプリは何も変えていないのにサインインが失敗し始めます。切り分けは Entra 管理センターのサインインログで該当ユーザーのエラー理由を見るのが最速です。WAM 構成にしておくとポリシー要求への対応力が上がり、この種の摩擦自体が減ります。5
  • 「シークレットの有効期限が切れると通知が来たが、このアプリは大丈夫か」: パブリッククライアントにはシークレットも証明書もそもそも無いので、期限切れもありません。この相談が来たら、機密クライアントのアプリ登録との混同か、誰かがパブリッククライアント用の登録に不要なシークレットを作っています(後者なら消してかまいません)。シークレット期限切れによる停止事故が構造的に起きないのは、この構成の隠れた利点です。
  • 「初回起動で『管理者の承認が必要です』と出る」: 3.3 節の管理者の同意漏れです。アクセス許可を後から追加した場合も、追加分の同意を取り直すまで同じ表示が出ます。
  • 「夜間バッチに組み込んだら動かない」: 5.2 節のとおり WAM は対話セッション前提です。無人処理はユーザー委任トークンの流用ではなく、アプリケーション権限での別設計にします。
  • 配布と更新: MSAL まわりは修正が活発で、ライブラリ更新を全端末へ配りきる仕組みが要ります。「自動アップデートのセキュリティ設計」で書いた更新経路の検証とセットで考えてください。

10. まとめ

WinForms / WPF アプリの Entra ID 認証対応は、次の 6 点に集約されます。

  • 自前のパスワード管理をやめること自体が目的。保管・リセット・退職者対応・監査がテナント側に一元化される
  • デスクトップアプリはパブリッククライアント。シークレットは持てないし、要らない
  • ROPC は廃止方向。ユーザー名パスワードを預かる画面は新規に作らない
  • 実装は MSAL.NET の AcquireTokenSilentAcquireTokenInteractive パターン一択
  • Windows では WAM ブローカー(WithBroker で SSO・条件付きアクセス・Windows Hello 対応
  • トークンキャッシュの永続化(Extensions.Msal / DPAPI 保護)を最初から組み込む

「ログインだけ差し替える」最小構成(7.1 節)なら、既存アプリへの影響はログイン画面とユーザーテーブルの周辺に限定でき、数日規模の改修で収まることが多いです。一方、条件付きアクセスやオフライン要件が絡むと、テナント側の設定と業務の実態を踏まえた設計判断が必要になります。手元のアプリをどの構成まで持っていくべきか、自前認証からの移行手順をどう刻むか、判断に迷う場合はお手伝いできます。

関連記事

関連する相談領域

合同会社小村ソフトでは、既存 WinForms / WPF アプリへの Entra ID 認証の組み込み(アプリ登録の設計、MSAL.NET 実装、自前認証からの移行計画)、自社 Web API のトークン検証の設計レビュー、条件付きアクセス絡みのサインイン障害の切り分けを扱っています。

参考リンク

  1. Microsoft Learn, Desktop app that calls web APIs: Code configuration. デスクトップアプリのリダイレクト URI(モバイルとデスクトップ プラットフォーム、nativeclient / localhost)、「パブリック クライアント フローを許可する」設定の意味について。  2 3

  2. Microsoft Learn, Microsoft identity platform and OAuth 2.0 Resource Owner Password Credentials. ROPC を使うべきでないこと、MFA と非互換でありブロックされること、ROPC 依存アプリが締め出される方向であること、デスクトップアプリはブローカーベース認証へ移行すべきことについて。  2 3 4

  3. Microsoft Learn, Desktop app that calls web APIs: Acquire a token using username and password. ユーザー名パスワードフロー(ROPC)がセキュリティリスクのため廃止(deprecated)とされたこと、移行ガイドの案内、MFA・条件付きアクセス・SSO 非対応の制約について。  2

  4. Microsoft Learn, Get a token from the token cache using MSAL.NET. AcquireTokenSilent を先に呼び MsalUiRequiredException で対話型へフォールバックする推奨パターン、キャッシュとリフレッシュトークンによる自動更新、アカウント削除によるキャッシュのクリアについて。  2 3 4

  5. Microsoft Learn, Using MSAL.NET with Web Account Manager (WAM). ブローカーの利点(セキュリティ強化、Windows Hello・条件付きアクセス・FIDO 対応、アカウントピッカー、トークン保護)、MSAL.NET 4.52.0+ と Microsoft.Identity.Client.Broker パッケージ、WithBroker と親ウィンドウハンドル必須、ms-appx-web リダイレクト URI、対応 OS とフォールバック、対話セッション必須などの制約について。  2 3 4 5 6 7 8 9

  6. Microsoft Learn, Token cache serialization. デスクトップアプリは Microsoft.Identity.Client.Extensions.Msal のクロスプラットフォームキャッシュを使う推奨、MsalCacheHelper の使い方、ProtectedData(DPAPI、CurrentUser スコープ)による自前シリアル化の例について。  2 3

  7. Microsoft Learn, Register an application with the Microsoft identity platform. Entra 管理センターでのアプリ登録手順、サポートされるアカウントの種類の選択、クライアント ID の取得、管理者の同意について。  2

  8. Microsoft Learn, Desktop app that calls web APIs: Acquire a token by using WAM. WAM 利用時もトークンキャッシュの永続化が必要であること、OperatingSystemAccount によるサイレントサインインの推奨パターン、アプリ登録側のリダイレクト URI 設定について。  2 3

  9. Microsoft Learn, Default reply URI. WithDefaultRedirectUri が設定するリダイレクト URI がプラットフォーム依存であること(.NET Framework デスクトップは https://login.microsoftonline.com/common/oauth2/nativeclient、.NET Core は http://localhost)について。  2

  10. Microsoft Learn, Using web browsers (MSAL.NET). フレームワーク別のブラウザー対応表(.NET Framework 4.6.2+ の既定が埋め込み、.NET 6+ はシステムブラウザーのみ)、システムブラウザーには http://localhost リダイレクト URI が必要なこと、WithUseEmbeddedWebView による切り替えについて。 

  11. Microsoft Learn, Protected web API: Verify scopes and app roles. [Authorize] 属性だけでは不十分であり、scp クレーム(スコープ)の検証が必要なこと、Microsoft.Identity.Web の RequiredScope 属性による宣言的な検証について。  2

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る