iT邦幫忙

0

桌面、手機、網站都要接 OAuth2 登入,能用同一套嗎?

  • 分享至 

  • xImage
  •  

Bee.OAuth2 改版而來的 Polhem.OAuth2,這次連 .NET MAUI 也一起涵蓋

Polhem OAuth2 套件介紹

在 .NET 應用程式加上「用 Google 登入」,看起來是早就有現成答案的事,但手上不只一種應用程式時就不一樣了。網站用 ASP.NET Core 的驗證處理常式,桌面工具需要能開瀏覽器、接住回呼的東西,行動 App 又是另一套做法,而那個還在線上服務的舊 Web Forms 網站,以上都用不上。

我想要一個能用同一套方式處理這些情境的小型函式庫,所以做了 Polhem.OAuth2,以 MIT 授權開源,可從 NuGet 取得。

如果你讀過我之前那篇 Bee.OAuth2 for .NET:快速整合 OAuth2 登入Polhem.OAuth2 就是它的後繼版本。它保留了「每個 provider(登入服務供應商,如 Google、LINE)一組設定」的概念,其餘大多重新打造,第 6 節列出改了什麼。


1️⃣ 涵蓋範圍

  • 桌面與主控台應用程式:開啟系統瀏覽器,在 loopback 位址(本機的 127.0.0.1localhost)接收回呼,並使用 PKCE。支援 Windows、macOS、Linux。
  • Android、iOS、Mac Catalyst 上的 .NET MAUI 應用程式:透過 WebAuthenticator 登入,使用 PKCE,App 裡不放 client secret。有自己 ASP.NET Core 後端的 App,也可以改經由後端登入,provider 的 token 就不會到裝置上。
  • ASP.NET Core:使用 PKCE 的授權碼流程,每次登入的狀態各自存在一個受保護的 cookie 裡。
  • 傳統 ASP.NET:同一套流程,給 System.Web 上的 Web Forms 與 MVC 應用程式用。
  • 其他伺服器框架:核心的 OAuth2Client 分兩步執行流程,不相依任何 HTTP 框架。

PKCE(Proof Key for Code Exchange)是 OAuth2 的延伸規格:發起登入時先產生一組隨機值,換 token 時再出示它,即使授權碼被攔截,沒有這組值也換不到 token。對保不住 client secret 的桌面與手機 App 來說,這是必備的保護。

支援的 provider:Google、Facebook、LINE、Microsoft Entra ID、Auth0、Okta。每一家回傳的結果格式都一樣:使用者 ID、名稱與 email,加上 access、refresh 與 ID token。

套件 目標框架 用途
Polhem.OAuth2 netstandard2.0、net10.0 各 provider;桌面、主控台與 .NET MAUI 應用程式;其他伺服器框架
Polhem.OAuth2.AspNetCore net10.0 ASP.NET Core
Polhem.OAuth2.AspNet net472 System.Web 上的 ASP.NET Web Forms 與 MVC

2️⃣ 桌面或主控台應用程式

LoopbackOAuth2Client 在回呼網址上監聽,用預設瀏覽器開啟登入頁,等 provider 導回來,再交換授權碼:

using Polhem.OAuth2;

var options = new GoogleOAuth2Options
{
    ClientId = "your-client-id",
    ClientSecret = "your-client-secret",
    RedirectUri = "http://127.0.0.1:0/callback"
};

var client = new LoopbackOAuth2Client(options);
AuthorizationResult result = await client.SignInAsync();

if (result.IsSuccess)
    Console.WriteLine($"{result.UserInfo.UserId} {result.UserInfo.UserName} {result.UserInfo.Email}");

Port 0 會在每次登入時挑一個空的 port。這只適用於接受任意 loopback port 的 provider(例如 Google),其他 provider 要用你註冊的 port。


3️⃣ ASP.NET Core 應用程式

在啟動時註冊 client:

builder.Services.AddOAuth2Client("Google", new GoogleOAuth2Options
{
    ClientId = "your-client-id",
    ClientSecret = "your-client-secret",
    RedirectUri = "https://localhost:7032/auth/callback"
});

接著用兩個 action 開始與完成登入:

public class AuthController(OAuth2Manager oauth2Manager) : ControllerBase
{
    [HttpGet("/auth/login")]
    public IActionResult Login()
    {
        return Redirect(oauth2Manager.CreateAuthorizationUrl(HttpContext, "Google"));
    }

    [HttpGet("/auth/callback")]
    public async Task<IActionResult> Callback()
    {
        AuthorizationResult result = await oauth2Manager.CompleteAuthorizationAsync(HttpContext, HttpContext.RequestAborted);
        return result.IsSuccess
            ? Content($"Hello, {result.UserInfo.UserName}")
            : Content($"The sign-in failed: {result.Exception.Message}");
    }
}

