/* * Passkey Implementation */ let passkey_debug = false; /** * Convert a ArrayBuffer to Base64 * @param {ArrayBuffer} buffer * @returns {String} */ function arrayBufferToBase64(buffer) { let binary = ''; let bytes = new Uint8Array(buffer); let len = bytes.byteLength; for (let i = 0; i < len; i++) { binary += String.fromCharCode( bytes[ i ] ); } return window.btoa(binary); } /** * convert RFC 1342-like base64 strings to array buffer * @param {mixed} obj * @returns {undefined} */ function recursiveBase64StrToArrayBuffer(obj) { let prefix = '=?BINARY?B?'; let suffix = '?='; if (typeof obj === 'object') { for (let key in obj) { if (typeof obj[key] === 'string') { let str = obj[key]; if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) { str = str.substring(prefix.length, str.length - suffix.length); let binary_string = window.atob(str); let len = binary_string.length; let bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); } obj[key] = bytes.buffer; } } else { recursiveBase64StrToArrayBuffer(obj[key]); } } } } function passkey_check_browser() { // check browser support if ((! window.fetch) || (! navigator.credentials) || (! navigator.credentials.create)) throw new Error('Browser not supported.'); /* // Availability of `window.PublicKeyCredential` means WebAuthn is usable. // `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable. // `isConditionalMediationAvailable` means the feature detection is usable. if (window.PublicKeyCredential && PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable && PublicKeyCredential.isConditionalMediationAvailable) { // Check if user verifying platform authenticator is available. Promise.all([ PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(), PublicKeyCredential.isConditionalMediationAvailable(), ]).then(results => { if (results.every(r => r === true)) { // Display "Create a new passkey" button } }); } */ if (passkey_debug) console.log('Passkey: Browser OK'); return true; } /** * Register/Create a passkey for a user */ async function passkey_register(csrf_token,icon_dom,icon,icon_shell_success,icon_shell_fail) { try { if (! passkey_check_browser()) return; // Change our icon so that it is obvious we are doing something icon_dom.find('i').removeClass(icon).addClass('spinner-grow spinner-grow-sm'); // Get our arguments var createArgs; $.ajax({ url: '/passkey/register', type: 'GET', dataType: 'json', async: false, cache: false, success: function(data) { if (passkey_debug) console.log('Passkey: Get Register Success'); recursiveBase64StrToArrayBuffer(data); createArgs = data; }, error: function(e,status,error) { throw new Error(status || 'Unknown error occurred'); } }); // Create credentials try { const cred = await navigator.credentials.create(createArgs); const authenticatorAttestationResponse = { id: cred.id, rawId: arrayBufferToBase64(cred.rawId), transports: cred.response.getTransports ? cred.response.getTransports() : null, clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null, authenticatorAttachment: cred.authenticatorAttachment, _token: csrf_token, }; $.ajax({ url: '/passkey/check', type: 'POST', data: authenticatorAttestationResponse, cache: false, success: function(data) { if (passkey_debug) console.log('Passkey: Registration Success'); icon_dom.find('i').addClass(icon).removeClass('spinner-grow spinner-grow-sm'); icon_dom.removeClass('btn-outline-primary').addClass('btn-primary'); }, error: function(e,status,error) { throw new Error(status || 'Unknown error occurred'); } }); } catch (status) { if (passkey_debug) console.log(status || 'Passkey: User Aborted Register'); // Restore the icon icon_dom.find('i').addClass(icon).removeClass('spinner-grow spinner-grow-sm'); return; } } catch (err) { window.alert(err || 'An UNKNOWN error occurred?'); } } /** * Check a passkey being presented */ async function passkey_check(csrf_token,redirect) { if (passkey_debug) console.log('Passkey: Check User Passkey'); try { if (! passkey_check_browser()) return; // Get our arguments var getArgs; $.ajax({ url: '/passkey/get', type: 'GET', dataType: 'json', async: false, cache: false, success: function(data) { if (passkey_debug) console.log('Passkey: Get Args Success'); recursiveBase64StrToArrayBuffer(data); getArgs = data; }, error: function(e,status,error) { throw new Error(status || 'Unknown error occurred'); } }); // check credentials with hardware const cred = await navigator.credentials.get(getArgs); // create object for transmission to server const authenticatorAttestationResponse = { id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null, clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null, signature: cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null, userHandle: cred.response.userHandle ? arrayBufferToBase64(cred.response.userHandle) : null, _token: csrf_token }; $.ajax({ url: '/passkey/process', type: 'POST', data: authenticatorAttestationResponse, cache: false, success: function(data) { if (passkey_debug) console.log('Passkey: Process Success'); // Direct to the home page if (data.success) window.location.href = (redirect !== undefined) ? redirect : '/'; else alert(data.msg || 'Unknown error occurred'); }, error: function(e,status,error) { throw new Error(status || 'Unknown error occurred'); } }); } catch (err) { window.alert(err || 'An UNKNOWN error occurred?'); } }