之前再研究的時候需要用到 WebAuth 所以研究了一下,然後拆解了一個範例,拆得比較簡單,這也是為何之前,我都在研究關於 CBOR 的原因
因為在案例裡面很常出現。

我把原本案例改成三個步驟
1.註冊會員
2.登入會員+綁定裝置
3.登入會員+驗證裝置
這篇就主要針對註冊會員,還有登入+綁定裝置
1. 首先安裝一下 Fido2.AspNet
2.其實在 Fido2.AspNet 中有一個 Fido2User 的物件 對我寫 sample 來說夠用只是少了一個 password 所以我直接繼承來使用
using Fido2NetLib;
namespace Fido2SimpleSample.Models
{
public class DFido2User: Fido2User
{
public string Pass { get; set; }
}
}
3. 註冊用戶的部分,其實也沒啥好說的,中間我也沒用到任何資料庫單純寫檔案紀錄資料
[HttpPost]
[Route("registuser")]
[Produces("application/json")]
public IActionResult RegistUser([FromForm] string userid,
[FromForm] string displayName,
[FromForm] string userpass)
{
try
{
if (string.IsNullOrEmpty(userid))
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = "userid cannot be null" });
}
var user = AuthStorageUtil.GetUserById(userid);
if (user == null)
{
user = new DFido2User
{
DisplayName = displayName,
Name = userid,
Id = Encoding.UTF8.GetBytes(userid),
Pass = AuthStorageUtil.GetMD5(System.Text.Encoding.UTF8.GetBytes(userpass))
};
AuthStorageUtil.SaveUserData(user);
return Ok(new CredentialCreateOptions { Status = "ok" });
}
else
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = "regist already." });
}
}
catch (Exception e)
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = e.Message });
}
}
4.登入會員+綁定裝置,其實在後端在登入後會產生一個新的憑證放在 session ( fido2.attestationOptions ) 中,當然這邊你可以改寫到 redis 或是其他Temp DB
JS Code:
$('#login').click(async function (event) {
event.preventDefault();
var userid = $('#userId').val();
var userpass = $('#userPass').val();
// possible values: none, direct, indirect
let attestation_type = "none";
// possible values: , platform, cross-platform
let authenticator_attachment = "";
// possible values: preferred, required, discouraged
let user_verification = "preferred";
// possible values: true,false
let require_resident_key = false;
// prepare form post data
var data = new FormData();
data.append('userid', userid);
data.append('userpass', userpass);
data.append('attType', attestation_type);
data.append('authType', authenticator_attachment);
data.append('userVerification', user_verification);
data.append('requireResidentKey', require_resident_key);
// send to server for registering
let makeCredentialOptions;
try {
makeCredentialOptions = await fetchMakeCredentialOptions(data);
} catch (e) {
console.error(e);
let msg = "Something wen't really wrong";
showErrorAlert(msg);
}
console.log("Credential Options Object", makeCredentialOptions);
if (makeCredentialOptions.status !== "ok") {
console.log("Error creating credential options");
console.log(makeCredentialOptions.errorMessage);
showErrorAlert(makeCredentialOptions.errorMessage);
return;
}
// Turn the challenge back into the accepted format of padded base64
makeCredentialOptions.challenge = coerceToArrayBuffer(makeCredentialOptions.challenge);
// Turn ID into a UInt8Array Buffer for some reason
makeCredentialOptions.user.id = coerceToArrayBuffer(makeCredentialOptions.user.id);
makeCredentialOptions.excludeCredentials = makeCredentialOptions.excludeCredentials.map((c) => {
c.id = coerceToArrayBuffer(c.id);
return c;
});
if (makeCredentialOptions.authenticatorSelection.authenticatorAttachment === null) makeCredentialOptions.authenticatorSelection.authenticatorAttachment = undefined;
console.log("Credential Options Formatted", makeCredentialOptions);
Swal.fire({
title: 'Registering...',
text: 'Tap your security key to finish registration.',
imageUrl: "/images/securitykey.min.svg",
showCancelButton: true,
showConfirmButton: false,
focusConfirm: false,
focusCancel: false
});
//Part 2
console.log("Creating PublicKeyCredential...");
let newCredential;
try {
newCredential = await navigator.credentials.create({
publicKey: makeCredentialOptions
});
} catch (e) {
var msg = "Could not create credentials in browser. Probably because the username is already registered with your authenticator. Please change username or authenticator."
console.error(msg, e);
showErrorAlert(msg, e);
}
console.log("PublicKeyCredential Created", newCredential);
try {
registerNewCredential(newCredential);
} catch (e) {
showErrorAlert(err.message ? err.message : err);
}
});
async function fetchMakeCredentialOptions(formData) {
//formData.set('authType', 'platform')
// formData.set('authType', 'cross-platform');
let response = await fetch('/api/auth/makeCredentialOptions', {
method: 'POST', // or 'PUT'
body: formData, // data can be `string` or {object}!
headers: {
'Accept': 'application/json'
}
});
let data = await response.json();
return data;
}
// This should be used to verify the auth data with the server
async function registerNewCredential(newCredential) {
// Move data into Arrays incase it is super long
let attestationObject = new Uint8Array(newCredential.response.attestationObject);
let clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
let rawId = new Uint8Array(newCredential.rawId);
const data = {
id: newCredential.id,
rawId: coerceToBase64Url(rawId),
type: newCredential.type,
extensions: newCredential.getClientExtensionResults(),
response: {
AttestationObject: coerceToBase64Url(attestationObject),
clientDataJson: coerceToBase64Url(clientDataJSON)
}
};
let response;
try {
response = await registerCredentialWithServer(data);
} catch (e) {
showErrorAlert(e);
}
console.log("Credential Object", response);
// show error
if (response.status !== "ok") {
console.log("Error creating credential");
console.log(response.errorMessage);
showErrorAlert(response.errorMessage);
return;
}
// show success
Swal.fire({
title: 'Registration Successful!',
text: 'You\'ve registered successfully.',
type: 'success',
timer: 2000
});
// redirect to dashboard?
//window.location.href = "/dashboard/" + state.user.displayName;
}
async function registerCredentialWithServer(formData) {
let response = await fetch('/api/auth/makeCredential', {
method: 'POST', // or 'PUT'
body: JSON.stringify(formData), // data can be `string` or {object}!
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
let data = await response.json();
return data;
}
C# Code:
[HttpPost]
[Route("makeCredentialOptions")]
[Produces("application/json")]
public IActionResult MakeCredentialOptions([FromForm] string userid,
[FromForm] string userpass,
[FromForm] string attType,
[FromForm] string authType,
[FromForm] bool requireResidentKey,
[FromForm] string userVerification)
{
try
{
if (string.IsNullOrEmpty(userid))
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = "user id null." });
}
if (string.IsNullOrEmpty(userpass))
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = "userpass null." });
}
// 1. Get user from DB by username
var user = AuthStorageUtil.GetUserById(userid);
if (user == null)
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = "User Not Existed." });
}
if (AuthStorageUtil.GetMD5(System.Text.Encoding.UTF8.GetBytes(userpass)) != user.Pass)
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = "User Data Error." });
}
// 2. Get user existing keys by username
//var existingKeys = DemoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList();
var existingKeys = AuthStorageUtil.GetCredentialsByUser(user.Name).Select(e => e.Descriptor).ToList(); ;
// 3. Create options
var authenticatorSelection = new AuthenticatorSelection
{
RequireResidentKey = requireResidentKey,
UserVerification = userVerification.ToEnum()
};
if (!string.IsNullOrEmpty(authType))
authenticatorSelection.AuthenticatorAttachment = authType.ToEnum();
var exts = new AuthenticationExtensionsClientInputs()
{
Extensions = true,
UserVerificationMethod = true,
};
var options = _fido2.RequestNewCredential(user, existingKeys, authenticatorSelection, attType.ToEnum(), exts);
// 4. Temporarily store options, session/in-memory cache/redis/db
HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson());
// 5. return options to client
return Ok(options);
}
catch (Exception e)
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = e.Message });
// return Problem();
}
}
[HttpPost]
[Route("makeCredential")]
[Produces("application/json")]
public async Task MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse, CancellationToken cancellationToken)
{
try
{
// 1. get the options we sent the client
var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions");
var options = CredentialCreateOptions.FromJson(jsonOptions);
// 2. Create callback so that lib can verify credential id is unique to this user
//if (AuthStorageUtil.IsCredentialIdExisted(attestationResponse.Id)) {
// return Ok(new CredentialMakeResult(status: "error", errorMessage:"Credentail Existed", result: null));
//}
IsCredentialIdUniqueToUserAsyncDelegate callback = static async (args, cancellationToken) =>
{
// var users = await DemoStorage.GetUsersByCredentialIdAsync(args.CredentialId, cancellationToken);
if (AuthStorageUtil.IsCredentialIdExisted(args.CredentialId))
return false;
return true;
};
// 2. Verify and make the credentials
var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback, cancellationToken: cancellationToken);
// 3. Store the credentials in db
var storeCredentail = new StoredCredential
{
Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
PublicKey = success.Result.PublicKey,
UserHandle = success.Result.User.Id,
SignatureCounter = success.Result.Counter,
CredType = success.Result.CredType,
RegDate = DateTime.Now,
AaGuid = success.Result.Aaguid
};
AuthStorageUtil.SaveStoredCredential(success.Result.User.Name, storeCredentail);
// 4. return "ok" to the client
return Ok(success);
}
catch (Exception e)
{
return Ok(new CredentialMakeResult(status: "error", errorMessage: FormatException(e), result: null));
}
}
private string FormatException(Exception e)
{
return string.Format("{0}{1}", e.Message, e.InnerException != null ? " (" + e.InnerException.Message + ")" : "");
}
result:
如果你是用 Chrome 也可以使用手機驗證
這邊有說不清楚的地方這邊有所有的 source code 您可以直接下載參考
Demo Source Code: https://github.com/donma/FIDO2SimpleSample
Online Sample: https://dmauth.azurewebsites.net/
參考文獻:
https://webcodingcenter.com/web-apis/Web-Authentication-(WebAuthn)
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
https://fidoalliance.org/specs/fido-v2.0-rd-20170927/fido-client-to-authenticator-protocol-v2.0-rd-20170927.html
https://stackoverflow.com/questions/59799729/webauthn-getting-the-credentialpublickey-length
https://stackoverflow.com/questions/8936984/uint8array-to-string-in-javascript
https://stackoverflow.com/questions/54045911/webauthn-byte-length-of-the-credential-public-key
重點參考:
https://blog.techbridge.cc/2019/08/17/webauthn-intro/
https://www.wenwoha.com/blog_detail-150.html
https://www.readfog.com/a/1648324634492375040
https://github.com/passwordless-lib/fido2-net-lib
範例改寫參考:
https://fido2-net-lib.passwordless.dev/