async function registerCredentials(userName, errorHandler = console.error) { // ask server to initiate registration let createOptions try { createOptions = await sendRemoteBeginRegistration(userName) } catch (err) { console.error(err) errorHandler("Failed to identify the user") } if (!createOptions) return false // decode some buffers in create options createOptions.publicKey.challenge = bufferDecode(createOptions.publicKey.challenge); createOptions.publicKey.user.id = bufferDecode(createOptions.publicKey.user.id); if (createOptions.publicKey.excludeCredentials) { for (let i = 0; i < createOptions.publicKey.excludeCredentials.length; i++) { createOptions.publicKey.excludeCredentials[i].id = bufferDecode(createOptions.publicKey.excludeCredentials[i].id); } } try { // create credentials and get them from the key const credential = await navigator.credentials.create({publicKey: createOptions.publicKey}) return await sendRemoteFinishRegistration(userName, credential) } catch (err) { console.error(err) errorHandler("Failed to register key") return false } } async function loginUser(userName, errorHandler = console.error) { // ask server to initiate login let loginOptions try { loginOptions = await sendRemoteBeginLogin(userName) } catch (err) { console.error(err) errorHandler("Failed to identify the user") } // decode some buffers in login options if (!loginOptions) return false loginOptions.publicKey.challenge = bufferDecode(loginOptions.publicKey.challenge); loginOptions.publicKey.allowCredentials.forEach(function (listItem) { listItem.id = bufferDecode(listItem.id) }); try { // create credentials and get them from the key const credential = await navigator.credentials.get({publicKey: loginOptions.publicKey}) await sendRemoteFinishLogin(userName, credential) return true } catch (err) { console.error(err) errorHandler("Failed to log in") return false } } // ---- remote requests ----------------------- function sendRemoteBeginRegistration(userName) { if (!userName) return return remotePostData("begin-registration", {userName}) } function sendRemoteFinishRegistration(userName, credential) { return remotePostData(`finish-registration?user=${userName}`, { id: credential.id, rawId: bufferEncode(credential.rawId), type: credential.type, response: { attestationObject: bufferEncode(credential.response.attestationObject), clientDataJSON: bufferEncode(credential.response.clientDataJSON), }, }) } function sendRemoteBeginLogin(userName) { if (!userName) return return remotePostData("begin-login", {userName}) } function sendRemoteFinishLogin(userName, assertion) { return remotePostData(`finish-login?user=${userName}`,{ id: assertion.id, rawId: bufferEncode(assertion.rawId), type: assertion.type, response: { authenticatorData: bufferEncode(assertion.response.authenticatorData), clientDataJSON: bufferEncode(assertion.response.clientDataJSON), signature: bufferEncode(assertion.response.signature), userHandle: bufferEncode(assertion.response.userHandle), }, }) } // http requests async function remotePostData(url, data) { try { return await remoteFetchData(url, {method: 'POST', body: JSON.stringify(data)}) } catch (err) { throw new Error(`remote error: ${err}`) } } async function remoteFetchData(url, init) { const resp = await fetch(url, init) // try to parse the response first let data = {} let errorMessage = "" try { data = await (resp.json()) || {} } catch (err) {} if (!resp.ok) { errorMessage = `${resp.status}` if (data.error) { errorMessage = `${errorMessage}: ${data.error}` } throw new Error(errorMessage) } return data.data || {} } // utils ----------------------------- // Base64 to ArrayBuffer function bufferDecode(value) { const decoded = atob(value.replace(/-/g, "+").replace(/_/g, "/")); return Uint8Array.from(decoded, c => c.charCodeAt(0)); } // ArrayBuffer to URLBase64 function bufferEncode(value) { return btoa(String.fromCharCode.apply(null, new Uint8Array(value))) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, "");; }