# [實作] .Net Framework WebAPI 實作 Passkey
### :arrow_forward: **綱要**
1. **PassKey 是什麼**
2. **公鑰與私鑰架構**
3. **本文環境與套件設定**
4. **資料庫 Schema**
5. **控制器全域物件設定**
6. **使用 PassKey 註冊 - 程式碼實作**
7. **使用 PassKey 登入 - 程式碼實作**
---
### 1. PassKey 是什麼
是一種新型態的帳戶登入作法,使用者透過裝置(電腦、手機等),就可以直接登入,無須輸入任何密碼
- **架構**:採用公、私鑰架構,建立非對稱加密
- **作法**:將公、私鑰分別儲存在網站資料庫以及使用者裝置,透過挑戰驗證使用者
- **優點**:使用者無須輸入任何密碼,同時也不用記住任何密碼
- **隱憂**:需要瀏覽器支援 WebAuthn API,否則無法使用
---
### 2. 公鑰與私鑰架構

#### (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

#### (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 實作

#### (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);
}
}
```
---