前言
密碼已是過去式。忘記密碼、弱密碼、密碼被釣魚攻擊——這些問題困擾開發者與使用者多年。在 .NET 10 中,ASP.NET Core Identity 正式內建支援 Passkey(密碼金鑰),讓你不需依賴第三方函式庫,就能為應用程式加上現代化、抗釣魚攻擊的無密碼認證機制。
本文將以 Razor Pages 為範例,帶你從零開始實作完整的 Passkey 註冊與登入流程。
什麼是 Passkey?
Passkey 是基於 Web Authentication API(WebAuthn) 與 FIDO2 標準所設計的密碼替代方案。它使用 非對稱金鑰加密:
- 私鑰(Private Key):安全儲存在使用者裝置上(例如 Windows Hello、Touch ID、Face ID,或密碼管理器)。
- 公鑰(Public Key):儲存在伺服器端。
認證時,使用者用裝置上的生物辨識或 PIN 驗證身分,私鑰永遠不會離開裝置,公鑰也沒有密碼外洩的風險。
主要優點
| 特性 | 說明 |
|---|---|
| 抗釣魚 | Passkey 與特定網站綁定,無法在假網站使用 |
| 無共享秘密 | 伺服器只存公鑰,資料庫洩露不影響用戶安全 |
| 使用方便 | 指紋、臉部辨識或 PIN 取代複雜密碼 |
| 跨裝置同步 | 透過 iCloud Keychain、Google Password Manager 等跨裝置同步 |
.NET 10 的 Identity Passkey 支援
.NET 10 將 Passkey 支援直接整合進 ASP.NET Core Identity,提供以下能力:
- 為既有帳號新增 Passkey 作為額外認證方式
- 無密碼帳號建立(直接以 Passkey 註冊)
- 純 Passkey 登入(完全不需要密碼)
重要限制: 此實作專注於 Identity 認證情境,不是完整 WebAuthn 函式庫。如需完整協定支援,可考慮社群套件如 fido2-net-lib。
核心概念
Attestation(證明 / 註冊)
使用者建立並註冊新 Passkey 的過程:
- 伺服器產生唯一挑戰(Challenge)
- 認證器建立金鑰對,回傳公鑰與 attestation 資料
- 伺服器驗證並儲存公鑰,供日後認證使用
Assertion(斷言 / 登入)
使用已存在的 Passkey 進行認證的過程:
- 伺服器產生新的挑戰
- 認證器用私鑰對挑戰簽名並回傳
- 伺服器用已儲存的公鑰驗證簽名,簽名正確即完成認證
環境需求
- .NET 10 SDK 或更新版本
- 支援 WebAuthn 的現代瀏覽器(Chrome、Edge、Safari)
- 具備平台認證器的裝置(Windows Hello、Apple Secure Enclave、實體安全金鑰等)
安全性考量
在實作前,請務必了解以下安全要求:
明確設定 ServerDomain
若未設定 ServerDomain,框架會從 Host Header 推斷,可能導致 credential-scoping 攻擊。建議在正式環境明確設定:
1 | builder.Services.Configure<IdentityPasskeyOptions>(options => |
子網域安全
在設定的 ServerDomain 範圍內,不可提供未受信任的內容。例如:在 contoso.com 註冊的 Passkey 也適用於 *.contoso.com,需確保所有子網域都受到控制。
實作步驟
以下我們使用**ASP.NET Core Web應用程式(Razor Pages)**專案來練習,
1. 建立 Razor Pages 專案
建立 ASP.NET Core Web應用程式(Razor Pages),架構選擇 **.NET 10.0(長期支援)**,驗證類型選擇 個別帳戶
1 | dotnet new webapp --auth Individual -f net10.0 -o PasskeyDemo |
2. 設定 Identity 與資料庫
- 預設會使用 SQLite, 資料庫名稱為
app.db
1 | // Program.cs |
3. 設定 Passkey 選項
1 | builder.Services.Configure<IdentityPasskeyOptions>(options => |
4. 執行 Migration
因為 IdentitySchemaVersions.Version3 新增了 AspNetUserPasskeys 資料表,需要執行 migration:
1 | dotnet ef migrations add SyncIdentitySchemaForNet10 |
執行後資料庫會新增 AspNetUserPasskeys 資料表,用於儲存每把 Passkey 的公鑰等資料。如下圖,
後端實作
4.1 PageModel:取得登入者的 Passkey 清單(OnGetAsync)
1 | using Microsoft.AspNetCore.Mvc; |
4.2 Passkey 註冊流程
Step 1:產生 Creation Options
1 | public async Task<IActionResult> OnPostPasskeyCreationOptions() |
Step 2:驗證並儲存 Passkey
1 | public async Task<IActionResult> OnPostPasskeyRegistration([FromBody] string credentialJson) |
4.3 Passkey 登入流程
Step 1:產生 Request Options
1 | public async Task<IActionResult> OnPostPasskeyRequestOptions() |
MakePasskeyRequestOptionsAsync(user)若帶入特定使用者,response 的allowCredentials只會列出該使用者的 Passkey。傳null則適合「無帳號輸入」的登入情境。
Step 2:驗證 Assertion 並登入
1 | public async Task<IActionResult> OnPostPasskeySignIn([FromBody] string credentialJson) |
4.4 刪除 Passkey
1 | public async Task<IActionResult> OnPostDeletePasskey([FromBody] string credentialIdBase64Url) |
UI(Index.cshtml) 實作
在 Razor Pages 中,建議把 UI 分成「已登入」與「未登入」兩個區塊,讓流程清楚:
- 已登入:可註冊 Passkey、查看已註冊清單、刪除指定 Passkey。
- 未登入:提供「使用 Passkey 登入」按鈕。
同時放一個隱藏表單承載 Anti-Forgery Token,供 AJAX 呼叫後端 handler 時附帶。
1 | @page |
UI 設計重點
- 顯示清單時,將
CredentialId轉成 Base64Url 字串再放入data-credential-id,方便前端傳回後端刪除。 Passkeys來源是OnGetAsync透過UserManager.GetPasskeysAsync(user)載入,畫面與資料一致。- 每次註冊/刪除/登入成功後重新整理頁面,確保清單、登入狀態與 Cookie 同步。
前端 JavaScript 實作
1 | @section Scripts { |
5.1 確認瀏覽器支援
1 | const browserSupportsPasskeys = |
5.2 Passkey 註冊
- 按下
註冊 PasskeyButton
1 | $("#btnRegisterPasskey").click(function () { |
5.3 Passkey 登入
- 按下
使用 Passkey 登入Button
1 | $("#btnSignInPasskey").click(function () { |
5.4 刪除 Passkey
- 按下
刪除Passkey 的 Button
1 | $(document).on("click", ".btnDeletePasskey", function () { |
完整程式碼
本文只拆解關鍵片段,完整可執行版本我放在 Gist:
操作過程
6.1 登入後註冊 Passkey
6.2 註冊 Passkey 成功後,會在 Passkey 列出註冊的資料
6.3 預設會存在 Browser 的密碼中
- 重覆註冊 Passkey 會發出
建立 Passkey 失敗:The user attempted to register an authenticator that contains one of the credentials already registered with the relying party.的錯誤。
6.4 按下 「使用 Passkey 登入」 Button
關鍵 API 整理
| API | 說明 |
|---|---|
SignInManager.MakePasskeyCreationOptionsAsync(user) |
產生 WebAuthn 註冊選項 JSON(出題),並查詢 DB 填入 excludeCredentials |
SignInManager.PerformPasskeyAttestationAsync(json) |
驗證前端回傳的 attestation(驗題),回傳 PasskeyAttestationResult |
UserManager.AddOrUpdatePasskeyAsync(user, passkey) |
將 Passkey 公鑰等資料寫入 AspNetUserPasskeys 資料表 |
SignInManager.MakePasskeyRequestOptionsAsync(user?) |
產生 WebAuthn 登入選項 JSON(出題),null 表示不指定使用者 |
SignInManager.PasskeySignInAsync(json) |
驗證 assertion 並自動 Sign In,回傳 SignInResult |
UserManager.GetPasskeysAsync(user) |
查詢使用者所有已註冊的 Passkey 清單(IList<UserPasskeyInfo>) |
UserManager.RemovePasskeyAsync(user, credentialId) |
從 DB 刪除指定 Passkey |
常見問題
Q:只刪除 Server 端的 Passkey 資料就夠了嗎?
不完全夠。Passkey 是雙邊資料:
- Server 端:公鑰、credentialId(你刪除的部分)
- 使用者裝置端:私鑰(存在 Keychain、密碼管理器等)
刪除 Server 端後,這把 Passkey 無法再登入你的網站,但裝置上仍有殘留憑證(orphan credential)。建議 UI 上提示使用者到裝置端(瀏覽器設定 / 系統密碼管理器)一併刪除。
Q:同一把 Passkey 刪掉後可以重新註冊嗎?
可以。刪除 Server 端資料後,MakePasskeyCreationOptionsAsync 產生的 options 不會再帶舊的 excludeCredentials,瀏覽器呼叫 navigator.credentials.create() 就不會被擋,可正常重新註冊。
Q:MakePasskeyCreationOptionsAsync 會查資料庫嗎?
會。它在產生選項時,會查詢 AspNetUserPasskeys 資料表取得該使用者既有的 Passkey,並填入 excludeCredentials,目的是讓瀏覽器/認證器避免在同一裝置上建立重複的 credential,防止重複綁定。
Q:JavaScript 可以直接刪除裝置上的 Passkey 嗎?
不能。WebAuthn API 提供 create 與 get,但不提供網站主動刪除認證器內憑證的能力。這是設計上的安全考量,避免任意網站偷偷移除使用者的憑證。
小結
.NET 10 將 Passkey 支援內建進 ASP.NET Core Identity,大幅降低了無密碼認證的實作門檻。整個流程的核心可以用一句話概括:
MakePasskey*OptionsAsync是「出題」,PerformPasskeyAttestation / PasskeySignIn是「驗題」。
只要掌握這個觀念,搭配 WebAuthn API 的 navigator.credentials.create() 與 navigator.credentials.get(),就能在 Razor Pages 應用程式中快速實現現代化的無密碼登入體驗。