624 lines
27 KiB
JavaScript
624 lines
27 KiB
JavaScript
const DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION = 'fido2_user_session'
|
||
const DFIDO2_LIB_LOCALSTG_NAME_REGISTERED = 'dfido2_lib_registered'
|
||
const DFIDO2_LIB_LOCALSTG_NAME_SVR_URL = 'dfido2_lib_svr_url'
|
||
|
||
/** ===APIs=== */
|
||
|
||
if(!localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL)){
|
||
localStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL, 'https://fido2.amipro.me')
|
||
}
|
||
|
||
function setFidoServerURL(url){
|
||
localStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL, url);
|
||
}
|
||
|
||
function canTryAutoAuthentication(){
|
||
//const session_text = localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION)
|
||
//alert('canTryAuth:'+session_text+"|"+(null != localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_REGISTERED)))
|
||
return null != localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_REGISTERED)
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {String} userId
|
||
* @param {String} rpId
|
||
*/
|
||
async function authenticateFido2(userId = null, rpId = null) {
|
||
var result
|
||
result = await doAssertion(userId, rpId);
|
||
if(result.status === 'ok'){
|
||
sessionStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION,
|
||
JSON.stringify({session:result.session, uid:result.username}))
|
||
localStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_REGISTERED, new Date());
|
||
}
|
||
return result
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {String} userId
|
||
* @param {String} rpId
|
||
*/
|
||
async function registerFido2(userId, userDisplay, rpId = null) {
|
||
if (isWebAuthnSupported()) {
|
||
const result = await doAttestation(userId, userDisplay, rpId);
|
||
if(result.status === 'ok'){
|
||
localStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_REGISTERED, new Date());
|
||
sessionStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION, JSON.stringify({session:result.session, uid:result.username}))
|
||
}
|
||
return result
|
||
}else return {status:'failed', errorMessage: getI18NErrorMessage('Fido2LibErr101:')}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {String} rpId
|
||
* @returns
|
||
*/
|
||
async function listUserDevicesFido2(rpId = null) {
|
||
try {
|
||
const session_text = sessionStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION)
|
||
if(!session_text) return {status:'ok', devices:[]}
|
||
|
||
const session_data = JSON.parse(session_text)
|
||
|
||
let req = {session: session_data.session}
|
||
if (rpId && 0 < rpId.length) {
|
||
req.rp = { id: rpId };
|
||
}
|
||
|
||
const response = await fetch(localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL) + "/usr/dvc/lst", {
|
||
method: "POST",
|
||
cache: "no-cache",
|
||
headers: {
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify(req)
|
||
});
|
||
const resp = await response.json();
|
||
if ('ok' === resp.status && resp.session === session_data.session) {
|
||
return {status:'ok', devices:resp.devices}
|
||
} else {
|
||
return {status:'failed', errorMessage: resp.errorMessage}
|
||
}
|
||
} catch (err) {
|
||
console.log(err)
|
||
let msg = err.message ? err.message : err;
|
||
//console.error("Assertion err: ", err);
|
||
var errRtn = {status:'failed', errorMessage: msg};
|
||
if(err.name) errRtn.name = err.name
|
||
return errRtn;
|
||
}
|
||
}
|
||
|
||
async function delUserDeviceFido2(device_id, rpId = null) {
|
||
try {
|
||
const session_text = sessionStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION)
|
||
const session_data = JSON.parse(session_text)
|
||
|
||
let req = {session: session_data.session, device_id: device_id}
|
||
if (rpId && 0 < rpId.length) {
|
||
req.rp = { id: rpId };
|
||
}
|
||
|
||
const response = await fetch(localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL) + "/usr/dvc/rm", {
|
||
method: "POST",
|
||
cache: "no-cache",
|
||
headers: {
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify(req)
|
||
});
|
||
const resp = await response.json();
|
||
|
||
return resp
|
||
} catch (err) {
|
||
console.log(err)
|
||
let msg = err.message ? err.message : err;
|
||
//console.error("Assertion err: ", err);
|
||
var errRtn = {status:'failed', errorMessage: msg};
|
||
if(err.name) errRtn.name = err.name
|
||
return errRtn;
|
||
}
|
||
}
|
||
|
||
function getSessionId() {
|
||
var rtn = null
|
||
try {
|
||
const session_text = sessionStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION)
|
||
if(session_text){
|
||
const session_data = JSON.parse(session_text)
|
||
|
||
rtn = session_data.session
|
||
}
|
||
|
||
return rtn
|
||
} catch (err) {
|
||
console.log(err)
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function validSession(rpId = null) {
|
||
try {
|
||
const session_text = sessionStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION)
|
||
if(!session_text) return false
|
||
|
||
const session_data = JSON.parse(session_text)
|
||
|
||
let req = {session: session_data.session}
|
||
if (rpId && 0 < rpId.length) {
|
||
req.rp = { id: rpId };
|
||
}
|
||
|
||
const response = await fetch(localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL) + "/usr/validsession", {
|
||
method: "POST",
|
||
cache: "no-cache",
|
||
headers: {
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify(req)
|
||
});
|
||
const resp = await response.json();
|
||
|
||
return resp.status === 'ok'
|
||
} catch (err) {
|
||
console.log(err)
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function logoutFido2UserSession(){
|
||
const session_text = sessionStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION)
|
||
if(!session_text) return
|
||
|
||
const session_data = JSON.parse(session_text)
|
||
let req = {session: session_data['session'], username: session_data['uid']}
|
||
const response = await fetch(localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL) + "/usr/delsession", {
|
||
method: "POST",
|
||
cache: "no-cache",
|
||
headers: {
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify(req)
|
||
});
|
||
|
||
sessionStorage.removeItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION);
|
||
}
|
||
|
||
async function getRegistrationUser(reg_session_id){
|
||
try {
|
||
let req = {session_id: reg_session_id}
|
||
const response = await fetch(localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL) + "/reg/username", {
|
||
method: "POST",
|
||
cache: "no-cache",
|
||
headers: {
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify(req)
|
||
});
|
||
const resp = await response.json();
|
||
|
||
return resp.username
|
||
} catch (err) {
|
||
console.log(err)
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function errProcessFido2(result){
|
||
alert(errMessageFido2(result));
|
||
}
|
||
|
||
function errMessageFido2(result){
|
||
var rtn
|
||
if(result.errCode && fido2LibErrCodes.unknown != result.errCode ){
|
||
switch (result.errCode){
|
||
case fido2LibErrCodes.user_canceled:
|
||
rtn=getI18NErrorMessage('Fido2LibErr102:');
|
||
break;
|
||
case fido2LibErrCodes.timeout:
|
||
rtn=getI18NErrorMessage('Fido2LibErr103:');
|
||
break;
|
||
default:
|
||
rtn=result.errorMessage?result.errorMessage:getI18NErrorMessage('Fido2LibErr104:');
|
||
}
|
||
}else if(result.name && "InvalidStateError" === result.name){
|
||
rtn=getI18NErrorMessage('Fido2LibErr105:');
|
||
}else if(result.errorMessage){
|
||
const msg = getI18NErrorMessage(result.errorMessage);
|
||
rtn=msg?msg:result.errorMessage;
|
||
}else{
|
||
rtn=getI18NErrorMessage(i18n_messages, 'Fido2LibErr104:');
|
||
}
|
||
|
||
return rtn;
|
||
}
|
||
|
||
const fido2LibErrCodes = {
|
||
user_canceled : -101,
|
||
timeout : -102,
|
||
unknown : -999
|
||
}
|
||
|
||
const errMsgs = new Map();
|
||
const fido2LibErrMsgLanguages = {
|
||
english: 'en-US',
|
||
japanese: 'ja',
|
||
chinese_cn: 'zh-CN',
|
||
//chinese_tw: 'zh-TW',
|
||
}
|
||
errMsgs.set(fido2LibErrMsgLanguages.english, new Map());
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr101:', 'Unregistered enterprise authenticator aaguid!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr102:', 'Unable to authenticate with a unique device binding key from another device!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr103:', 'Unable to verify signature!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr104:', 'Key not found!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr105:', 'Username does not exist!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr106:', 'Unique Device ID is null!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr107:', '/attestation/result request body has no ID field!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr108:', 'ID field is not Base64Url encoded in /attestation/result request body!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr109:', '/attestation/result request body has no TYPE field!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr110:', 'TYPE field is not a DOMString in /attestation/result request body!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr111:', 'The TYPE field is not a public key in the /attestation/result request body!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr112:', 'ID field is not a DOMString in /attestation/result request body!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr115:', 'authenticatorData not found!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr116:', 'authenticatorData is not base64 URL encoded!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr117:', 'Signature not found!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr118:', 'Signature is not base64 URL encoded!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr119:', 'No user session!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr120:', 'User has reached the device limit!');
|
||
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr101:', 'Your browser does not support FIDO2.');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr102:', 'The user canceled.');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr103:', 'The process timeout.');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr104:', 'System error.');
|
||
errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr105:', 'The same authenticator cannot be registered again.');
|
||
|
||
errMsgs.set(fido2LibErrMsgLanguages.japanese, new Map());
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr101:', '登録されていないエンタープライズ認証デバイス aaguid!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr102:', '別のデバイスからの一意のデバイス バインド キーで認証できません!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr103:', '署名を認証できません!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr104:', 'キーが見つかりません!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr105:', 'ユーザー名は存在しません!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr106:', '固有のデバイス ID が null です!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr107:', '/attestation/result request の本文に ID フィールドがありません!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr108:', 'ID フィールドは、/attestation/result リクエストの本文でエンコードされた Base64Url ではありません!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr109:', '/attestation/result リクエストのボディに TYPE フィールドがありません!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr110:', 'TYPE フィールドは、/attestation/result リクエストの本文の DOMString ではありません!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr111:', 'TYPE フィールドは、/attestation/result リクエストの本文の公開鍵ではありません!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr112:', 'ID フィールドは、/attestation/result リクエストの本文の DOMString ではありません!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr115:', 'authenticatorData が見つかりません!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr116:', 'authenticatorData は base64 URL エンコードされていません!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr117:', '署名が見つかりません!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr118:', '署名は base64 URL エンコードされていません!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr119:', 'ユーザーセッションがありません!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr120:', 'ユーザーはデバイスの制限数に達しました!');
|
||
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr101:', 'お使いのブラウザは FIDO2 をサポートしていません。');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr102:', 'ユーザーがキャンセルしました。');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr103:', 'プロセスがタイムアウトしました。');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr104:', 'システムエラー。');
|
||
errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr105:', '同じ認証デバイスを再登録することはできません。');
|
||
|
||
errMsgs.set(fido2LibErrMsgLanguages.chinese_cn, new Map());
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr101:', '未注册的企业认证器 aaguid!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr102:', '无法使用来自其他设备的唯一设备绑定密钥进行身份验证!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr103:', '无法验证签名!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr104:', '认证Key未找到!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr105:', '用户名不存在!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr106:', 'Unique Device ID 为 null!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr107:', '/attestation/result请求体没有ID字段!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr108:', 'ID字段不是/attestation/result请求体中编码的Base64Url!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr109:', '/attestation/result请求体没有TYPE字段!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr110:', '/attestation/result 请求正文中的 TYPE 字段不是 DOMString!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr111:', 'TYPE字段不是/attestation/result请求体中的公钥!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr112:', 'ID 字段不是 /attestation/result 请求体中的 DOMString!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr115:', 'authenticatorData 未找到!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr116:', 'authenticatorData 不是 base64 URL 编码!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr117:', '未找到签名!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr118:', '签名不是 base64 URL 编码!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr119:', '未建立用户会话!');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr120:', '用户已达到设备限制数!');
|
||
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr101:', '您的浏览器不支持FIDO2.');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr102:', '用户取消了操作。');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr103:', '操作超时。');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr104:', '系统错误。');
|
||
errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr105:', '无法再次注册相同的认证器。');
|
||
|
||
/**
|
||
*
|
||
* @param {String} errorMessage
|
||
* @param {errMsgLanguages} language
|
||
*/
|
||
function getI18NErrorMessage(errorMessage, language = null){
|
||
var lang = language ? language : window.navigator.language
|
||
var msgs = errMsgs.get(lang)
|
||
if(!msgs)msgs = errMsgs.get(fido2LibErrMsgLanguages.english)
|
||
if(errorMessage){
|
||
const msgHeader = 0<errorMessage.indexOf(':')?errorMessage.substring(0, errorMessage.indexOf(':')):errorMessage
|
||
const msg = msgs.get(msgHeader+":")
|
||
return msg?msgHeader+":"+msg:errorMessage;
|
||
} else return errorMessage;
|
||
}
|
||
|
||
/** ===utils=== */
|
||
|
||
function isWebAuthnSupported() {
|
||
if (window.PublicKeyCredential) {
|
||
return true;
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function makePublicKey(attOptsResp) {
|
||
if (attOptsResp.excludeCredentials) {
|
||
attOptsResp.excludeCredentials = attOptsResp.excludeCredentials.map(
|
||
function (cred) {
|
||
cred.id = _base64ToArrayBuffer(_fromBase64URL(cred.id));
|
||
cred.transports = ["internal", "usb", "ble", "nfc"];
|
||
return cred;
|
||
}
|
||
);
|
||
|
||
//console.log("Attestation Options:");
|
||
//console.log(attOptsResp);
|
||
}
|
||
|
||
const keys = {
|
||
publicKey: {
|
||
attestation: attOptsResp.attestation,
|
||
authenticatorSelection: attOptsResp.authenticatorSelection,
|
||
excludeCredentials: attOptsResp.excludeCredentials,
|
||
rp: attOptsResp.rp,
|
||
user: {
|
||
id: _stringToArrayBuffer(attOptsResp.user.id), //_base64ToArrayBuffer(_fromBase64URL(attOptsResp.user.id)),
|
||
name: attOptsResp.user.name,
|
||
displayName: attOptsResp.user.displayName,
|
||
},
|
||
pubKeyCredParams: attOptsResp.pubKeyCredParams,
|
||
timeout: attOptsResp.timeout,
|
||
challenge: _base64ToArrayBuffer(_fromBase64URL(attOptsResp.challenge)),
|
||
}
|
||
}
|
||
return keys;
|
||
}
|
||
|
||
async function doAttestation(username, displayName, rpId, userVerification = 'preferred') {
|
||
var process_time_limit = Number.MAX_SAFE_INTEGER
|
||
try {
|
||
const attestationOptions = {
|
||
username: username,
|
||
displayName: encodeURIComponent(displayName),
|
||
authenticatorSelection: {
|
||
//authenticatorAttachment: "platform",
|
||
userVerification: userVerification,
|
||
requireResidentKey: false,
|
||
},
|
||
//attestation: "none",
|
||
};
|
||
|
||
if (rpId && 0 < rpId.length) {
|
||
attestationOptions.rp = { id: rpId }
|
||
}
|
||
|
||
const svrUrl = localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL)
|
||
const response = await fetch(svrUrl + "/attestation/options", {
|
||
method: "POST",
|
||
cache: "no-cache",
|
||
headers: {
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify(attestationOptions),
|
||
});
|
||
|
||
const resp = await response.json();
|
||
if (resp.status === "failed") {
|
||
return {status:'failed', errorMessage: resp.errorMessage}
|
||
} else {
|
||
process_time_limit = (new Date()).getTime() + resp.timeout;
|
||
const res = await navigator.credentials.create(makePublicKey(resp));
|
||
if (res) {
|
||
let attResult = {
|
||
id: res.id,
|
||
rawId: _toBase64URL(btoa(_bufferToString(res.rawId)))
|
||
,
|
||
type: "public-key",
|
||
response: {
|
||
clientDataJSON: _toBase64URL(btoa(_bufferToString(res.response.clientDataJSON)))
|
||
,
|
||
attestationObject: _toBase64URL(btoa(_bufferToString(res.response.attestationObject)))
|
||
,
|
||
},
|
||
};
|
||
|
||
// Capture transports if available (WebAuthn Level 2+)
|
||
if (res.response.getTransports) {
|
||
attResult.transports = res.response.getTransports();
|
||
}
|
||
|
||
const result = await fetch(localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL) + "/attestation/result", {
|
||
method: "POST",
|
||
cache: "no-cache",
|
||
headers: {
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify(attResult),
|
||
});
|
||
|
||
const respResult = await result.json();
|
||
if (respResult) {
|
||
if (respResult.status === "ok") {
|
||
return respResult
|
||
} else {
|
||
return {status:'failed', errorMessage: respResult.errorMessage}
|
||
}
|
||
} else {
|
||
return {status:'failed', errorMessage: 'Fido2LibErr999:Svr result error'}
|
||
}
|
||
} else {
|
||
return {status:'failed', errorMessage: 'Fido2LibErr999:Undefined Result'};
|
||
}
|
||
}
|
||
} catch (err) {
|
||
var errRtn = {status:'failed', errorMessage: err.message};
|
||
if(err.name) errRtn.name = err.name
|
||
if(err.name && 'NotAllowedError' === err.name){
|
||
const nowtm = (new Date()).getTime()
|
||
if(nowtm > process_time_limit){
|
||
errRtn.errCode = fido2LibErrCodes.timeout
|
||
}else{
|
||
errRtn.errCode = fido2LibErrCodes.user_canceled
|
||
}
|
||
}else errRtn.errCode = fido2LibErrCodes.unknown
|
||
|
||
return errRtn;
|
||
}
|
||
}
|
||
|
||
async function doAssertion(username = null, rpId = null, userVerification = 'preferred') {
|
||
var process_time_limit = Number.MAX_SAFE_INTEGER
|
||
try {
|
||
let authnOptions;
|
||
/*
|
||
Tested the codes below, but most browsers cannot find correct account when username is null.
|
||
if (!username || 0 === username.length) {
|
||
authnOptions = {
|
||
authenticatorSelection: {
|
||
//authenticatorAttachment: "platform",
|
||
userVerification: userVerification
|
||
}
|
||
};
|
||
} else {
|
||
authnOptions = {
|
||
username: username,
|
||
authenticatorSelection: {
|
||
//authenticatorAttachment: "platform",
|
||
userVerification: userVerification
|
||
}
|
||
};
|
||
}*/
|
||
authnOptions = {
|
||
username: username,
|
||
authenticatorSelection: {
|
||
//authenticatorAttachment: "platform",
|
||
userVerification: userVerification
|
||
}
|
||
};
|
||
|
||
if (rpId && 0 < rpId.length) {
|
||
authnOptions.rp = { id: rpId };
|
||
}
|
||
|
||
const response = await fetch(localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL) + "/assertion/options", {
|
||
method: "POST",
|
||
cache: "no-cache",
|
||
headers: {
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify(authnOptions)
|
||
});
|
||
const resp = await response.json();
|
||
if ('ok' === resp.status) {
|
||
process_time_limit = (new Date()).getTime() + resp.timeout;
|
||
resp.allowCredentials = resp.allowCredentials || [];
|
||
let mappedAllowCreds = resp.allowCredentials.map(x => {
|
||
return {
|
||
id: _base64ToArrayBuffer(_fromBase64URL(x.id)),
|
||
type: x.type,
|
||
transports: x.transports // can set like ['internal', 'usb'] to override server settings
|
||
};
|
||
});
|
||
|
||
const cred = await navigator.credentials.get({
|
||
publicKey: {
|
||
challenge: _base64ToArrayBuffer(_fromBase64URL(resp.challenge)),
|
||
timeout: resp.timeout,
|
||
rpId: resp.rpId,
|
||
userVerification: resp.userVerification,
|
||
allowCredentials: mappedAllowCreds
|
||
}
|
||
});
|
||
|
||
if (cred) {
|
||
let authRequest = {
|
||
id: cred.id,
|
||
rawId: Array.from(new Uint8Array(cred.rawId)),
|
||
type: cred.type,
|
||
response: {
|
||
authenticatorData: _toBase64URL(btoa(_bufferToString(cred.response.authenticatorData))),
|
||
clientDataJSON: _toBase64URL(btoa(_bufferToString(cred.response.clientDataJSON))),
|
||
signature: _toBase64URL(btoa(_bufferToString(cred.response.signature))),
|
||
userHandle: _toBase64URL(btoa(_bufferToString(cred.response.userHandle))) //_toBase64URL(btoa(_bufferToString(cred.response.userHandle)))
|
||
}
|
||
};
|
||
const res = await fetch(localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL) + "/assertion/result", {
|
||
method: "POST",
|
||
cache: "no-cache",
|
||
headers: {
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify(authRequest)
|
||
});
|
||
const result = await res.json();
|
||
if (result.status === 'ok') {
|
||
return result
|
||
} else {
|
||
return {status:'failed', errorMessage: result.errorMessage}
|
||
}
|
||
} else {
|
||
return {status:'failed', errorMessage: 'Fido2LibErr999:Undefined Result'};
|
||
}
|
||
} else {
|
||
return {status:'failed', errorMessage: resp.errorMessage}
|
||
}
|
||
} catch (err) {
|
||
var errRtn = {status:'failed', errorMessage: err.message};
|
||
if(err.name) errRtn.name = err.name
|
||
if(err.name && 'NotAllowedError' === err.name){
|
||
const nowtm = (new Date()).getTime()
|
||
if(nowtm > process_time_limit){
|
||
errRtn.errCode = fido2LibErrCodes.timeout
|
||
}else{
|
||
errRtn.errCode = fido2LibErrCodes.user_canceled
|
||
}
|
||
}else errRtn.errCode = fido2LibErrCodes.unknown
|
||
|
||
return errRtn;
|
||
}
|
||
}
|
||
|
||
function _toBase64URL(s) {
|
||
return (s = (s = (s = s.split("=")[0]).replace(/\+/g, "-")).replace(/\//g, "_"));
|
||
}
|
||
|
||
function _base64ToArrayBuffer(base64) {
|
||
var binary_string = window.atob(base64);
|
||
var len = binary_string.length;
|
||
var bytes = new Uint8Array(len);
|
||
for (var i = 0; i < len; i++) {
|
||
bytes[i] = binary_string.charCodeAt(i);
|
||
}
|
||
return bytes;
|
||
}
|
||
|
||
function _stringToArrayBuffer(src) {
|
||
return (new Uint8Array([].map.call(src, function (c) {
|
||
return c.charCodeAt(0)
|
||
}))).buffer;
|
||
}
|
||
|
||
function _fromBase64URL(s) {
|
||
var chk = (s = s.replace(/-/g, "+").replace(/_/g, "/")).length % 4;
|
||
if (chk) {
|
||
if (1 === chk) throw new Error("Base64url string is wrong.");
|
||
s += new Array(5 - chk).join("=");
|
||
}
|
||
return s;
|
||
}
|
||
|
||
function _bufferToString(s) {
|
||
return new Uint8Array(s).reduce((s, e) => s + String.fromCodePoint(e), "");
|
||
}
|