接續上篇文章 初探 WebAuth 簡單範例 (註冊+綁定裝置實作) ,這一篇我們就是要繼續實作登入+驗證裝置的部分

接續之前已經註冊用戶+綁定裝置了,今天這一篇會先登入,確認身分,之後在 javscript 端 呼叫 navigator.credentials.get({ publicKey: makeAssertionOptions })
將裝置資料傳回後進行驗證
登入,主要先確定帳號密碼是不是在資料庫中(範例我只是寫入檔案系統)
Javascript code:
$('#login').click(async function (event) {
event.preventDefault();
var userid = $('#userId').val();
var userpass = $('#userPass').val();
// prepare form post data
var data = new FormData();
data.append('userid', userid);
data.append('userpass', userpass);
// send to server for registering
let makeAssertionOptions;
try {
var res = await fetch('/api/auth/assertionOptions', {
method: 'POST',
body: data, // data can be `string` or {object}!
headers: {
'Accept': 'application/json'
}
});
makeAssertionOptions = await res.json();
} catch (e) {
showErrorAlert("Request to server failed", e);
}
console.log("Assertion Options Object", makeAssertionOptions);
// show options error to user
if (makeAssertionOptions.status !== "ok") {
console.log("Error creating assertion options");
console.log(makeAssertionOptions.errorMessage);
showErrorAlert(makeAssertionOptions.errorMessage);
return;
}
// todo: switch this to coercebase64
const challenge = makeAssertionOptions.challenge.replace(/-/g, "+").replace(/_/g, "/");
makeAssertionOptions.challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0));
// fix escaping. Change this to coerce
makeAssertionOptions.allowCredentials.forEach(function (listItem) {
var fixedId = listItem.id.replace(/\_/g, "/").replace(/\-/g, "+");
listItem.id = Uint8Array.from(atob(fixedId), c => c.charCodeAt(0));
});
console.log("Assertion options", makeAssertionOptions);
Swal.fire({
title: 'Logging In...',
text: 'Tap your security key to login.',
imageUrl: "/images/securitykey.min.svg",
showCancelButton: true,
showConfirmButton: false,
focusConfirm: false,
focusCancel: false
});
// ask browser for credentials (browser will ask connected authenticators)
let credential;
try {
credential = await navigator.credentials.get({ publicKey: makeAssertionOptions })
} catch (err) {
showErrorAlert(err.message ? err.message : err);
}
try {
await verifyAssertionWithServer(credential);
} catch (e) {
showErrorAlert("Could not verify assertion", e);
}
});
/**
* Sends the credential to the the FIDO2 server for assertion
* @param {any} assertedCredential
*/
async function verifyAssertionWithServer(assertedCredential) {
// Move data into Arrays incase it is super long
let authData = new Uint8Array(assertedCredential.response.authenticatorData);
let clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON);
let rawId = new Uint8Array(assertedCredential.rawId);
let sig = new Uint8Array(assertedCredential.response.signature);
const data = {
id: assertedCredential.id,
rawId: coerceToBase64Url(rawId),
type: assertedCredential.type,
extensions: assertedCredential.getClientExtensionResults(),
response: {
authenticatorData: coerceToBase64Url(authData),
clientDataJson: coerceToBase64Url(clientDataJSON),
signature: coerceToBase64Url(sig)
}
};
let response;
try {
let res = await fetch("api/auth/makeAssertion", {
method: 'POST', // or 'PUT'
body: JSON.stringify(data), // data can be `string` or {object}!
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
response = await res.json();
} catch (e) {
showErrorAlert("Request to server failed", e);
throw e;
}
console.log("Assertion Object", response);
// show error
if (response.status !== "ok") {
console.log("Error doing assertion");
console.log(response.errorMessage);
showErrorAlert(response.errorMessage);
return;
}
// show success message
await Swal.fire({
title: 'Logged In!',
text: 'You\'re logged in successfully.',
type: 'success',
timer: 2000
});
// redirect to dashboard to show keys
//window.location.href = "/";
}
之後透過 ajax 到 webapi 進行驗證後呼叫 navigator.credentials.get({ publicKey: makeAssertionOptions }) ,並且傳回給
server 進行驗證是不是有該
clientResponse.Id
C# code:
[HttpPost]
[Route("assertionOptions")]
[Produces("application/json")]
public ActionResult AssertionOptionsPost([FromForm] string userid, [FromForm] string userpass, [FromForm] string userVerification)
{
try
{
var existingCredentials = new List();
if (!string.IsNullOrEmpty(userid))
{
// 1. Get user from DB
var user = AuthStorageUtil.GetUserById(userid) ?? throw new ArgumentException("user was not registered");
if (AuthStorageUtil.GetMD5(System.Text.Encoding.UTF8.GetBytes(userpass)) != user.Pass) {
throw new ArgumentException("user data error");
}
// 2. Get registered credentials from database
existingCredentials = AuthStorageUtil.GetCredentialsByUser(userid).Select(c => c.Descriptor).ToList();
}
var exts = new AuthenticationExtensionsClientInputs()
{
UserVerificationMethod = true
};
// 3. Create options
var uv = string.IsNullOrEmpty(userVerification) ? UserVerificationRequirement.Discouraged : userVerification.ToEnum();
var options = _fido2.GetAssertionOptions(
existingCredentials,
uv,
exts
);
// 4. Temporarily store options, session/in-memory cache/redis/db
HttpContext.Session.SetString("fido2.assertionOptions", options.ToJson());
// 5. Return options to client
// return Json(options);
return Ok(options);
}
catch (Exception e)
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = e.Message });
// return Json(new AssertionOptions { Status = "error", ErrorMessage = FormatException(e) });
}
}
[HttpPost]
[Route("makeAssertion")]
[Produces("application/json")]
public async Task MakeAssertion([FromBody] AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken)
{
try
{
// 1. Get the assertion options we sent the client
var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions");
var options = AssertionOptions.FromJson(jsonOptions);
// 2. Get registered credential from database
//var creds = AuthStorageUtil.GetStoreCredentialByUserId(, clientResponse.Id) ?? throw new Exception("Unknown credentials");
var userId= AuthStorageUtil.GetUserIdByCredentialsById(clientResponse.Id);
if (userId == null) {
throw new Exception("Unknown credentials");
}
var creds = AuthStorageUtil.GetStoreCredentialByUserId(userId, clientResponse.Id);
if (creds == null) {
throw new Exception("Unknown credentials");
}
// 3. Get credential counter from database
var storedCounter = creds.SignatureCounter;
// 4. Create callback to check if userhandle owns the credentialId
//IsUserHandleOwnerOfCredentialIdAsync callback = static async (args, cancellationToken) =>
//{
// var storedCreds = await DemoStorage.GetCredentialsByUserHandleAsync(args.UserHandle, cancellationToken);
// return storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId));
// return true;
//};
// 5. Make the assertion
var res = await _fido2.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, null, cancellationToken: cancellationToken);
// 6. Store the updated counter
// DemoStorage.UpdateCounter(res.CredentialId, res.Counter);
// 7. return OK to client
return Ok(res);
}
catch (Exception e)
{
//return Json(new AssertionVerificationResult { Status = "error", ErrorMessage = FormatException(e) });
return Ok(new AssertionVerificationResult { Status = "error", ErrorMessage = FormatException(e) });
}
}
目前寫到這到一個段落,其實中間有許多格式的部分我也略過沒有敘述,當然你也可以詳細的鑽下去 或是可以參考這一篇 https://blog.techbridge.cc/2019/08/17/webauthn-intro/
這一篇寫得非常的詳細,感謝各位前輩留下的東西讓我實作上面也比較方便,如果有不懂的細節就直接看我的 code 吧,也可以幫助您快速實作
當然如果您覺得我寫得很不清不楚這邊有最原始的程式碼 https://fido2-net-lib.passwordless.dev/ 應該也可以幫助到您,因為這距離我之前寫好有點時間了
所以很多東西也沒有說得很仔細,不過就先筆記一下,避免之後自己忘記
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/