# [實作] .Net Framework WebAPI 實作 Passkey ### :arrow_forward: **綱要** 1. **PassKey 是什麼** 2. **公鑰與私鑰架構** 3. **本文環境與套件設定** 4. **資料庫 Schema** 5. **控制器全域物件設定** 6. **使用 PassKey 註冊 - 程式碼實作** 7. **使用 PassKey 登入 - 程式碼實作** --- ### 1. PassKey 是什麼 是一種新型態的帳戶登入作法,使用者透過裝置(電腦、手機等),就可以直接登入,無須輸入任何密碼 - **架構**:採用公、私鑰架構,建立非對稱加密 - **作法**:將公、私鑰分別儲存在網站資料庫以及使用者裝置,透過挑戰驗證使用者 - **優點**:使用者無須輸入任何密碼,同時也不用記住任何密碼 - **隱憂**:需要瀏覽器支援 WebAuthn API,否則無法使用 --- ### 2. 公鑰與私鑰架構 ![image](https://hackmd.io/_uploads/HJRInLPZ0.png) #### (1) 原則:私鑰進行加密,僅可以使用公鑰進行解密,反之亦然 #### (2) 應用:網站後端發送挑戰("A口A"),使用者使用私鑰進行加密,回傳加密物件給與網站後端,後端透過公鑰進行解密,若解密結果和挑戰("A口A")相同,代表此使用者為該公鑰的主人 --- ### 3. 本文環境與套件設定 #### (1) 環境 - IDE:VS-2022 - 框架:.Net Framework 4.7.2 - 專案:ASP NET Web API #### (2) 套件 - Nuget 套件:Fido2.NetFramework - 套件 Github:https://github.com/wzychla/Fido2.NetFramework --- ### 4. 資料庫 Schema Code First 設定 #### (1) User 會員表單 ```csharp public class User { [Key] [Display(Name = "編號")] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } ... [Display(Name = "憑證")] // 虛擬欄位 public virtual ICollection<Credential> Credential { get; set; } } ``` #### (2) Credential 公鑰表單 ```csharp public class User { [Key] [Display(Name = "編號")] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } [Display(Name = "CredentialId")] [Required] public byte[] CredentialId { get; set; } [Display(Name = "公鑰")] [Required] public byte[] PublicKey { get; set; } ... [Display(Name = "User Id")] public int UserId { get; set; } // 外鍵欄位 [JsonIgnore] [ForeignKey("UserId")] [Display(Name = "User 表單")] public virtual User User { get; set; } // 虛擬欄位 } ``` --- ### 5. 控制器類別內部變數設定 #### (1) DbModel 物件建立 #### (2) IFido2 物件建立 ```csharp public class UserController : ApiController { // DbModel 物件建立 private DbModel db = new DbModel(); // _fido2 欄位建立 private IFido2 _fido2; // UserController 建構子建立,目的為初始化 _fido2 欄位 public UserController() { // Fido2 Config 物件建立 Fido2Configuration config = new Fido2Configuration(); config.ServerDomain = "sun-live.vercel.app"; // 此處 需要替換為 網站路由 config.ServerName = "FarmerProject"; // 名稱 可自由命名 // 此處 需要替換為 網站路由 config.Origins = new HashSet<string>(new[] { @"https://sun-live.vercel.app" }); config.TimestampDriftTolerance = int.Parse("300000"); _fido2 = new Fido2(config); // 建立 _fido2 物件 } ... } ``` --- ### 6. 註冊 API ![image](https://hackmd.io/_uploads/r1bbevvbA.png) #### (1) API 1:建構與回傳選項物件 ```csharp public IHttpActionResult API_1(string AccountName) { // 進行 DB 會員註冊 var InsertUser = new User { AccountName = AccountName, ... }; db.Users.Add(InsertUser); db.SaveChanges(); // 取得資料庫 UserId int userId = InsertUser.Id; //==== 開始建構選項物件==== // 一、建立 userIdBytes byte 陣列 byte[] userIdBytes = BitConverter.GetBytes(userId); // 二、判斷 使用者 是否已經於資料庫內 擁有 其他公鑰 // 這邊不進行任何設定,僅 new 物件(細節作法可參考作者GH) var existingKeys = new List<PublicKeyCredentialDescriptor>(); // 三、建立 Selction 選項物件,可以設定一些後端指定的選項內容 var authenticatorSelection = new AuthenticatorSelection { ResidentKey = ResidentKeyRequirement.Required, UserVerification = UserVerificationRequirement.Preferred, }; // 四、建立 ClientInputs 物件 var exts = new AuthenticationExtensionsClientInputs() { }; // 五、建立 options 選項物件 var options = _fido2.RequestNewCredential( new Fido2User() // 需要提供以下欄位資料 { DisplayName = displayName, Id = userIdBytes, Name = username }, existingKeys, authenticatorSelection, AttestationConveyancePreference.None, exts); //==== 建構選項物件完成,以下回傳給前端==== var result = new { statusCode = 200, status = "success", message = "設定成功,請返回option物件給予使用者", option = options // 提供選項物件 }; return Content(HttpStatusCode.OK, result); } ``` #### (2) API 2:儲存公鑰與回傳結果 - 前端將會回傳一個物件,內部包含兩個物件,**aarr** 與 **ccp**,因此創立一個類別,接收該物件 (如下) - Note:**ccp** 物件即為 API-1 所創立的 **options** 選項物件,需要請前端進行瀏覽器暫存,藉由 API-2 一併回傳給與後端 - Note:**aarr** 物件即為公鑰物件,內部包含使用者所產生的公鑰 ```csharp public class AuthnAttestationResultInput { public AuthenticatorAttestationRawResponse aarr { get; set; } public CredentialCreateOptions ccp { get; set; } } ``` ```csharp public IHttpActionResult API_2(AuthnAttestationResultInput inputs) { // 一、分離 inputs 物件 AuthenticatorAttestationRawResponse aarr = inputs.aarr; CredentialCreateOptions options = inputs.ccp; // 二、檢驗 User 唯一性 // 這邊永遠回傳 true (細節作法可參考作者GH) IsCredentialIdUniqueToUserAsyncDelegate callback = async (args, cancellationToken) => { return true; }; // 三、套件進行處理 var success = await _fido2.MakeNewCredentialAsync( aarr, options, callback); // 四、存入資料庫的前置資料預處理: // 取得使用者 Id,作為 Credential表單的 User表外鍵 Fido2User inputUserInfor = options.User; byte[] userIDbytes = inputUserInfor.Id; int userId = BitConverter.ToInt32(userIDbytes, 0); // 五、存入資料庫 var InsertNewPK = new Credential { UserId = userId, // 外鍵欄位 CredentialId = success.Result.Id, // Credential Id 欄位 PublicKey = success.Result.PublicKey // 公鑰 欄位, }; db.Credential.Add(InsertNewPK); db.SaveChanges(); var result = new { statusCode = 200, status = "success", message = "註冊成功,請重新登入", }; return Content(HttpStatusCode.OK, result); } ``` --- ### 7. 登入 API 實作 ![image](https://hackmd.io/_uploads/rkqGewDW0.png) #### (1) API 3:建構與回傳驗證物件 ```csharp public IHttpActionResult API_3() { // 一、建構 Selction 物件,可以設定一些後端指定的選項內容 var authenticatorSelection = new AuthenticatorSelection { ResidentKey = ResidentKeyRequirement.Required, UserVerification = UserVerificationRequirement.Required, }; // 二、建立 ClientInputs 物件 var exts = new AuthenticationExtensionsClientInputs(){}; // 三、建立 驗證 選項物件 var VerifyObject = _fido2.GetAssertionOptions( new List<PublicKeyCredentialDescriptor>() { }, UserVerificationRequirement.Required, exts); // 四、發送給予前端 var result = new { statusCode = 200, status = "success", message = "設定成功,請返回option物件給予使用者", data = VerifyObject }; return Content(HttpStatusCode.OK, result); } ``` #### (2) API 4:驗證加密物件與回傳結果 - 前端將會回傳一個物件,內部包含兩個物件,**aarr** 與 **ao**,因此創立一個類別,接收該物件 (如下) - Note:**ao** 物件即為 API-3 所創立的 **VerifyObject** 驗證物件,需要請前端進行瀏覽器暫存,藉由 API-4 一併回傳給與後端 - Note:**aarr** 物件即為加密物件,內部包含使用者所產生的加密內容 ```csharp public class AuthnAssertionResultInput { public AuthenticatorAssertionRawResponse aarr { get; set; } public AssertionOptions ao { get; set; } } ``` ```csharp public IHttpActionResult API_4(AuthnAssertionResultInput inputs) { // 一、分離 inputs 物件 var options = inputs.ao; var AssertionResult = inputs.aarr; // 二、取得 使用者 Credential 資料表資料 var CredentialData = db.Credential.Where(x => x.CredentialId == AssertionResult.Id)?.FirstOrDefault(); // 三、設定 credential counter 有幾筆資料 // Note:進行簡化,直接設定 0 uint storedCounter = 0; // 四、檢驗 User 是否擁有 credentialId // 這邊永遠回傳 true (細節作法可參考作者GH) IsUserHandleOwnerOfCredentialIdAsync callback = async (args, cancellationToken) => { return true; }; // 五、套件進行解碼與驗證比對 var res = await _fido2.MakeAssertionAsync( AssertionResult, options, CredentialData.PublicKey, new List<byte[]>(), storedCounter, callback); // 如果比對驗證成功,就可以進行登入處理 if (res.Status == "ok") { var result = new { statusCode = 200, status = "success", message = "登入成功", ... }; return Content(HttpStatusCode.OK, result); } else { var result = new { statusCode = 401, status = "error", message = "驗證失敗", }; return Content(HttpStatusCode.OK, result); } } ``` ---