在 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. 先講結論

  • 停止自行管理帳號密碼、改用 Entra ID 之後,密碼保管、重設支援、離職者帳號停用、登入稽核全部變成租戶端的工作。應用程式端的責任範圍大幅縮小,是最大的好處。
  • 桌面應用程式是公用客戶端。exe 到了發布的地方就可能被分析,因此不能(也不該)持有用戶端密鑰。應用程式註冊也要設定為公用客戶端。1
  • 應用程式直接接收使用者名稱與密碼的 ROPC(Resource Owner Password Credentials)官方已明確標示為「廢除(deprecated)」,並公開了遷移指南。它與 MFA、條件式存取無法並存,實質上正朝著無法使用的方向前進。新專案應視為禁止採用。23
  • 實作用 MSAL.NET(Microsoft.Identity.Client),唯一的基本呼叫模式是:先呼叫 AcquireTokenSilent,只有在收到 MsalUiRequiredException 時才回退到 AcquireTokenInteractive4
  • 在 Windows 上,建議透過 WAM(Web Account Manager)代理進行驗證。只要一行 WithBroker,就能取得與已登入 Windows 帳號的 SSO、條件式存取・Windows Hello・FIDO 金鑰支援,以及重新整理權杖的裝置繫結。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) 網頁伺服器或常駐程式等能保存密鑰或憑證的應用程式。桌面應用程式不屬於這一類
ID 權杖 表示「這個人是誰」的 JWT。只需要登入功能的話,這個就夠了
存取權杖 呼叫特定 API(Microsoft Graph 或自家 Web API)用的通行證,裡面內嵌了目標(audience)與範圍
重新整理權杖 用來在不需互動的情況下更新上述兩種權杖的權杖。由 MSAL 在快取中自動管理,應用程式不會直接看到

第一行是重點。因為 exe 到了發布的地方就能被分析、反編譯,內嵌進去的「秘密」就不再是秘密。因此才要註冊成不需要密鑰、能運作的公用客戶端,而驗證本身(輸入密碼、MFA)則交給瀏覽器或作業系統的代理,應用程式只負責接收權杖。應用程式完全不接觸使用者密碼,正是這套機制的根本。

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 運作,也請以遷移為前提訂定計畫。實務上桌面應用程式可以使用的取得權杖方式,實質上只有以下兩種。

流程 使用場合
互動式(代理 / 瀏覽器) 一般的 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://localhost9 如果 .NET Framework 應用程式只登錄了 ms-appx-webhttp://localhost,當 WAM 回退到瀏覽器時,就會因為和 nativeclient 不一致而發生驗證錯誤。請把 3 個都登錄,或者用 WithRedirectUri(...) 明確固定下來。不小心登錄到「Web」平台而導致驗證錯誤,也是常見的踩坑點。

3.3 API 存取權限與管理員同意

[API 權限] 加入應用程式要呼叫的 API 的委派存取權限(delegated permission)。如果只需要登入和顯示個人資料,預設就會授予的 Microsoft Graph User.Read 就足夠。

加入之後,請執行 [授予(租戶名稱)的管理員同意]7 這樣就不會在第一次登入時跳出每個使用者的同意對話框。在使用者自行同意功能已停用的租戶裡,沒有管理員同意,第一次登入會停在「需要管理員核准」,因此內部發布的應用程式,原則上要在發布前就完成管理員同意

3.4 「允許公用客戶端流程」開關

[驗證] 進階設定裡的「允許公用客戶端流程」,是在使用不依賴重新導向 URI 的流程時(例如裝置代碼流程或整合式 Windows 驗證)設為「是」。1 如果只用互動式(瀏覽器 / 代理),不需要開啟。另外要注意,這個應用程式註冊完全不會建立用戶端密鑰或憑證。「憑證和密鑰」一欄是空的,才是公用客戶端的正確狀態(因為常有人搞混,第 9 章會再提一次)。

4. 用 MSAL.NET 實作 ── Silent→Interactive 的基本模式