每次登入的 state、PKCE code verifier 與回呼網址各自存在一個 cookie 裡,以 ASP.NET Core Data Protection 加密。因此不需要 Session,在多個分頁同時開始的登入不會互相覆蓋,多台伺服器只要共用 Data Protection 金鑰就能一起運作。


4️⃣ .NET MAUI 應用程式

行動 App 裡的任何東西都可能被解開,保不住 client secret,所以 AppOAuth2Client 一律使用 PKCE、不帶 client secret。你要傳給它一個開啟瀏覽器的函式,通常就是 WebAuthenticator

var options = new Auth0OAuth2Options
{
    Domain = "your-tenant.auth0.com",
    ClientId = "your-native-client-id",
    RedirectUri = "com.example.app:/oauth2redirect"
};

var client = new AppOAuth2Client(options, async (url, redirectUri, cancellationToken) =>
    (await WebAuthenticator.Default.AuthenticateAsync(url, redirectUri)).CallbackUri);

AuthorizationResult result = await client.SignInAsync();

如果 App 要登入自己的後端,可以由後端以 web client 的身分完成登入,再交給 App 一個只能用一次的 code。App 用這個 code 換取使用者資訊,provider 的 token 則留在伺服器上。


5️⃣ 設計取捨

幾個決策決定了這個函式庫的行為:

  • PKCE 預設開啟,所有 provider 端點都必須是 https。
  • 建立 client 時就檢查設定:設定不正確會立刻擲出例外;在 ASP.NET Core 是應用程式啟動時,而不是第一次登入時。
  • 預期中的失敗以結果回傳,程式錯誤才擲出例外:HTTP 請求失敗、state 不符,或 provider 回傳錯誤,都放在 result.Exception;未註冊的 client 名稱屬於程式錯誤,直接擲出。
  • 用 provider 加上使用者 ID 識別使用者,不用 email。email 會變,各 provider 對 email 是否驗證過的做法也不一。
  • 相依很少:核心套件的 net10.0 版本沒有任何套件相依,公開 API 有 nullable 標註,net10.0 版本也經過 trim 與 AOT analyzer 檢查(確認發佈時裁掉未用程式碼、預先編譯成原生碼後仍能正常運作)。

每個決策的理由都以 ADR(Architecture Decision Record,架構決策紀錄)寫在 repo 的 docs/adr/ 裡。


6️⃣ 從 Bee.OAuth2 過來

  • 五個套件變成三個Bee.OAuth2.WinFormsBee.OAuth2.Desktop 改由核心套件裡的 LoopbackOAuth2Client 取代。
  • 桌面登入改用系統瀏覽器,不再用內嵌的 WebView2 視窗。使用者看得到網址列,沿用自己既有的登入狀態與密碼管理員,App 也看不到他們輸入了什麼。Google 的政策本來就拒絕內嵌瀏覽器;拿掉 WebView2 之後,桌面登入也不再綁定 Windows。
  • 網站登入不再需要 Session 或 OAUTH2_STATE_KEY 環境變數:每次登入有自己的 cookie,以 ASP.NET Core Data Protection 或 MachineKey 保護。
  • ASP.NET Core 一行註冊AddOAuth2Client 取代原本手動組出來的 OAuth2Manager singleton。
  • 新增 Okta 與 .NET MAUI 支援
  • 不再相依 Bee.BaseNewtonsoft.Json,JSON 改用 System.Text.Json 解析。

README 有一份遷移指南,逐一對應舊的型別與方法該換成什麼。


✅ 結語

安裝核心套件:

dotnet add package Polhem.OAuth2

README 涵蓋各類應用程式的用法,包括如何向各 provider 註冊回呼網址。repo 裡也附了範例應用程式:主控台、.NET 與 .NET Framework 上的 Windows Forms、ASP.NET Core、.NET MAUI。

這個函式庫只處理「登入並取得使用者資訊與 token」這一段,不驗證 ID token 的簽章,也不管登入後的帳號與權限。如果你的專案已經用 ASP.NET Core 內建的驗證機制或 OpenID Connect 套件處理得很好,不一定需要換;它比較適合手上同時有桌面、手機、網站幾種應用程式,想用同一套寫法的情境。歡迎回饋、開 issue 或送 pull request。

延伸閱讀


📘 HackMD 原文筆記:
👉 https://hackmd.io/@jeff377/polhem-oauth2

📢 歡迎轉載,請註明出處
📬 歡迎追蹤我的技術筆記與實戰經驗分享
FacebookHackMDGitHubNuGet


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言