1/** 2 * @package Joomla.Plugin 3 * @subpackage System.webauthn 4 * 5 * @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org> 6 * @license GNU General Public License version 2 or later; see LICENSE.txt 7 */ 8 9window.Joomla = window.Joomla || {}; 10((Joomla, document) => { 11 12 /** 13 * Converts a simple object containing query string parameters to a single, escaped query string. 14 * This method is a necessary evil since Joomla.request can only accept data as a string. 15 * 16 * @param object {object} A plain object containing the query parameters to pass 17 * @param prefix {string} Prefix for array-type parameters 18 * 19 * @returns {string} 20 */ 21 const interpolateParameters = (object, prefix = '') => { 22 let encodedString = ''; 23 Object.keys(object).forEach(prop => { 24 if (typeof object[prop] !== 'object') { 25 if (encodedString.length > 0) { 26 encodedString += '&'; 27 } 28 if (prefix === '') { 29 encodedString += `${encodeURIComponent(prop)}=${encodeURIComponent(object[prop])}`; 30 } else { 31 encodedString += `${encodeURIComponent(prefix)}[${encodeURIComponent(prop)}]=${encodeURIComponent(object[prop])}`; 32 } 33 return; 34 } 35 36 // Objects need special handling 37 encodedString += `${interpolateParameters(object[prop], prop)}`; 38 }); 39 return encodedString; 40 }; 41 42 /** 43 * Finds the first field matching a selector inside a form 44 * 45 * @param {HTMLFormElement} form The FORM element 46 * @param {String} fieldSelector The CSS selector to locate the field 47 * 48 * @returns {Element|null} NULL when no element is found 49 */ 50 const findField = (form, fieldSelector) => { 51 const elInputs = form.querySelectorAll(fieldSelector); 52 if (!elInputs.length) { 53 return null; 54 } 55 return elInputs[0]; 56 }; 57 58 /** 59 * Find a form field described by the CSS selector fieldSelector. 60 * The field must be inside a <form> element which is either the 61 * outerElement itself or enclosed by outerElement. 62 * 63 * @param {Element} outerElement The element which is either our form or contains our form. 64 * @param {String} fieldSelector The CSS selector to locate the field 65 * 66 * @returns {null|Element} NULL when no element is found 67 */ 68 const lookForField = (outerElement, fieldSelector) => { 69 let elInput = null; 70 if (!outerElement) { 71 return elInput; 72 } 73 const elElement = outerElement.parentElement; 74 if (elElement.nodeName === 'FORM') { 75 elInput = findField(elElement, fieldSelector); 76 return elInput; 77 } 78 const elForms = elElement.querySelectorAll('form'); 79 if (elForms.length) { 80 for (let i = 0; i < elForms.length; i += 1) { 81 elInput = findField(elForms[i], fieldSelector); 82 if (elInput !== null) { 83 return elInput; 84 } 85 } 86 } 87 return null; 88 }; 89 90 /** 91 * A simple error handler. 92 * 93 * @param {String} message 94 */ 95 const handleLoginError = message => { 96 Joomla.renderMessages({ 97 error: [message] 98 }); 99 }; 100 101 /** 102 * Handles the browser response for the user interaction with the authenticator. Redirects to an 103 * internal page which handles the login server-side. 104 * 105 * @param { Object} publicKey Public key request options, returned from the server 106 */ 107 const handleLoginChallenge = publicKey => { 108 const arrayToBase64String = a => btoa(String.fromCharCode(...a)); 109 const base64url2base64 = input => { 110 let output = input.replace(/-/g, '+').replace(/_/g, '/'); 111 const pad = output.length % 4; 112 if (pad) { 113 if (pad === 1) { 114 throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding'); 115 } 116 output += new Array(5 - pad).join('='); 117 } 118 return output; 119 }; 120 if (!publicKey.challenge) { 121 handleLoginError(Joomla.Text._('PLG_SYSTEM_WEBAUTHN_ERR_INVALID_USERNAME')); 122 return; 123 } 124 publicKey.challenge = Uint8Array.from(window.atob(base64url2base64(publicKey.challenge)), c =>
124 c.charCodeAt(0)); 125 if (publicKey.allowCredentials) { 126 publicKey.allowCredentials = publicKey.allowCredentials.map(data => { 127 data.id = Uint8Array.from(window.atob(base64url2base64(data.id)), c => c.charCodeAt(0)); 128 return data; 129 }); 130 } 131 navigator.credentials.get({ 132 publicKey 133 }).then(data => { 134 const publicKeyCredential = { 135 id: data.id, 136 type: data.type, 137 rawId: arrayToBase64String(new Uint8Array(data.rawId)), 138 response: { 139 authenticatorData: arrayToBase64String(new Uint8Array(data.response.authenticatorData)), 140 clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)), 141 signature: arrayToBase64String(new Uint8Array(data.response.signature)), 142 userHandle: data.response.userHandle ? arrayToBase64String(new Uint8Array(data.response.userHandle)) : null 143 } 144 }; 145 146 // Send the response to your server 147 const paths = Joomla.getOptions('system.paths'); 148 window.location = `${paths ? `${paths.base}/index.php` : window.location.pathname}?${Joomla.getOptions('csrf.token')}=1&option=com_ajax&group=system&plugin=webauthn&` + `format=raw&akaction=login&encoding=redirect&data=${btoa(JSON.stringify(publicKeyCredential))}`; 149 }).catch(error => { 150 // Example: timeout, interaction refused... 151 handleLoginError(error); 152 }); 153 }; 154 155 /** 156 * Initialize the passwordless login, going through the server to get the registered certificates 157 * for the user. 158 * 159 * @param {string} formId The login form's or login module's HTML ID 160 * 161 * @returns {boolean} Always FALSE to prevent BUTTON elements from reloading the page. 162 */ 163 Joomla.plgSystemWebauthnLogin = formId => { 164 // Get the username 165 const elFormContainer = document.getElementById(formId); 166 const elUsername = lookForField(elFormContainer, 'input[name=username]'); 167 const elReturn = lookForField(elFormContainer, 'input[name=return]'); 168 if (elUsername === null) { 169 Joomla.renderMessages({ 170 error: [Joomla.Text._('PLG_SYSTEM_WEBAUTHN_ERR_CANNOT_FIND_USERNAME')] 171 }); 172 return false; 173 } 174 const username = elUsername.value; 175 const returnUrl = elReturn ? elReturn.value : null; 176 177 // No username? We cannot proceed. We need a username to find the acceptable public keys :( 178 if (username === '') { 179 Joomla.renderMessages({ 180 error: [Joomla.Text._('PLG_SYSTEM_WEBAUTHN_ERR_EMPTY_USERNAME')] 181 }); 182 return false; 183 } 184 185 // Get the Public Key Credential Request Options (challenge and acceptable public keys) 186 const postBackData = { 187 option: 'com_ajax', 188 group: 'system', 189 plugin: 'webauthn', 190 format: 'raw', 191 akaction: 'challenge', 192 encoding: 'raw', 193 username, 194 returnUrl 195 }; 196 postBackData[Joomla.getOptions('csrf.token')] = 1; 197 const paths = Joomla.getOptions('system.paths'); 198 Joomla.request({ 199 url: `${paths ? `${paths.base}/index.php` : window.location.pathname}?${Joomla.getOptions('csrf.token')}=1`, 200 method: 'POST', 201 data: interpolateParameters(postBackData), 202 onSuccess(rawResponse) { 203 let jsonData = {}; 204 try { 205 jsonData = JSON.parse(rawResponse); 206 } catch (e) { 207 /** 208 * In case of JSON decoding failure fall through; the error will be handled in the login 209 * challenge handler called below. 210 */ 211 } 212 handleLoginChallenge(jsonData); 213 }, 214 onError: xhr => { 215 handleLoginError(`${xhr.status} ${xhr.statusText}`); 216 } 217 }); 218 return false; 219 }; 220 221 // Initialization. Runs on DOM content loaded since this script is always loaded deferred. 222 document.querySelectorAll('.plg_system_webauthn_login_button').forEach(button => { 223 button.addEventListener('click', ({ 224 currentTarget 225 }) => Joomla.plgSystemWebauthnLogin(currentTarget.getAttribute('data-webauthn-form'))); 226 }); 227})(Joomla, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.