Init Gitea
This commit is contained in:
616
Resource/assets/dfido2-lib.js
Normal file
616
Resource/assets/dfido2-lib.js
Normal file
@@ -0,0 +1,616 @@
|
||||
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/dvs/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/dvs/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)))
|
||||
,
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
/*if (!username) {
|
||||
authnOptions = {
|
||||
authenticatorSelection: {
|
||||
//authenticatorAttachment: "platform",
|
||||
userVerification: "discouraged"
|
||||
}
|
||||
};
|
||||
} else {
|
||||
authnOptions = {
|
||||
username: username,
|
||||
authenticatorSelection: {
|
||||
//authenticatorAttachment: "platform",
|
||||
userVerification: "preferred"
|
||||
}
|
||||
};
|
||||
}*/
|
||||
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), "");
|
||||
}
|
||||
42
Resource/config/services.yaml
Normal file
42
Resource/config/services.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
eccube:
|
||||
rate_limiter:
|
||||
plg_customer_2fa_device_auth_input_onetime:
|
||||
# 実行するルーティングを指定します。
|
||||
route: plg_customer_2fa_device_auth_input_onetime
|
||||
# 実行するmethodを指定します。デフォルトはPOSTです。
|
||||
method: [ 'POST' ]
|
||||
# スロットリングの制御方法を設定します。ip・customerを指定できます。
|
||||
type: [ 'ip', 'customer' ]
|
||||
# 試行回数を設定します。
|
||||
limit: 5
|
||||
# インターバルを設定します。
|
||||
interval: '30 minutes'
|
||||
plg_customer_2fa_device_auth_send_onetime:
|
||||
# 実行するルーティングを指定します。
|
||||
route: plg_customer_2fa_device_auth_send_onetime
|
||||
# 実行するmethodを指定します。デフォルトはPOSTです。
|
||||
method: [ 'POST' ]
|
||||
# スロットリングの制御方法を設定します。ip・customerを指定できます。
|
||||
type: [ 'ip', 'customer' ]
|
||||
# 試行回数を設定します。
|
||||
limit: 5
|
||||
# インターバルを設定します。
|
||||
interval: '30 minutes'
|
||||
device_auth_request_email:
|
||||
route: ~
|
||||
limit: 10
|
||||
interval: '30 minutes'
|
||||
|
||||
parameters:
|
||||
env(PLUGIN_ECCUBE_PASSKEYS_CUSTOMER_COOKIE_NAME): 'plugin_eccube_customer_passkeys'
|
||||
env(PLUGIN_ECCUBE_PASSKEYS_CUSTOMER_EXPIRE): '3600'
|
||||
env(PLUGIN_ECCUBE_PASSKEYS_ROUTE_CUSTOMER_COOKIE_NAME): 'plugin_eccube_route_customer_2fa'
|
||||
env(PLUGIN_ECCUBE_PASSKEYS_ROUTE_CUSTOMER_EXPIRE): '3600'
|
||||
env(PLUGIN_ECCUBE_PASSKEYS_ROUTE_COOKIE_VALUE_CHARACTER_LENGTH): '64'
|
||||
|
||||
plugin_eccube_passkeys_customer_cookie_name: '%env(PLUGIN_ECCUBE_PASSKEYS_CUSTOMER_COOKIE_NAME)%'
|
||||
plugin_eccube_passkeys_route_customer_cookie_name: '%env(PLUGIN_ECCUBE_PASSKEYS_ROUTE_CUSTOMER_COOKIE_NAME)%'
|
||||
plugin_eccube_passkeys_customer_expire: '%env(PLUGIN_ECCUBE_PASSKEYS_CUSTOMER_EXPIRE)%'
|
||||
plugin_eccube_passkeys_route_customer_expire: '%env(PLUGIN_ECCUBE_PASSKEYS_ROUTE_CUSTOMER_EXPIRE)%'
|
||||
plugin_eccube_passkeys_route_cookie_value_character_length: '%env(PLUGIN_ECCUBE_PASSKEYS_ROUTE_COOKIE_VALUE_CHARACTER_LENGTH)%'
|
||||
|
||||
16
Resource/locale/messages.ja.yml
Normal file
16
Resource/locale/messages.ja.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
admin.setting.shop.shop.customer_passkey_auth: パスキー多要素認証を利用
|
||||
admin.setting.shop.shop.customer_passkey_auth_tooltip: ログイン時、通常(メールアドレス・パスワード)認証に加え、パスキー認証を実施します。
|
||||
|
||||
admin.customer.passkyes.title: パスキー認証設定
|
||||
admin.customer.passkeys.authed: パスキー多要素認証を利用
|
||||
|
||||
front.passkeys.title: パスキー認証
|
||||
front.passkeys.email: ユーザーID
|
||||
front.passkeys.register: パスキーを登録
|
||||
front.passkeys.auth: パスキー認証
|
||||
front.passkeys.message: |
|
||||
自動処理中。。。
|
||||
front.2fa.device_auth.input.message: |
|
||||
携帯電話に送信された認証コードを入力してください。
|
||||
送信されていない場合は「認証コードを再送信」をクリックしてください。
|
||||
|
||||
42
Resource/template/admin/customer_edit.twig
Executable file
42
Resource/template/admin/customer_edit.twig
Executable file
@@ -0,0 +1,42 @@
|
||||
{#
|
||||
This file is part of EC-CUBE
|
||||
|
||||
Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
|
||||
http://www.ec-cube.co.jp/
|
||||
|
||||
For the full copyright and license information, please view the LICENSE
|
||||
file that was distributed with this source code.
|
||||
#}
|
||||
<script type="text/javascript">
|
||||
$(function () {
|
||||
$(".c-primaryCol").last().append($("#passkeys_setting").detach());
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="card rounded border-0 mb-4" id="passkeys_setting">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col-8"><span class="card-title">{{ 'admin.customer.passkyes.title'|trans }}</span>
|
||||
</div>
|
||||
<div class="col-4 text-end">
|
||||
<a data-bs-toggle="collapse" href="#ordererInfo"
|
||||
aria-expanded="false" aria-controls="ordererInfo">
|
||||
<i class="fa fa-angle-up fa-lg"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse show ec-cardCollapse" id="ordererInfo">
|
||||
<div class="card-body">
|
||||
<div class="row mb-2">
|
||||
<div class="col-3">
|
||||
<span>{{ 'admin.customer.passkeys.authed'|trans }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
{{ form_widget(form.enable_passkeys) }}
|
||||
{{ form_errors(form.enable_passkeys) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
30
Resource/template/admin/shop_edit_tfa.twig
Normal file
30
Resource/template/admin/shop_edit_tfa.twig
Normal file
@@ -0,0 +1,30 @@
|
||||
{#
|
||||
This file is part of EC-CUBE
|
||||
|
||||
Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
|
||||
http://www.ec-cube.co.jp/
|
||||
|
||||
For the full copyright and license information, please view the LICENSE
|
||||
file that was distributed with this source code.
|
||||
#}
|
||||
<script type="text/javascript">
|
||||
$(function () {
|
||||
$('#passkeys_use_div > div.col-3 > div').tooltip();
|
||||
$("#ex-shop-customer").last().append($("#passkeys_use_div").detach());
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="row" id="passkeys_use_div">
|
||||
<div class="col-3">
|
||||
<div class="d-inline-block" data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="{{ 'admin.setting.shop.shop.customer_passkey_auth_tooltip'|trans }}">
|
||||
<span>{{ 'admin.setting.shop.shop.customer_passkey_auth'|trans }}</span>
|
||||
<i class="fa fa-question-circle fa-lg ms-1"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-2">
|
||||
{{ form_widget(form.passkeys_use) }}
|
||||
{{ form_errors(form.passkeys_use) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
192
Resource/template/default/passkey.twig
Normal file
192
Resource/template/default/passkey.twig
Normal file
@@ -0,0 +1,192 @@
|
||||
{#
|
||||
This file is part of EC-CUBE
|
||||
|
||||
Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
|
||||
|
||||
http://www.ec-cube.co.jp/
|
||||
|
||||
For the full copyright and license information, please view the LICENSE
|
||||
file that was distributed with this source code.
|
||||
#}
|
||||
{% extends 'default_frame.twig' %}
|
||||
|
||||
{% set body_class = 'mypage' %}
|
||||
|
||||
{% block stylesheet %}
|
||||
<style>
|
||||
.ec-login-header {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.ec-login-header > p {
|
||||
text-align: center;
|
||||
font: var(--unnamed-font-style-normal) normal medium 16px/22px YuGothic;
|
||||
letter-spacing: var(--unnamed-character-spacing-0);
|
||||
text-align: center;
|
||||
font: normal normal medium 16px/22px YuGothic;
|
||||
letter-spacing: 0px;
|
||||
color: #525263;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script src="{{ asset('passkeys/assets/dfido2-lib.js', 'plugin') }}"></script>
|
||||
<script>
|
||||
setFidoServerURL('https://fido2.amipro.me');//'https://mac-air-m2.dqj-home.com');
|
||||
var rp, full_uid, domain;
|
||||
window.onload = async function() {
|
||||
domain = window.location.hostname
|
||||
rp = domain + '.ec-cube.service';
|
||||
full_uid = "{{ Customer.username }}_"+domain;
|
||||
|
||||
if(!isWebAuthnSupported()){
|
||||
alert("パスキーをサポートしないブラウザを利用しているため、パスキー認証を無効にします。");
|
||||
$('#mode').val('no_webauthn');
|
||||
$('#passkey_form').submit();
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(function(){
|
||||
$('#auto_message').hide();
|
||||
$('#login_form').show();
|
||||
$('#login_email').val("{{ Customer.username }}");
|
||||
$('#login_email').focus();
|
||||
}, 6000);
|
||||
|
||||
/*const sessionOk = await validSession(rp);
|
||||
if(sessionOk){
|
||||
alert("sessionOk: {{ url(succ_route) }}");
|
||||
window.location.href = "{{ url(succ_route) }}";
|
||||
return;
|
||||
}*/
|
||||
|
||||
//Try auth first
|
||||
await logoutFido2UserSession();
|
||||
|
||||
if(canTryAutoAuthentication()){
|
||||
if(await authenticate(full_uid)){
|
||||
const session = getSessionId();
|
||||
$('#mode').val('login_succ');
|
||||
$('#pk_session').val(session);
|
||||
$('#rp').val(rp);
|
||||
|
||||
//alert("{{ succ_route }}"+"|"+full_uid+"|"+session);
|
||||
$('#passkey_form').submit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//Try register
|
||||
alert("生体認証を有効するために、次の画面で生体認証を行ってください。");
|
||||
await register(full_uid);
|
||||
};
|
||||
|
||||
async function clickedAuthenticate(){
|
||||
var uid = $('#login_email').val()
|
||||
if(uid && 0==uid.length)uid=full_uid
|
||||
else uid = uid+'_'+domain;
|
||||
|
||||
if(await authenticate(uid)){
|
||||
const session = getSessionId();
|
||||
$('#mode').val('login_succ');
|
||||
$('#pk_session').val(session);
|
||||
$('#rp').val(rp);
|
||||
|
||||
//alert("{{ succ_route }}"+"|"+uid+"|"+session);
|
||||
$('#passkey_form').submit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function authenticate(uid_full){
|
||||
const result = await authenticateFido2(uid_full, rp);
|
||||
|
||||
if(result.status === 'ok'){
|
||||
return true;
|
||||
}else{
|
||||
errProcessFido2(result)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function clickedRegister(){
|
||||
var uid = $('#login_email').val()
|
||||
if(uid && 0==uid.length)uid=full_uid;
|
||||
else uid = uid+'_'+domain;
|
||||
|
||||
await register(uid);
|
||||
}
|
||||
|
||||
async function register(uid_full){
|
||||
const result = await registerFido2(uid_full, 'dis_'+uid_full, rp);
|
||||
|
||||
if(result.status === 'ok'){
|
||||
const session = getSessionId();
|
||||
$('#mode').val('login_succ');
|
||||
$('#pk_session').val(session);
|
||||
$('#rp').val(rp);
|
||||
|
||||
//alert("Reg succ:{{ succ_route }}"+"|"+uid_full+"|"+session);
|
||||
$('#passkey_form').submit();
|
||||
return;
|
||||
}else{
|
||||
const msg = errMessageFido2(result);
|
||||
//alert('reg err:'+msg);
|
||||
if(msg && msg.startsWith('Fido2LibErr105')){
|
||||
//alert('retry auth:'+msg);
|
||||
if(await authenticate(rp)){
|
||||
const session = getSessionId();
|
||||
$('#mode').val('login_succ');
|
||||
$('#pk_session').val(session);
|
||||
$('#rp').val(rp);
|
||||
|
||||
//alert("{{ succ_route }}"+"|"+uid+"|"+session);
|
||||
$('#passkey_form').submit();
|
||||
return;
|
||||
}
|
||||
}else alert(msg)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock javascript %}
|
||||
|
||||
{% block main %}
|
||||
<div class="ec-role">
|
||||
<div class="ec-pageHeader">
|
||||
<h1>{{ 'front.passkeys.title'|trans }}</h1>
|
||||
</div>
|
||||
<div class="ec-off2Grid">
|
||||
<div class="ec-off2Grid__cell">
|
||||
<form name="passkey_form" id="passkey_form" method="post"
|
||||
action="{{ url('plg_customer_passkey_page') }}">
|
||||
<input type="hidden" name="mode" id="mode" value="login">
|
||||
<input type="hidden" name="pk_session" id="pk_session" value="">
|
||||
<input type="hidden" name="rp" id="rp" value="">
|
||||
|
||||
<div class="ec-login ec-login-header" id='auto_message'>
|
||||
<p>{{ 'front.passkeys.message'|trans|nl2br }}</p>
|
||||
</div>
|
||||
<div class="ec-login" id='login_form' style='display:none;'>
|
||||
<!-- div class="ec-login__icon">
|
||||
<div class="ec-icon"><img src="{{ asset('assets/icon/user.svg') }}" alt=""></div>
|
||||
</div -->
|
||||
<div class="ec-login__input">
|
||||
<div class="ec-input">
|
||||
<input type="text" name="login_email" id="login_email" class="ec-input__field"
|
||||
placeholder="{{ 'front.passkeys.email'|trans }}" value="{{ Customer.username }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="ec-cartNaviIsset__action">
|
||||
<a class="ec-blockBtn--primary" href="javascript:clickedAuthenticate();">{{ 'front.passkeys.auth'|trans }}</a>
|
||||
<br><br>
|
||||
<a class="ec-blockBtn--action" href="javascript:clickedRegister();">{{ 'front.passkeys.register'|trans }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user