透過 NuGet 加入 Microsoft.Identity.Client。實作模式只要記住一個就夠了:一定先呼叫 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 另外要注意,在 .NET Framework 應用程式裡不要省略 WithUseEmbeddedWebView(false)。因為.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 整個應用程式應該共用一個實例。因為快取是綁在實例上的,每次呼叫都 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

  • 強化安全性:重新整理權杖會與裝置繫結,即使被偷走也無法在其他裝置上使用(權杖保護)。安全性的改善會透過作業系統的更新持續帶入。
  • 功能支援Windows Hello、條件式存取、FIDO 金鑰等作業系統・服務整合的驗證功能,不需要額外程式碼就能使用。
  • 系統整合:已登入 Windows 的帳號會出現在內建的帳戶選擇器中,多數情況下不需要輸入密碼就能完成登入,實質上等於 SSO。
  • 權杖保護:可以配合條件式存取的權杖保護政策。

如果公司的電腦已加入 Entra(或混合式加入),體驗就會變成「啟動應用程式 → 選擇 Windows 帳號 → 立即完成登入」,完全不需要在任何地方輸入密碼。

5.1 實作 ── WithBroker 與套件

要使用 WAM,需要 MSAL.NET 4.52.0 以上版本,以及額外的套件 Microsoft.Identity.Client.Broker5 在第 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();

靜默取得這一端還可以再強化一行。當快取裡沒有帳號時,傳入 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 忘記這一步,會導致代理錯誤,讓互動式驗證失敗。另外要注意,上面範例中的 WithDefaultRedirectUri(),只決定 WAM 無法使用而回退到瀏覽器時要用的重新導向 URI,而它的解析結果依平台而異(.NET Framework → nativeclient,.NET → http://localhost)。9 如果已經照 3.2 節的表登錄了 3 個 URI,兩種都能通過;如果想縮小登錄範圍,就用 WithRedirectUri(...) 明確指定。

5.2 WAM 的限制 ── 不知道會踩到的地方

限制 內容
作業系統 Windows 10 (1703)+ / Windows Server 2019+。更早版本、Mac、Linux 會自動回退到瀏覽器5
身分識別提供者 僅限 Entra ID。Azure AD B2C・AD FS 的 authority 不支援(會回退到瀏覽器)5
執行環境 前提是能在互動式使用者工作階段中顯示 UI。Windows 服務、工作排程器(使用者工作階段之外)、用 runas 以其他使用者身分執行時,設計上就會發生錯誤5

第三行特別重要。「在有畫面的應用程式裡能動,但同一份程式碼拿去用在夜間批次就會失敗」,這是規格,不是 bug。無人執行的部分,應該用應用程式權限(機密客戶端)另外設計,而不是沿用委派給使用者的權杖。因為回退機制本來就內建在規格裡,「先試 WAM,不行再用瀏覽器」這件事,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() 之後立刻註冊一次

在 Windows 上,快取會被加密儲存。官方文件裡自行實作的範例,是用 ProtectedDataDPAPI,DataProtectionScope.CurrentUser)加密權杖後存成檔案,Extensions.Msal 可以理解成把這個做法完成到產品品質等級的函式庫。6 「使用者個人的秘密要用使用者範圍的 DPAPI 保護」這個原則,跟「Windows 應用程式的機密資訊保存 - 用 DPAPI 避免明文設定」裡寫的設定檔議題是一樣的。把權杖快取存成明文 JSON 的自行實作,應該和把連線字串存成明文一樣嚴重地對待。

