更新履歴(5件・最終更新 2026年08月02日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- Silent から Interactive へ落ちるときのトークン取得の流れをシーケンス図にし、導入可否の判断表に判定順を示す図を添えました。あわせて第5章に、WAMブローカーとシステムブラウザーで MSAL が受け取るものが違う(ブローカーはトークン取得まで完了して返し、ブラウザーは認可コードを返す)ことを示す図を追加しています。本文の説明は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- サインインするアカウントを`GetAccountsAsync()`の`FirstOrDefault()`で決めていたのを直しました。アカウントを切り替えたあとや共有端末では、キャッシュに複数のアカウントが残ります。列挙順に意味は無いので、これは「たまたま先頭に居た人」を黙って選ぶ実装です。しかもそのアカウントのトークンが生きていれば`AcquireTokenSilent`が成功してピッカーも開かないため、別人のGraphデータを表示・更新しても誰も気づけません。前回選んだアカウントの`HomeAccountId`を保存して照合し、候補を1つに決められないときは対話でユーザーに選んでもらう形にしました。
- 冒頭に略語の表(MSAL、SSO、MFA、FIDO、JWT、ROPC、WAM、UPN)と対象読者・前提環境の表を追加しました。ブローカーを優先しつつブラウザーへ切り替える統合例を新設し、`WithRedirectUri`と`WithDefaultRedirectUri()`の使い分けを表にしました。管理センターでの作業を「開く画面名と、その画面で押すもの」の早見表にしています。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589927)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「WinForms/WPFアプリにEntra ID認証を組み込む ── MSAL.NETとWAMブローカーの実務構成」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589927 https://staging.comcomponent.com/blog/winforms-wpf-entra-id-auth/
- DOI(最新版)
- 10.5281/zenodo.21589927
- DOI(この版)
- 10.5281/zenodo.21732918
「社内の業務アプリごとにログイン画面を作って、パスワードを自前のデータベースで管理している。退職者が出るたびにアプリごとにアカウントを止めて回るのがつらい」「Microsoft 365 は全社で入れているので、そのアカウントでそのままログインさせられないか」。デスクトップアプリの改修相談で、ここ数年着実に増えているテーマです。パスワードの漏えい事故やゼロトラスト対応の文脈で情報システム部門から「自前のパスワード管理をやめてほしい」と言われた、という形で来ることもあります。
結論から言えば、Microsoft 365 を使っている組織なら、WinForms / WPF の社内アプリのログインを Entra ID(旧 Azure AD) に寄せるのは筋の良い投資です。アプリはパスワードを一切預からなくなり、多要素認証(MFA)・条件付きアクセス・サインインログといったテナント側の防御と監査がそのまま社内アプリにも効くようになります。実装も、MSAL.NET というライブラリと数十行のコードで済む範囲です。
ただし、デスクトップアプリならではの落とし穴がいくつかあります。「ユーザー名とパスワードをテキストボックスで受けて裏で認証する」昔ながらの作り(ROPC)は公式に廃止の方向で、新規に採用してはいけません。トークンキャッシュを永続化しないと起動のたびにサインイン画面が出ますし、Windows ではブローカー(WAM)を使うかどうかで体験とセキュリティが大きく変わります。この記事では、概念の最小限の整理から、アプリ登録、MSAL.NET の実装、WAM、キャッシュ、導入判断、運用の罠までを一通りまとめます。
この記事の対象読者と前提環境
| 項目 | 内容 |
|---|---|
| 対象読者 | 既存または新規の WinForms / WPF 業務アプリに、Microsoft 365 のアカウントでのログインを組み込む立場の開発者 |
| テナント側の前提 | Microsoft 365 / Entra ID を導入済みであること。アプリ登録と管理者の同意(3 章)はテナント側の作業なので、自分で操作できない場合は 3 章をそのまま情報システム部門への依頼書にしてください |
| 実行環境 | Windows。WAM ブローカー(5 章)を使うなら Windows 10(1703)以降 / Windows Server 2019 以降。それ以前や Mac・Linux ではブラウザーへ自動フォールバックします1 |
| .NET | .NET Framework 4.6.2 以降、または .NET 6 以降。対話認証で使われるブラウザーの既定がフレームワークで異なります(4 章)2 |
| NuGet パッケージ | Microsoft.Identity.Client(必須)、Microsoft.Identity.Client.Broker(WAM 用。MSAL.NET 4.52.0 以降)1、Microsoft.Identity.Client.Extensions.Msal(トークンキャッシュの永続化)3 |
| ネットワーク | 初回サインインとトークン更新に Entra ID への到達性が必要です。完全オフライン環境では成立しません(8 章) |
この記事で使う略語
| 略語 | 正式名 | この記事での意味 |
|---|---|---|
| MSAL | Microsoft Authentication Library | Microsoft が提供する認証ライブラリ。.NET 版が MSAL.NET(NuGet の Microsoft.Identity.Client) |
| SSO | Single Sign-On(シングルサインオン) | 一度サインインすれば、別のアプリでサインインし直さずに済む状態 |
| MFA | Multi-Factor Authentication(多要素認証) | パスワードに加えて、スマートフォンのアプリや生体認証など別の要素も要求する認証 |
| FIDO | Fast IDentity Online | パスワードを使わない認証の標準規格。この記事では USB 型などのセキュリティキーを指します |
| JWT | JSON Web Token | 署名付きの JSON でクレーム(利用者の属性)を運ぶトークン形式。ID トークン・アクセストークンの実体 |
| ROPC | Resource Owner Password Credentials | ユーザー名とパスワードをアプリが直接受け取って認証するフロー。廃止方向(2.3 節) |
| WAM | Web Account Manager | Windows に組み込まれた認証ブローカー(5 章) |
| UPN | User Principal Name | taro@example.co.jp の形式のサインイン名。姓の変更などで変わり得ます(7.1 節) |
1. まず結論
- ID とパスワードの自前管理をやめて Entra ID に寄せると、パスワード保管・リセット対応・退職者のアカウント停止・サインイン監査が全部テナント側の仕事になります。アプリ側の責任範囲が劇的に小さくなるのが最大の利点です。
- デスクトップアプリは パブリッククライアントです。exe は配布先で解析できるため、クライアントシークレットを持てません(持たせてはいけません)。アプリ登録もパブリッククライアントとして構成します。4
- ユーザー名とパスワードをアプリが直接預かる ROPC(Resource Owner Password Credentials)は公式に「廃止(deprecated)」と明記され、移行ガイドが出ています。MFA・条件付きアクセスと両立せず、実質的に使えなくなる方向です。新規採用は禁止と考えてください。56
- 実装は MSAL.NET(Microsoft.Identity.Client) で、まず
AcquireTokenSilent、MsalUiRequiredExceptionが出たらAcquireTokenInteractiveという呼び出しパターンが唯一の基本形です。7 - Windows では WAM(Web Account Manager)ブローカー経由の認証が推奨です。Windows サインイン済みアカウントとの SSO、条件付きアクセス・Windows Hello・FIDO キー対応、リフレッシュトークンのデバイスバインドが、
WithBrokerの 1 行で手に入ります。1 - トークンキャッシュの永続化を忘れると、アプリを再起動するたびにサインイン画面が出ます。
Microsoft.Identity.Client.Extensions.Msalの暗号化キャッシュを最初から組み込んでください。3 - Entra 認証はネットワーク前提の仕組みです。完全オフラインで動く必要がある現場アプリでは成立しないので、8 章の判断表で導入可否を先に見極めてください。
この記事の知識マップ
WinForms/WPFの社内アプリにEntra ID認証を組み込む記事で、MSAL.NETがデスクトップアプリをクライアントシークレットを持たないパブリッククライアントとして扱い、AcquireTokenSilentからAcquireTokenInteractiveへフォールバックする呼び出しパターンを基本形とすることを示す。ユーザー名とパスワードを直接扱うROPCはMFA・条件付きアクセスと非両立で公式に非推奨とされ、Windows環境ではWindows HelloやFIDOキー連携が効くWAMブローカーが推奨される一方、タスクスケジューラなど対話セッション外の無人実行とは両立しない。トークンキャッシュを永続化しないと再起動のたびに対話認証へ戻ってしまい、複数アカウントがキャッシュに残る場合はHomeAccountIdでの照合が、列挙順に依存した誤ったサイレント認証によるアカウント取り違えを防ぐ。
flowchart LR
accTitle: WinForms/WPFのEntra ID認証の知識マップ
accDescr: MSAL.NETがデスクトップアプリをパブリッククライアントとして扱いSilentからInteractiveへフォールバックすること、ROPCの非推奨、WAMブローカーの利点と対話セッション前提の制約、トークンキャッシュ永続化とアカウント誤選択対策の関係を示す図
entra_id["Microsoft Entra ID"]
msal_dotnet["MSAL.NET"]
wam_broker["WAM(Web Account Manager)ブローカー"]
windows_forms["Windows Forms"]
wpf["WPF"]
public_client_application["パブリッククライアント"]
app_registration_entra["Entra IDのアプリ登録"]
redirect_uri["リダイレクトURI"]
admin_consent["管理者の同意"]
ropc["ROPC(Resource Owner Password Credentials)"]
conditional_access["条件付きアクセス"]
acquiretokensilent_pattern["Silent→Interactiveパターン"]
token_cache_persistence["トークンキャッシュの永続化"]
dpapi["DPAPI"]
windows_hello["Windows Hello"]
fido2["FIDO2"]
task_scheduler["タスクスケジューラの無人実行"]
windows_service["Windowsサービス"]
id_token["IDトークン"]
access_token["アクセストークン"]
microsoft_graph["Microsoft Graph"]
home_account_id["HomeAccountId"]
silent_account_misselection["サイレント認証でのアカウント誤選択"]
windows_forms -.->|"利用する"| msal_dotnet
wpf -.->|"利用する"| msal_dotnet
msal_dotnet -->|"実装を担う"| public_client_application
msal_dotnet -.->|"前提とする"| entra_id
public_client_application -->|"前提とする"| app_registration_entra
app_registration_entra -->|"で構成できる"| redirect_uri
app_registration_entra -.->|"前提とする"| admin_consent
public_client_application -.->|"で構成できる"| app_registration_entra
ropc -->|"用いるのは非推奨"| public_client_application
ropc -->|"両立しない"| conditional_access
msal_dotnet -->|"実装を担う"| acquiretokensilent_pattern
acquiretokensilent_pattern -.->|"前提とする"| token_cache_persistence
token_cache_persistence -.->|"利用する"| dpapi
wam_broker -.->|"利用する"| windows_hello
wam_broker -.->|"利用する"| fido2
wam_broker -->|"推奨される対応"| entra_id
wam_broker -.->|"前提とする"| token_cache_persistence
wam_broker -->|"前提とする"| redirect_uri
wam_broker -.->|"両立しない"| task_scheduler
wam_broker -->|"両立しない"| windows_service
msal_dotnet -.->|"利用する"| id_token
msal_dotnet -.->|"利用する"| access_token
microsoft_graph -->|"前提とする"| access_token
home_account_id -->|"軽減する"| silent_account_misselection
acquiretokensilent_pattern -.->|"原因になり得る"| silent_account_misselection
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全25件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
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(JSON Web Token)。ログイン機能だけ欲しい場合はこれで足りる |
| アクセストークン | 特定の API(Microsoft Graph や自社 Web API)を呼ぶための通行証。宛先(audience)とスコープが焼き込まれている |
| リフレッシュトークン | 上記 2 つを対話なしで更新するためのトークン。MSAL がキャッシュ内で自動管理し、アプリから直接は見えない |
重要なのは 1 行目です。exe は配布先で解析・逆コンパイルできるため、埋め込んだ「秘密」は秘密になりません。だからシークレットなしで動くパブリッククライアントとして登録し、認証そのもの(パスワード入力や MFA)はブラウザまたは OS のブローカーに委ね、アプリはトークンだけを受け取る設計になります。アプリがユーザーのパスワードに触れないこと自体が、この仕組みの根幹です。
2.3 ROPCは終わった方式 ── 裏取りの結果
昔ながらの発想だと「自前のログイン画面でユーザー名とパスワードを受けて、裏で Entra ID に検証してもらえばいい」となりがちです。これが ROPC(ユーザー名パスワード直渡し)で、MSAL.NET にも AcquireTokenByUsernamePassword として残ってはいますが、公式ドキュメントの現在の記述は明確です。
- パブリッククライアント向けの ROPC は「セキュリティリスクのため廃止(deprecated)された」と明記され、より安全なフローへの移行ガイドが公開されています。6
- ROPC は MFA・条件付きアクセスと非互換です。テナントで MFA が必須化されているユーザーは、このフローではブロックされてサインインできません。5
- SSO が効かず、個人 Microsoft アカウントも使えず、パスワードレス(FIDO、Authenticator)のアカウントもサインインできません。5
- Microsoft の Web API 側でも MFA 済みトークンしか受け付けない動きが進んでおり、公式ドキュメント自身が「ROPC に依存するアプリは締め出される(locked out)。デスクトップアプリはブローカーベースの認証へ移行せよ」と書いています。5
MFA の必須化はテナント側の設定でいつでも起こるので、「今は動いているから」で ROPC を採用すると、ある日突然全ユーザーがログインできなくなります。既存アプリが ROPC で動いている場合も移行を前提に計画を立ててください。デスクトップアプリで使ってよい取得方法は実質次の 2 つです。
| フロー | 使いどころ |
|---|---|
| 対話型(ブローカー / ブラウザ) | 通常の GUI アプリ。本命 |
| デバイスコードフロー | ブラウザを表示できない環境(SSH 先のコンソールなど)。URL とコードを表示し、別デバイスのブラウザでサインインしてもらう |
3. アプリ登録 ── Entra管理センターでの設定
コードを書く前に、テナントにアプリを登録します。開発者が自分でできない場合は、この節の内容をそのまま情報システム部門への依頼書にしてください。
管理センターの画面構成は改定が入ることがあるので、この記事ではメニューの見た目ではなくたどり着く画面の名前と、その画面で押すもので書きます。全体の作業を先に一覧にしておきます。
| 目的 | 開く画面 | その画面での操作 |
|---|---|---|
| アプリを登録する | 「アプリの登録」 | 「新規登録」→ 名前と「サポートされているアカウントの種類」を指定(3.1 節) |
| ID を控える | 登録したアプリの「概要」 | 「アプリケーション (クライアント) ID」と「ディレクトリ (テナント) ID」をコピー |
| リダイレクト URI を足す | 登録したアプリの「認証」 | 「プラットフォームを追加」→「モバイル アプリケーションとデスクトップ アプリケーション」→ URI を入力(3.2 節の表の 3 つ) |
| アクセス許可を足す | 登録したアプリの「API のアクセス許可」 | 「アクセス許可の追加」→「Microsoft Graph」→「委任されたアクセス許可」→ User.Read(3.3 節) |
| 管理者の同意を与える | 登録したアプリの「API のアクセス許可」 | 「(テナント名)に管理者の同意を与えます」を実行(3.3 節) |
| 公開クライアントの許可 | 登録したアプリの「認証」 | 「詳細設定」の「パブリック クライアント フローを許可する」(3.4 節) |
3.1 登録本体
Microsoft Entra 管理センター(entra.microsoft.com)の [アプリの登録] → [新規登録] で作成します。8
- 名前: 同意画面やサインインログに表示されるので、「在庫管理システム」など業務側に通じる名前にします。
- サポートされているアカウントの種類: 社内アプリなら「この組織ディレクトリのみのアカウント」(シングルテナント)一択です。マルチテナントは複数組織へ配布する製品の場合だけです。
- 登録後に表示されるアプリケーション (クライアント) ID とディレクトリ (テナント) ID を控え、アプリの設定に埋め込みます(どちらも秘密情報ではありません)。
3.2 リダイレクトURI ── プラットフォームは「モバイルアプリケーションとデスクトップアプリケーション」
認証後にトークンを受け取る場所の宣言です。[認証] → [プラットフォームを追加] → [モバイル アプリケーションとデスクトップ アプリケーション] を選び、認証方式に応じた URI を登録します。4
| 認証方式 | 登録するリダイレクトURI |
|---|---|
| WAM ブローカー(本命、5 章) | ms-appx-web://microsoft.aad.brokerplugin/{クライアントID} |
| システムブラウザー | http://localhost |
| 埋め込みブラウザー | https://login.microsoftonline.com/common/oauth2/nativeclient |
WAM 用の ms-appx-web://... は MSAL 側のコードには書きませんが、アプリ登録側には必須です。9 WAM が使えない環境でのブラウザーフォールバック(5 章)を考えると、表の 3 つを最初から全部登録しておくのが実務的です。特に注意したいのが WithDefaultRedirectUri() の挙動で、解決先はプラットフォーム依存です。.NET Framework では https://login.microsoftonline.com/common/oauth2/nativeclient、.NET(Core 以降)では http://localhost に解決されます。10 ms-appx-web と http://localhost しか登録していない .NET Framework アプリが WAM からブラウザーへフォールバックすると、nativeclient との不一致で認証エラーになります。3 つとも登録するか、WithRedirectUri(...) で明示的に固定してください。「Web」プラットフォームの方へ登録してしまい認証エラーになるのも定番のつまずきです。
3.3 APIのアクセス許可と管理者の同意
[API のアクセス許可] で、アプリが呼ぶ API の委任されたアクセス許可(delegated permission)を追加します。ログインとプロフィール表示だけなら、既定で付与されている Microsoft Graph の User.Read で足ります。
追加したら [(テナント名)に管理者の同意を与えます] を実行してもらいます。8 これで初回サインイン時のユーザーごとの同意ダイアログが出なくなります。ユーザー自身の同意が無効化されたテナントでは、管理者の同意なしだと初回サインインが「管理者の承認が必要です」で止まるため、社内配布アプリでは配布前に管理者の同意まで済ませておくのが原則です。
3.4 「パブリック クライアント フローを許可する」フラグ
[認証] の詳細設定にある「パブリック クライアント フローを許可する」は、デバイスコードフローや統合 Windows 認証のようにリダイレクト URI を使わないフローを使う場合に「はい」にします。4 対話型(ブラウザー / ブローカー)だけなら必須ではありません。なお、このアプリ登録にはクライアントシークレットも証明書も作りません。「証明書とシークレット」欄が空なのがパブリッククライアントの正しい状態です(混同の相談が多いので 9 章で再度触れます)。
4. MSAL.NETでの実装 ── Silent→Interactiveの基本形
先に、この基本形で誰が何をするのかを図にしておきます。アプリはパスワードを一度も受け取らず、トークンだけを受け取るという点が読み取れれば十分です。
sequenceDiagram
participant APP as デスクトップアプリ
participant MSAL as MSAL.NET(トークンキャッシュ)
participant UI as ブローカー / ブラウザー
participant EID as Entra ID
APP->>MSAL: AcquireTokenSilent
alt キャッシュに使えるトークンがある
MSAL-->>APP: アクセストークン(画面は出ない)
else 無い、または更新できない
MSAL-->>APP: MsalUiRequiredException
APP->>MSAL: AcquireTokenInteractive
MSAL->>UI: 対話の出し先は構成で決まる(5章)
UI->>EID: サインイン(MFA / Windows Hello / FIDO)
EID-->>UI: 認証の結果
UI-->>MSAL: 結果を返す(返るものは経路で違う・図2)
MSAL-->>APP: アクセストークン
end
図1: パスワードの入力はブローカーまたはブラウザーの中で完結し、アプリに渡るのはトークンだけ。だからアプリ側にシークレットが要らない。受け取ったトークンで API を呼ぶところから先は第7章
NuGet で Microsoft.Identity.Client を追加します。実装パターンは 1 つだけ覚えれば足ります。必ず AcquireTokenSilent を先に呼び、MsalUiRequiredException を受けたときだけ対話型にフォールバックします。AcquireTokenInteractive はキャッシュを一切見ない設計なので、いきなり呼ぶと毎回サインイン画面が出ます。7
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 では必須です。1 もう 1 点、WithUseEmbeddedWebView(false) は .NET Framework アプリでは省略しないでください。.NET Framework の対話認証の既定は埋め込み WebView で、http://localhost リダイレクトはシステムブラウザー用のものだからです(組み合わせがずれると、条件付きアクセスや Windows Hello / FIDO が動かない旧式の埋め込みブラウザーに落ちたり、リダイレクト URI 不一致になったりします)。2 .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を呼ぶのが正しい使い方です。キャッシュに有効なトークンがあれば即座に返り、期限が近ければ自動更新されます。7 アクセストークンを自前で保持して寿命管理してはいけません。 - UI スレッドで
.Resultや.Wait()で待つとデッドロックします(「WPF/WinFormsのasyncとUIスレッドを一枚で整理」参照)。
5. WAMブローカー ── Windowsでの推奨構成
4 章のコードはブラウザーを開く構成ですが、Windows ではもう一段良い方法があります。WAM(Web Account Manager)は Windows 10(1703 以降)と Windows Server 2019 以降に組み込まれた認証ブローカーで、公式ドキュメントが挙げる利点は次の 4 つです。1
- セキュリティ強化: リフレッシュトークンがデバイスにバインドされ、盗み出しても他の端末で使えなくなります(トークン保護)。セキュリティ改善が OS 側の更新で継続的に入ります。
- 機能サポート: Windows Hello、条件付きアクセス、FIDO キーといった OS・サービス連携の認証機能が、追加コードなしで使えます。
- システム統合: Windows にサインイン済みのアカウントが組み込みのアカウントピッカーに出るため、多くの場合パスワード入力なしでサインインが完了します。実質的な SSO です。
- トークン保護: 条件付きアクセスのトークン保護ポリシーに対応できます。
社内 PC が Entra 参加(または Hybrid Join)していれば、「アプリを起動→ Windows のアカウントを選ぶ→即ログイン完了」という体験になり、パスワードをどこにも入力しません。
図1で「返るものは経路で違う」と書いた部分が、ここでの差です。ブローカーはブラウザーの代わりではなく、トークンの取得まで自分で終わらせて結果を返す主体です。
flowchart TB
A["AcquireTokenInteractive"] --> Q{"WithBroker が有効で<br/>WAM が使える環境か"}
Q -->|"使える"| BR["WAM ブローカー<br/>ブローカー側でトークン取得まで完了し、<br/>MSAL へトークンを返す"]
Q -->|"使えない / 非対応環境(4章)"| BW["システムブラウザー<br/>認可コードを MSAL へ返し、<br/>MSAL が Entra ID でトークンに交換する"]
BR --> T["MSAL がキャッシュに入れてアプリへ返す"]
BW --> T
図2: 対話の出し先が変わると、MSAL が受け取るものも変わる。ブローカー経由では認可コードの交換が MSAL 側に残らない
5.1 実装 ── WithBrokerとパッケージ
WAM を使うには MSAL.NET 4.52.0 以降と、追加パッケージ Microsoft.Identity.Client.Broker が必要です。1 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 にサインインしているアカウント」でのサイレントサインインを試せます。初回起動からダイアログなしでログインが決まる、公式推奨のパターンです。9
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} を「モバイル アプリケーションとデスクトップ アプリケーション」プラットフォームで登録しておきます。1 これを忘れるとブローカーエラーで対話認証が失敗します。もう 1 点、上のサンプルの WithDefaultRedirectUri() は WAM が使えずブラウザーへフォールバックしたときのリダイレクト URI を決めるもので、解決先がプラットフォーム依存です(.NET Framework → nativeclient、.NET → http://localhost)。10 3.2 節の表の 3 つを登録済みならどちらでも通りますが、登録を絞りたい場合は WithRedirectUri(...) で明示してください。
5.2 WAMの制約 ── 知らないとハマる
| 制約 | 内容 |
|---|---|
| OS | Windows 10 (1703)+ / Windows Server 2019+。それ以前・Mac・Linux では自動的にブラウザーへフォールバックする1 |
| ID プロバイダー | Entra ID 専用。Azure AD B2C・AD FS の authority は非対応(ブラウザーへフォールバック)1 |
| 実行コンテキスト | 対話的なユーザーセッションで UI を出せることが前提。Windows サービス、タスクスケジューラ(ユーザーセッション外)、runas での別ユーザー実行では設計上エラーになる1 |
3 行目は特に重要です。「画面ありアプリでは動くのに、同じコードを夜間バッチに流用したら失敗する」のは仕様です。無人実行はユーザー委任ではなくアプリケーション権限(機密クライアント)で設計を分けるべき領域になります。フォールバックが仕様として組み込まれているため、「WAM を第一候補、ダメならブラウザー」をコード 1 本で実現できるのが MSAL の良いところです。
5.3 最終形 ── ブローカー優先+ブラウザーフォールバックの統合例
4 章はブラウザーだけの構成、5.1 節はブローカーの追加分だけを示したので、実務でそのまま使える形に合流させます。先に、混乱しやすいリダイレクト URI の指定方法を整理します。
| 書き方 | 実際に使われるリダイレクト URI | 使いどころ |
|---|---|---|
WithRedirectUri("http://localhost") |
常に http://localhost(システムブラウザー用) |
登録済みの 1 つに固定したいとき。.NET 6 以降はこれで一致します |
WithRedirectUri("https://login.microsoftonline.com/common/oauth2/nativeclient") |
常に nativeclient | .NET Framework で埋め込み WebView を使う構成のとき |
WithDefaultRedirectUri() |
プラットフォーム依存(.NET Framework → nativeclient、.NET → http://localhost)10 |
3.2 節の表の 3 つをすべて登録済みで、フレームワークを問わず同じコードにしたいとき |
どちらを選んでも、WAM が使えている間はこの URI は登場しません。効いてくるのはブラウザーへフォールバックしたときだけです。以下の統合例は、登録を絞っても事故らないよう WithRedirectUri で明示的に固定しています。
using System.IO;
using System.Linq;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Broker; // WithBroker(BrokerOptions) 用
using Microsoft.Identity.Client.Extensions.Msal; // MsalCacheHelper 用
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;
private readonly IntPtr _ownerHwnd;
private AuthService(IPublicClientApplication app, IntPtr ownerHwnd)
{
_app = app;
_ownerHwnd = ownerHwnd;
}
// キャッシュ登録が非同期なのでファクトリーメソッドにする。
// アプリで1インスタンスだけ作って使い回す(4章)
public static async Task<AuthService> CreateAsync(IntPtr ownerHwnd)
{
var brokerOptions = new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
{
Title = "在庫管理システム" // アカウントピッカーに出るタイトル
};
var app = PublicClientApplicationBuilder.Create(ClientId)
.WithAuthority(AzureCloudInstance.AzurePublic, TenantId)
// WAMが使えずブラウザーへ落ちたときのリダイレクトURI。
// 解決先がプラットフォーム依存になる WithDefaultRedirectUri() ではなく、
// アプリ登録済みの値へ明示的に固定する
.WithRedirectUri("http://localhost")
.WithParentActivityOrWindow(() => ownerHwnd) // WAMでは必須
.WithBroker(brokerOptions) // 使えなければ自動でブラウザーへ
.Build();
// トークンキャッシュの永続化(6章)。Build() 直後に1回だけ登録する
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);
return new AuthService(app, ownerHwnd);
}
// 前回サインインしたアカウントの識別子。アプリの設定に保存し、
// 起動時に読み込んでおく(保存先はアプリ設定・レジストリなど何でもよい)
private string? _homeAccountId;
// APIを呼ぶ直前に毎回これを呼ぶ。アクセストークンを自前で保持しない(4章)
public async Task<AuthenticationResult> AcquireTokenAsync()
{
// 1. どのアカウントで silent を試すかを決める
IAccount? account = await ResolveAccountAsync();
if (account is not null)
{
try
{
return await _app.AcquireTokenSilent(Scopes, account).ExecuteAsync();
}
catch (MsalUiRequiredException)
{
// 対話へ落とす
}
}
// 2. 対話。WAMが使えればアカウントピッカー、
// 使えなければ上で指定したシステムブラウザーが開く
AuthenticationResult result =
await _app.AcquireTokenInteractive(Scopes)
.WithParentActivityOrWindow(_ownerHwnd)
.WithUseEmbeddedWebView(false) // .NET Framework用(4章)
.ExecuteAsync();
// 選ばれた人を覚える。次回はこの人で silent を試す
_homeAccountId = result.Account?.HomeAccountId?.Identifier;
SaveHomeAccountId(_homeAccountId);
return result;
}
private async Task<IAccount?> ResolveAccountAsync()
{
List<IAccount> accounts = (await _app.GetAccountsAsync()).ToList();
// 前回選んだ人がキャッシュに残っていれば、その人
if (_homeAccountId is not null)
{
IAccount? saved = accounts.FirstOrDefault(
a => a.HomeAccountId?.Identifier == _homeAccountId);
if (saved is not null) { return saved; }
}
// 候補が1人だけなら、それを使ってよい
if (accounts.Count == 1) { return accounts[0]; }
// 0人、または複数居て決め手がない。
// ここで FirstOrDefault を使わないこと(下記)
return null;
}
}
GetAccountsAsync() の結果を FirstOrDefault() で受けないでください。キャッシュに入っているアカウントは1つとは限りません。別のアカウントへ切り替えた後、共有端末で複数人が使った後、テナントをまたいで検証した後 ── どれでも複数残ります。列挙順に意味は無いので、FirstOrDefault() は「たまたま先頭に居た人」を黙って選びます。
これが厄介なのは、選び間違えても画面に何も出ないことです。そのアカウントのトークンがキャッシュに生きていれば AcquireTokenSilent は成功し、アカウントピッカーは開きません。利用者は自分のつもりで操作し、実際には別人の Graph データを表示・更新します。誰も気づけません。
なので、上のコードでは次の順に決めています。
| 状況 | どうするか |
|---|---|
| 前回選んだ人がキャッシュに残っている | その人で silent を試す |
| 候補が1人だけ | その人で silent を試す |
| 0人、または複数居て決め手がない | silent を試さず、対話でユーザーに選んでもらう |
HomeAccountId.Identifier は「どのテナントのどのユーザーか」を表す文字列なので、これを保存しておけば次回も同じ人で入れます(トークンではないので、機微情報として扱う必要はありません)。Windows のサインイン中アカウントを既定にしたい構成なら、最後の行を PublicClientApplication.OperatingSystemAccount にする設計もありえます。避けたいのは「順番で決める」ことだけです。
呼び出し側は次のようになります。CreateAsync はアプリ起動時に 1 回だけ実行し、戻り値を保持してください。
// フォームのフィールドとして持つ(アプリで1インスタンス)
private AuthService _authService;
// ボタンのClickハンドラーなどから。WPFは4章と同じくWindowInteropHelperでHWNDを取る
private async Task SignInAsync()
{
_authService ??= await AuthService.CreateAsync(this.Handle);
var result = await _authService.AcquireTokenAsync();
this.Text = $"ログイン中: {result.Account.Username}";
}
この 1 つで、WAM が使える端末では Windows アカウントを選ぶだけ、使えない端末ではシステムブラウザー、再起動後はキャッシュからのサイレント取得、という 3 つの経路がすべて成立します。あとはアプリ登録側に 3.2 節の表の URI(少なくとも ms-appx-web://... と http://localhost)が入っていることを確認するだけです。
6. トークンキャッシュの永続化 ── 再起動のたびにログイン画面を出さない
MSAL.NET のトークンキャッシュは既定ではメモリ上のみで、デスクトップアプリでは永続化の実装がアプリ側の責任です。永続化しないと、プロセスを再起動するたびに AcquireTokenSilent が失敗して対話サインインになります。7 「導入テストでは良かったのに、毎朝ログイン画面が出ると現場から苦情が来た」という相談の原因はほぼこれです。
公式の推奨は、クロスプラットフォームキャッシュライブラリ Microsoft.Identity.Client.Extensions.Msal(NuGet)を使うことです。3
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 ではキャッシュが暗号化されて保存されます。公式ドキュメントに載っている自前実装の例は ProtectedData(DPAPI、DataProtectionScope.CurrentUser)でトークンを暗号化してファイル保存する形で、Extensions.Msal はそれを製品品質に仕上げたライブラリという位置づけです。3 「ユーザー単位の秘密はユーザースコープの DPAPI で守る」原則は、「Windowsアプリの機密情報保存 - DPAPIで平文設定を避ける」で書いた設定ファイルの話と同じです。トークンキャッシュを平文 JSON で保存する自作実装は、接続文字列の平文保存と同じ深刻度で扱ってください。
運用面の注意を 3 点。
- WAM を使う場合でもキャッシュ永続化は必要です。MSAL は ID トークンとアカウントのメタデータを引き続き自分のキャッシュに保存するためです。9
- 保存先は
%LOCALAPPDATA%\会社名\アプリ名が基本です。DPAPI の紐付けの関係で別 PC・別ユーザーでは復号できませんが、サイレント取得に失敗して再サインインになるだけで実害はありません。 - 「ログアウト」は
GetAccountsAsyncで列挙したアカウントをRemoveAsyncで消して実装します。キャッシュファイルの削除は不要です。ただしRemoveAsyncが消すのは MSAL のローカルキャッシュだけで、WAM・ブラウザー・Windows サインイン側のセッションはそのまま残ります。次の対話サインインで同じアカウントがサイレントに再サインインされ得るため、共有 PC でアカウントの切り替えを成立させたい場合は、AcquireTokenInteractiveにWithPrompt(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 段です。
- 署名と発行者: 正しいテナントの Entra ID が発行した JWT か(ASP.NET Core + Microsoft.Identity.Web ならミドルウェアが処理)
- audience(
aud): トークンの宛先がこの API 自身か。Graph 用のトークンを自社 API に流用させない - スコープ(
scpクレーム): 期待するスコープが入っているか。Microsoft.Identity.Web なら[RequiredScope("access_as_user")]属性で宣言できる11
2 と 3 を省くと「Entra のトークンっぽければ誰でも通る API」ができあがります。「Windowsアプリ開発のセキュリティ最低限チェックリスト」の通信・入力検証の項目とあわせて設計レビューの対象にしてください。
8. 導入判断 ── 完全社内ツールにEntra認証を入れるべきか
すべての社内アプリに入れるべきかというと、そうでもありません。まず判定の順序を図にします。ネットワークと ID 基盤という 2 つの前提が先に来て、そのあとでアプリ側の事情を見ます。
flowchart TD
Q1{"アプリが動く環境から<br/>Entra ID に到達できるか"}
Q1 -->|"完全オフラインの区画がある"| NG1["不可、または要設計<br/>トークン更新にネットワークが要る"]
Q1 -->|"到達できる"| Q2{"組織の ID 基盤は何か"}
Q2 -->|"オンプレ AD のみ"| ALT1["Windows 統合認証を検討する"]
Q2 -->|"Google Workspace のみ"| ALT2["Google 側の仕組みを検討する"]
Q2 -->|"Microsoft 365 / Entra ID"| Q3{"アプリにログインの概念、<br/>API 呼び出し、監査要件のどれかがあるか"}
Q3 -->|"ない(単機能の変換ツールなど)"| NO["導入しない<br/>Windows ログオンで足りている"]
Q3 -->|"ある"| YES["導入する<br/>自前のパスワード管理を丸ごと手放せる"]
図3: 判断は上から順。オフライン要件と ID 基盤で先に絞ってから、アプリ側の必要性を見る
| 状況 | 推奨 | 理由 |
|---|---|---|
| Microsoft 365 / Entra ID を全社導入済み+アプリにログイン概念がある | 導入する | 自前パスワード管理の負債が丸ごと消える。実装コストは小さい |
| アプリが自社 Web API やクラウド資源を呼ぶ | 導入する | API 保護に認証基盤は必須。自前トークンを発明するより確実 |
| 監査要件がある(誰がいつ使ったかの記録、MFA 必須化) | 導入する | サインインログ・条件付きアクセスがテナント側で一元化される |
| ログイン概念がない単機能ツール(変換ツール、ビューアー等) | 不要 | 認証を足す動機がない。Windows ログオンで足りている |
| 完全オフライン環境(閉域の製造ライン、持ち出し PC)で動く | 不可または要設計 | 初回サインインとトークン更新にネットワークが必須 |
| Entra ID 未導入(オンプレ AD のみ、Google Workspace のみ) | 別解を検討 | 前者は AD 認証(Windows 統合認証)、後者は Google 側の仕組みが自然 |
オフライン要件は特に注意してください。AcquireTokenSilent はキャッシュ内のアクセストークンが有効な間(経験則でおおむね 1 時間強)はオフラインでも返せますが、期限が切れれば更新にネットワークが要ります。導入前に、この寿命の前提で業務が回るのかを現場の利用パターンと突き合わせる必要があります。
9. 運用の罠 ── 導入後に来る問い合わせ
導入して終わりではなく、運用フェーズで定番の問い合わせがあります。先回りして書いておきます。
- 「昨日まで使えたのに突然ログインできない」: 第一容疑者は条件付きアクセスポリシーの変更です。情報システム部門が「未登録デバイスをブロック」などを有効にすると、アプリは何も変えていないのにサインインが失敗し始めます。切り分けは Entra 管理センターのサインインログで該当ユーザーのエラー理由を見るのが最速です。WAM 構成にしておくとポリシー要求への対応力が上がり、この種の摩擦自体が減ります。1
- 「シークレットの有効期限が切れると通知が来たが、このアプリは大丈夫か」: パブリッククライアントにはシークレットも証明書もそもそも無いので、期限切れもありません。この相談が来たら、機密クライアントのアプリ登録との混同か、誰かがパブリッククライアント用の登録に不要なシークレットを作っています(後者なら消してかまいません)。シークレット期限切れによる停止事故が構造的に起きないのは、この構成の隠れた利点です。
- 「初回起動で『管理者の承認が必要です』と出る」: 3.3 節の管理者の同意漏れです。アクセス許可を後から追加した場合も、追加分の同意を取り直すまで同じ表示が出ます。
- 「夜間バッチに組み込んだら動かない」: 5.2 節のとおり WAM は対話セッション前提です。無人処理はユーザー委任トークンの流用ではなく、アプリケーション権限での別設計にします。
- 配布と更新: MSAL まわりは修正が活発で、ライブラリ更新を全端末へ配りきる仕組みが要ります。「自動アップデートのセキュリティ設計」で書いた更新経路の検証とセットで考えてください。
10. まとめ
WinForms / WPF アプリの Entra ID 認証対応は、次の 6 点に集約されます。
- 自前のパスワード管理をやめること自体が目的。保管・リセット・退職者対応・監査がテナント側に一元化される
- デスクトップアプリはパブリッククライアント。シークレットは持てないし、要らない
- ROPC は廃止方向。ユーザー名パスワードを預かる画面は新規に作らない
- 実装は MSAL.NET の
AcquireTokenSilent→AcquireTokenInteractiveパターン一択 - Windows では WAM ブローカー(
WithBroker) で SSO・条件付きアクセス・Windows Hello 対応 - トークンキャッシュの永続化(Extensions.Msal / DPAPI 保護)を最初から組み込む
「ログインだけ差し替える」最小構成(7.1 節)なら、既存アプリへの影響はログイン画面とユーザーテーブルの周辺に限定でき、数日規模の改修で収まることが多いです。一方、条件付きアクセスやオフライン要件が絡むと、テナント側の設定と業務の実態を踏まえた設計判断が必要になります。手元のアプリをどの構成まで持っていくべきか、自前認証からの移行手順をどう刻むか、判断に迷う場合はお手伝いできます。
関連記事
- GCPWとは - WindowsログオンをGoogle認証で扱う方法
- Windowsアプリの機密情報保存 - DPAPIで平文設定を避ける
- Windowsアプリ開発のセキュリティ最低限チェックリスト
- 自動アップデートのセキュリティ設計 - HTTPSだけでは足りない理由
関連する相談領域
合同会社小村ソフトでは、既存 WinForms / WPF アプリへの Entra ID 認証の組み込み(アプリ登録の設計、MSAL.NET 実装、自前認証からの移行計画)、自社 Web API のトークン検証の設計レビュー、条件付きアクセス絡みのサインイン障害の切り分けを扱っています。
参考リンク
-
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 ↩10 ↩11
-
Microsoft Learn, Using web browsers (MSAL.NET). フレームワーク別のブラウザー対応表(.NET Framework 4.6.2+ の既定が埋め込み、.NET 6+ はシステムブラウザーのみ)、システムブラウザーには http://localhost リダイレクト URI が必要なこと、WithUseEmbeddedWebView による切り替えについて。 ↩ ↩2
-
Microsoft Learn, Token cache serialization. デスクトップアプリは Microsoft.Identity.Client.Extensions.Msal のクロスプラットフォームキャッシュを使う推奨、MsalCacheHelper の使い方、ProtectedData(DPAPI、CurrentUser スコープ)による自前シリアル化の例について。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Desktop app that calls web APIs: Code configuration. デスクトップアプリのリダイレクト URI(モバイルとデスクトップ プラットフォーム、nativeclient / localhost)、「パブリック クライアント フローを許可する」設定の意味について。 ↩ ↩2 ↩3
-
Microsoft Learn, Microsoft identity platform and OAuth 2.0 Resource Owner Password Credentials. ROPC を使うべきでないこと、MFA と非互換でありブロックされること、ROPC 依存アプリが締め出される方向であること、デスクトップアプリはブローカーベース認証へ移行すべきことについて。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Desktop app that calls web APIs: Acquire a token using username and password. ユーザー名パスワードフロー(ROPC)がセキュリティリスクのため廃止(deprecated)とされたこと、移行ガイドの案内、MFA・条件付きアクセス・SSO 非対応の制約について。 ↩ ↩2
-
Microsoft Learn, Get a token from the token cache using MSAL.NET. AcquireTokenSilent を先に呼び MsalUiRequiredException で対話型へフォールバックする推奨パターン、キャッシュとリフレッシュトークンによる自動更新、アカウント削除によるキャッシュのクリアについて。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Register an application with the Microsoft identity platform. Entra 管理センターでのアプリ登録手順、サポートされるアカウントの種類の選択、クライアント ID の取得、管理者の同意について。 ↩ ↩2
-
Microsoft Learn, Desktop app that calls web APIs: Acquire a token by using WAM. WAM 利用時もトークンキャッシュの永続化が必要であること、OperatingSystemAccount によるサイレントサインインの推奨パターン、アプリ登録側のリダイレクト URI 設定について。 ↩ ↩2 ↩3
-
Microsoft Learn, Default reply URI. WithDefaultRedirectUri が設定するリダイレクト URI がプラットフォーム依存であること(.NET Framework デスクトップは https://login.microsoftonline.com/common/oauth2/nativeclient、.NET Core は http://localhost)について。 ↩ ↩2 ↩3
-
Microsoft Learn, Protected web API: Verify scopes and app roles. [Authorize] 属性だけでは不十分であり、scp クレーム(スコープ)の検証が必要なこと、Microsoft.Identity.Web の RequiredScope 属性による宣言的な検証について。 ↩ ↩2
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
Windowsアプリのタスクトレイ常駐とトースト通知 ── NotifyIconの落とし穴とAppNotificationの選び方
業務Windowsアプリのタスクトレイ常駐とトースト通知の実装を整理します。NotifyIconの正しい使い方、Explorer再起動時の再登録、トーストAPI3種の選定判断表、通知が届かないケースまで解説します。
WindowsデスクトップアプリのUI自動テスト ── UI Automationの仕組みとFlaUIで作る壊れにくいテスト
WinForms/WPFアプリのUI自動テストを、Windows UI Automationの仕組みから整理します。FlaUIによる最小実装、AutomationId設計と条件待機で壊れにくくする方法、CI無人実行の罠まで実務目線で解説します。
WPFの高DPI対応 ── 「DPIに強いはず」なのにぼやける・にじむ原因と対処
WPFはSystem DPI Awareですが、DPIの違うモニターへ移すと全体がにじみ、ビットマップはぼやけます。原因の切り分け、Per-Monitor DPI対応、UseLayoutRoundingなど定番対処、WindowsFormsHost混在の罠まで整理します。
WinFormsの高DPI対応 ── 4Kモニターでぼやける・崩れる原因と現実的な対処
4KモニターでWinFormsアプリがぼやける・崩れる原因を、DPI仮想化とDPI認識モード(System Aware / Per-Monitor V2)から整理します。.NETと.NET Frameworkの設定方法、AutoScaleModeの罠まで解説します。
WinForms / WPFアプリのCI/CD実践 ── GitHub Actionsでビルドから署名・配布まで自動化する
WinForms / WPFアプリのCI/CDをGitHub Actionsで組む実務ガイド。windows-latestでのビルド+テストの最小YAML、タグ駆動のバージョン採番、signtoolによる署名の組み込み、MSI/MSIX/ClickOnce/xcopy別のC...
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
UI スレッド / タイマーテーマ
WPF / WinForms、UI スレッド、async/await、タイマー設計を整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
技術相談・設計レビュー
改修方針、設計レビュー、既存資産の扱いを整理するための技術相談です。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- WinForms/WPFアプリにEntra ID認証を入れるメリットは何ですか?
- パスワードの保管・リセット対応・退職者のアカウント停止・サインイン監査がすべてテナント側の仕事になり、アプリ側の責任範囲が劇的に小さくなります。退職者はEntra IDのアカウントを無効化すれば全アプリで即座にログイン不能になり、多要素認証や条件付きアクセスもそのまま社内アプリに効きます。実装もMSAL.NETというライブラリと数十行のコードで済む範囲です。Microsoft 365導入済みの組織で自前認証を続ける理由はほとんどありません。
- ユーザー名とパスワードを自前の画面で受けて認証する方式(ROPC)は使えますか?
- 新規採用は禁止と考えてください。パブリッククライアント向けのROPCは公式に「セキュリティリスクのため廃止(deprecated)」と明記され、移行ガイドが公開されています。MFA・条件付きアクセスと非互換のため、テナントでMFAが必須化されているユーザーはブロックされてサインインできません。MFAの必須化はテナント側の設定でいつでも起こるので、今動いていてもある日突然全ユーザーがログインできなくなるリスクがあります。
- WAMブローカーは使ったほうがよいですか?
- Windowsでは推奨です。WithBrokerの1行で、Windowsサインイン済みアカウントとのSSO、条件付きアクセス・Windows Hello・FIDOキー対応、リフレッシュトークンのデバイスバインドが手に入ります。社内PCがEntra参加していれば、アプリ起動後にWindowsのアカウントを選ぶだけでパスワード入力なしにログインが完了します。ただしEntra ID専用で、Windowsサービスやタスクスケジューラなど対話的なユーザーセッション外では設計上エラーになる制約があります。
- アプリを再起動するたびにサインイン画面が出るのはなぜですか?
- トークンキャッシュを永続化していないためです。MSAL.NETのトークンキャッシュは既定ではメモリ上のみで、デスクトップアプリでは永続化の実装がアプリ側の責任です。公式の推奨はMicrosoft.Identity.Client.Extensions.Msalパッケージで、Windowsではキャッシュが暗号化されて保存されます。WAMを使う場合でもIDトークンとアカウントのメタデータの保存にキャッシュ永続化は必要です。