補充 3 點維運上的注意事項。

  • 即使使用 WAM,也需要持久化快取。因為 MSAL 仍然會把 ID 權杖與帳號的元資料存進自己的快取裡。8
  • 儲存位置基本上是 %LOCALAPPDATA%\公司名稱\應用程式名稱。因為 DPAPI 的綁定關係,在別的電腦或別的使用者下無法解密,但實際影響只是靜默取得失敗、退回重新登入,不會造成真正的損害。
  • 「登出」的實作方式,是用 GetAccountsAsync 列出帳號後,用 RemoveAsync 刪除,不需要刪除快取檔案。但 RemoveAsync 清除的只是 MSAL 的本地快取,WAM、瀏覽器、Windows 登入端的工作階段都還留著。下一次互動式登入時,同一個帳號有可能被靜默地重新登入,所以如果需要在共用電腦上確實做到帳號切換,就要明確設計:在 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 改變也不會變動),並刪除密碼欄位。因為不需要更動本地資料庫的設計,只是替換驗證方式,這是最容易推薦的第一步構成。

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 登入就足夠
在完全離線環境(封閉的生產線、攜出電腦)中運作 不可行或需要額外設計 第一次登入與權杖更新都需要網路
未導入 Entra ID(僅有地端 AD、僅有 Google Workspace) 考慮其他方案 前者用 AD 驗證(整合式 Windows 驗證),後者用 Google 端的機制更自然

離線需求要特別留意。AcquireTokenSilent 在快取的存取權杖有效期間內(經驗上大約是一小時多一點),就算離線也能返回權杖,但一旦過期,更新就需要網路。導入之前,需要把這個生命週期的前提,拿去跟現場的實際使用模式對照確認是否可行。

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 節),對現有應用程式的影響可以限定在登入畫面與使用者資料表周邊,通常幾天規模的改造就能完成。另一方面,一旦涉及條件式存取或離線需求,就需要根據租戶端設定與業務實際狀況做出設計判斷。如果不確定手上的應用程式該做到哪個構成、該怎麼安排從自建驗證遷移的步驟,歡迎與我們聯絡協助。

相關文章

相關諮詢領域

合同會社小村軟體處理將 Entra ID 驗證整合進既有 WinForms / WPF 應用程式(應用程式註冊設計、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、支援的作業系統與回退機制,以及需要互動式工作階段等限制。  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

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

在 WinForms/WPF 應用程式中加入 Entra ID 驗證有什麼好處?
密碼保管、重設支援、離職者帳號停用、登入稽核全部變成租戶端的工作,應用程式端的責任範圍大幅縮小。離職者只要停用 Entra ID 帳號,就會立刻無法登入所有應用程式,多重要素驗證與條件式存取也會直接套用到內部應用程式。實作只需要 MSAL.NET 這個函式庫和幾十行程式碼。已導入 Microsoft 365 的組織,幾乎沒有理由繼續維持自建驗證。
可以用自己的畫面接收使用者名稱與密碼再進行驗證(ROPC)嗎?
新專案應視為禁止採用。公用客戶端用的 ROPC 官方已明確標示「因安全風險而廢除(deprecated)」,並公開了遷移指南。它與 MFA、條件式存取不相容,租戶強制要求 MFA 的使用者會被擋下、無法登入。由於 MFA 的強制設定隨時可能由租戶端啟用,即使現在還能運作,也有可能哪天突然導致所有使用者都無法登入。
應該使用 WAM 代理嗎?
在 Windows 上是建議使用的。只要一行 WithBroker,就能取得與已登入 Windows 帳號的 SSO、條件式存取・Windows Hello・FIDO 金鑰支援,以及重新整理權杖的裝置繫結。只要公司的電腦已加入 Entra,啟動應用程式後選擇 Windows 帳號即可完成登入,完全不需要輸入密碼。不過它專屬於 Entra ID,且在 Windows 服務、工作排程器這類沒有互動式使用者工作階段的環境下,設計上就會出錯。
為什麼每次重新啟動應用程式都會出現登入畫面?
因為沒有把權杖快取持久化。MSAL.NET 的權杖快取預設只存在於記憶體中,在桌面應用程式裡,持久化的實作是應用程式端的責任。官方建議使用 Microsoft.Identity.Client.Extensions.Msal 套件,在 Windows 上快取會被加密儲存。即使使用 WAM,仍然需要持久化快取,因為 MSAL 還是需要保存 ID 權杖與帳號的元資料。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