diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f3aaf432e239f..515ec79e526ed 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -50,3 +50,10 @@ build/media_source/*/js/* @wilsonge plugins/system/httpheaders/* @zero-24 administrator/components/com_csp/* @zero-24 components/com_csp/* @zero-24 + +# Web Authentication (WebAuthn) + +plugins/system/webauthn/* @nikosdion +media/plg_system_webauthn/* @nikosdion +language/administrator/en-GB/en-GB.plg_system_webauthn.ini @nikosdion +language/administrator/en-GB/en-GB.plg_system_webauthn.sys.ini @nikosdion diff --git a/.gitignore b/.gitignore index 89f7804d40967..ee85aa319e20d 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,6 @@ RoboFile.ini # Media Manager /media/com_media/js/mediamanager.min.js.map /media/com_media/css/mediamanager.min.css.map + +# Web Authentication plugin +!/build/media_source/plg_system_webauthn/js/*.es6.js diff --git a/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-02.sql b/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-02.sql new file mode 100644 index 0000000000000..4cf70da3dffb4 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-02.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS `#__webauthn_credentials` +( + `id` VARCHAR(1000) NOT NULL COMMENT 'Credential ID', + `user_id` VARCHAR(128) NOT NULL COMMENT 'User handle', + `label` VARCHAR(190) NOT NULL COMMENT 'Human readable label', + `credential` MEDIUMTEXT NOT NULL COMMENT 'Credential source data, JSON format', + PRIMARY KEY (`id`(100)), + INDEX (`user_id`(100)) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + DEFAULT COLLATE = utf8mb4_unicode_ci; + +INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `manifest_cache`, `params`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES +(0, 'plg_system_webauthn', 'plugin', 'webauthn', 'system', 0, 1, 1, 0, '', '{}', 0, '0000-00-00 00:00:00', 0, 0); diff --git a/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-02.sql b/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-02.sql new file mode 100644 index 0000000000000..f3b8c2402776c --- /dev/null +++ b/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-02.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS "#__webauthn_credentials" +( + "id" varchar(1000) NOT NULL, + "user_id" varchar(128) NOT NULL, + "label" varchar(190) NOT NULL, + "credential" TEXT NOT NULL, + PRIMARY KEY ("id") +); + +CREATE INDEX "#__webauthn_credentials_user_id" ON "#__webauthn_credentials" ("user_id"); + +INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "checked_out", "checked_out_time", "ordering", "state") VALUES +(0, 'plg_system_webauthn', 'plugin', 'webauthn', 'system', 0, 1, 1, 0, '', '{}', 0, '1970-01-01 00:00:00', 8, 0); diff --git a/administrator/language/en-GB/en-GB.plg_system_webauthn.ini b/administrator/language/en-GB/en-GB.plg_system_webauthn.ini new file mode 100644 index 0000000000000..57ea022d85be4 --- /dev/null +++ b/administrator/language/en-GB/en-GB.plg_system_webauthn.ini @@ -0,0 +1,46 @@ +; Joomla! Project +; Copyright (C) 2005 - 2019 Open Source Matters. All rights reserved. +; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php +; Note : All ini files need to be saved as UTF-8 + +PLG_SYSTEM_WEBAUTHN="System - WebAuthn Passwordless Login" +PLG_SYSTEM_WEBAUTHN_DESCRIPTION="Enables passwordless authentication using the W3C Web Authentication (WebAuthn) API." + +PLG_SYSTEM_WEBAUTHN_LOGIN_LABEL="Web Authentication" +PLG_SYSTEM_WEBAUTHN_LOGIN_DESC="Login without a password using the W3C Web Authentication (WebAuthn) standard in compatible browsers. You need to have already set up WebAuthn authentication in your user profile." + +PLG_SYSTEM_WEBAUTHN_HEADER="W3C Web Authentication (WebAuthn) Login" +PLG_SYSTEM_WEBAUTHN_FIELD_LABEL="W3C Web Authentication (WebAuthn) Login" +PLG_SYSTEM_WEBAUTHN_FIELD_DESC="Lets you manage passwordless login methods using the W3C Web Authentication standard. You need a supported browser and authenticator (e.g. Google Chrome or Firefox with a FIDO2 certified security key)." + +PLG_SYSTEM_WEBAUTHN_MANAGE_FIELD_KEYLABEL_LABEL="Authenticator name" +PLG_SYSTEM_WEBAUTHN_MANAGE_FIELD_KEYLABEL_DESC="A short name for the authenticator used with this passwordless login method." +PLG_SYSTEM_WEBAUTHN_MANAGE_HEADER_NOMETHODS_LABEL="No authenticators have been set up yet." +PLG_SYSTEM_WEBAUTHN_MANAGE_HEADER_ACTIONS_LABEL="Actions" +PLG_SYSTEM_WEBAUTHN_MANAGE_BTN_DELETE_LABEL="Remove" +PLG_SYSTEM_WEBAUTHN_MANAGE_BTN_EDIT_LABEL="Edit name" +PLG_SYSTEM_WEBAUTHN_MANAGE_BTN_ADD_LABEL="Add new authenticator" +PLG_SYSTEM_WEBAUTHN_MANAGE_BTN_SAVE_LABEL="Save" +PLG_SYSTEM_WEBAUTHN_MANAGE_BTN_CANCEL_LABEL="Cancel" + +PLG_SYSTEM_WEBAUTHN_LBL_DEFAULT_AUTHENTICATOR_LABEL="Authenticator added on %s" + +PLG_SYSTEM_WEBAUTHN_MSG_SAVED_LABEL="The label has been saved successfully." +PLG_SYSTEM_WEBAUTHN_MSG_DELETED="The authenticator has been removed successfully." + +PLG_SYSTEM_WEBAUTHN_ERR_NO_STORED_CREDENTIAL="Cannot find the stored credentials for your login authenticator." +PLG_SYSTEM_WEBAUTHN_ERR_CORRUPT_STORED_CREDENTIAL="The stored credentials are corrupt for your user account. Log in using another method, then remove and add again your login authenticator." +PLG_SYSTEM_WEBAUTHN_ERR_CANT_STORE_FOR_GUEST="Cannot possibly store credentials for Guest user!" +PLG_SYSTEM_WEBAUTHN_ERR_CREDENTIAL_ID_ALREADY_IN_USE="Cannot save credentials. These credentials are already being used by a different user." +PLG_SYSTEM_WEBAUTHN_ERR_USER_REMOVED="The user for this authenticator seems to no longer exist on this site." +PLG_SYSTEM_WEBAUTHN_ERR_NO_BROWSER_SUPPORT="Sorry, your browser does not support the W3C Web Authentication standard for passwordless logins. You will need to log into this site using your username and password." +PLG_SYSTEM_WEBAUTHN_ERR_CREATE_NO_PK="The server has not issued a Public Key for authenticator registration but somehow received an authenticator registration request from the browser. This means that someone tried to hack you or something is broken." +PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_PK="The authenticator registration has failed. The authenticator response received from the browser does not match the Public Key issued by the server. This means that someone tried to hack you or something is broken." +PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_USER="For security reasons you are not allowed to register passwordless authentication tokens on behalf of another user." +PLG_SYSTEM_WEBAUTHN_ERR_CREATE_NO_ATTESTED_DATA="Something went wrong but no further information about the error is available at this time. Please retry registering your authenticator." +PLG_SYSTEM_WEBAUTHN_ERR_LABEL_NOT_SAVED="Could not save the new label" +PLG_SYSTEM_WEBAUTHN_ERR_NOT_DELETED="Could not remove the authenticator" +PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST="Invalid passwordless login request. Something is broken or this is an attempt to hack the site." +PLG_SYSTEM_WEBAUTHN_ERR_CANNOT_FIND_USERNAME="Cannot find the username field in the login module. Sorry, Passwordless authentication will not work on this site unless you use a different login module." +PLG_SYSTEM_WEBAUTHN_ERR_EMPTY_USERNAME="You need to enter your username (but NOT your password) before clicking the Passwordless Login button." +PLG_SYSTEM_WEBAUTHN_ERR_INVALID_USERNAME="The specified username does not correspond to a user account that has enabled passwordless login on this site." diff --git a/administrator/language/en-GB/en-GB.plg_system_webauthn.sys.ini b/administrator/language/en-GB/en-GB.plg_system_webauthn.sys.ini new file mode 100644 index 0000000000000..4be68e3ed6857 --- /dev/null +++ b/administrator/language/en-GB/en-GB.plg_system_webauthn.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; Copyright (C) 2005 - 2019 Open Source Matters. All rights reserved. +; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php +; Note : All ini files need to be saved as UTF-8 + +PLG_SYSTEM_WEBAUTHN="System - WebAuthn Passwordless Login" +PLG_SYSTEM_WEBAUTHN_DESCRIPTION="Enables passwordless authentication using the W3C Web Authentication (WebAuthn) API." diff --git a/administrator/modules/mod_login/mod_login.php b/administrator/modules/mod_login/mod_login.php index 53445e1b0f54e..23d407779b1b6 100644 --- a/administrator/modules/mod_login/mod_login.php +++ b/administrator/modules/mod_login/mod_login.php @@ -15,6 +15,7 @@ $langs = LoginHelper::getLanguageList(); $twofactormethods = AuthenticationHelper::getTwoFactorMethods(); +$extraButtons = AuthenticationHelper::getLoginButtons('form-login'); $return = LoginHelper::getReturnUri(); require ModuleHelper::getLayoutPath('mod_login', $params->get('layout', 'default')); diff --git a/administrator/modules/mod_login/tmpl/default.php b/administrator/modules/mod_login/tmpl/default.php index 16fcc38bf42ce..36c61a791e36a 100644 --- a/administrator/modules/mod_login/tmpl/default.php +++ b/administrator/modules/mod_login/tmpl/default.php @@ -96,6 +96,25 @@ class="form-control input-full" + +
+ +
+
diff --git a/build/media_source/plg_system_webauthn/images/webauthn-black.png b/build/media_source/plg_system_webauthn/images/webauthn-black.png new file mode 100644 index 0000000000000..db7a146ef9e80 Binary files /dev/null and b/build/media_source/plg_system_webauthn/images/webauthn-black.png differ diff --git a/build/media_source/plg_system_webauthn/images/webauthn-color.png b/build/media_source/plg_system_webauthn/images/webauthn-color.png new file mode 100644 index 0000000000000..ec570bd00ac80 Binary files /dev/null and b/build/media_source/plg_system_webauthn/images/webauthn-color.png differ diff --git a/build/media_source/plg_system_webauthn/images/webauthn-white.png b/build/media_source/plg_system_webauthn/images/webauthn-white.png new file mode 100644 index 0000000000000..42cc9b452e74a Binary files /dev/null and b/build/media_source/plg_system_webauthn/images/webauthn-white.png differ diff --git a/build/media_source/plg_system_webauthn/js/login.es6.js b/build/media_source/plg_system_webauthn/js/login.es6.js new file mode 100644 index 0000000000000..7cd60d0680397 --- /dev/null +++ b/build/media_source/plg_system_webauthn/js/login.es6.js @@ -0,0 +1,231 @@ +/** + * @package Joomla.Plugin + * @subpackage System.updatenotification + * + * @copyright Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved. + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +/** + * Finds the first field matching a selector inside a form + * + * @param {HTMLFormElement} elForm The FORM element + * @param {String} fieldSelector The CSS selector to locate the field + * + * @returns {Element|null} NULL when no element is found + */ +function plgSystemWebauthnFindField(elForm, fieldSelector) { + let elInputs = elForm.querySelectorAll(fieldSelector); + + if (!elInputs.length) { + return null; + } + + return elInputs[0]; +} + +/** + * Find a form field described the CSS selector fieldSelector. The field must be inside a
+ * element which is either the outerElement itself or enclosed by outerElement. + * + * @param {Element} outerElement The element which is either our form or contains our form. + * @param {String} fieldSelector The CSS selector to locate the field + * + * @returns {null|Element} NULL when no element is found + */ +function plgSystemWebauthnLookForField(outerElement, fieldSelector) { + var elElement = outerElement.parentElement; + var elInput = null; + + if (elElement.nodeName === 'FORM') { + elInput = plgSystemWebauthnFindField(elElement, fieldSelector); + + return elInput; + } + + var elForms = elElement.querySelectorAll('form'); + + if (elForms.length) { + for (var i = 0; i < elForms.length; i++) { + elInput = plgSystemWebauthnFindField(elForms[i], fieldSelector); + + if (elInput !== null) { + return elInput; + } + } + } + + return null; +} + +/** + * Initialize the passwordless login, going through the server to get the registered certificates + * for the user. + * + * @param {string} form_id The login form's or login module's HTML ID + * @param {string} callback_url The URL we will use to post back to the server. Must include + * the anti-CSRF token. + * + * @returns {boolean} Always FALSE to prevent BUTTON elements from reloading the page. + */ +function plgSystemWebauthnLogin(form_id, callback_url) { + // Get the username + let elFormContainer = document.getElementById(form_id); + let elUsername = plgSystemWebauthnLookForField(elFormContainer, 'input[name=username]'); + let elReturn = plgSystemWebauthnLookForField(elFormContainer, 'input[name=return]'); + + if (elUsername === null) { + alert(Joomla.JText._('PLG_SYSTEM_WEBAUTHN_ERR_CANNOT_FIND_USERNAME')); + + return false; + } + + let username = elUsername.value; + let returnUrl = elReturn ? elReturn.value : null; + + // No username? We cannot proceed. We need a username to find the acceptable public keys :( + if (username === '') { + alert(Joomla.JText._('PLG_SYSTEM_WEBAUTHN_ERR_EMPTY_USERNAME')); + + return false; + } + + // Get the Public Key Credential Request Options (challenge and acceptable public keys) + let postBackData = { + 'option': 'com_ajax', + 'group': 'system', + 'plugin': 'webauthn', + 'format': 'raw', + 'akaction': 'challenge', + 'encoding': 'raw', + 'username': username, + 'returnUrl': returnUrl + }; + + Joomla.request({ + url: callback_url, + method: 'POST', + data: plgSystemWebauthnInterpolateParameters(postBackData), + onSuccess(rawResponse) { + let jsonData = {}; + + try { + jsonData = JSON.parse(rawResponse); + } catch (e) { + /** + * In case of JSON decoding failure fall through; the error will be handled in the login + * challenge handler called below. + */ + } + + plgSystemWebauthnHandleLoginChallenge(jsonData, callback_url); + }, + onError: (xhr) => { + plgSystemWebauthnHandleLoginError(xhr.status + ' ' + xhr.statusText); + } + }); + + return false; +} + +/** + * Handles the browser response for the user interaction with the authenticator. Redirects to an + * internal page which handles the login server-side. + * + * @param { Object} publicKey Public key request options, returned from the server + * @param {String} callback_url The URL we will use to post back to the server. Must include + * the anti-CSRF token. + */ +function plgSystemWebauthnHandleLoginChallenge(publicKey, callback_url) { + function arrayToBase64String(a) { + return btoa(String.fromCharCode(...a)); + } + + if (!publicKey.challenge) { + plgSystemWebauthnHandleLoginError(Joomla.JText._('PLG_SYSTEM_WEBAUTHN_ERR_INVALID_USERNAME')); + + return; + } + + publicKey.challenge = Uint8Array.from(window.atob(publicKey.challenge), c => c.charCodeAt(0)); + publicKey.allowCredentials = publicKey.allowCredentials.map(function (data) { + return { + ...data, + 'id': Uint8Array.from(atob(data.id), c => c.charCodeAt(0)) + }; + }); + + navigator.credentials.get({publicKey}) + .then(data => { + let publicKeyCredential = { + id: data.id, + type: data.type, + rawId: arrayToBase64String(new Uint8Array(data.rawId)), + response: { + authenticatorData: arrayToBase64String(new Uint8Array(data.response.authenticatorData)), + clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)), + signature: arrayToBase64String(new Uint8Array(data.response.signature)), + userHandle: data.response.userHandle ? arrayToBase64String( + new Uint8Array(data.response.userHandle)) : null + } + }; + + window.location = callback_url + '&option=com_ajax&group=system&plugin=webauthn&' + + 'format=raw&akaction=login&encoding=redirect&data=' + + btoa(JSON.stringify(publicKeyCredential)); + + }, error => { + // Example: timeout, interaction refused... + console.log(error); + plgSystemWebauthnHandleLoginError(error); + }); +} + +/** + * A simple error handler. + * + * @param {String} message + */ +function plgSystemWebauthnHandleLoginError(message) { + alert(message); + + console.log(message); +} + +/** + * Converts a simple object containing query string parameters to a single, escaped query string. + * This method is a necessary evil since Joomla.request can only accept data as a string. + * + * @param object {object} A plain object containing the query parameters to pass + * @param prefix {string} Prefix for array-type parameters + * + * @returns {string} + */ +function plgSystemWebauthnInterpolateParameters(object, prefix) { + prefix = prefix || ''; + var encodedString = ''; + + for (var prop in object) { + if (object.hasOwnProperty(prop)) { + if (encodedString.length > 0) { + encodedString += '&'; + } + + if (typeof object[prop] !== 'object') { + if (prefix === '') { + encodedString += encodeURIComponent(prop) + '=' + encodeURIComponent(object[prop]); + } else { + encodedString += + encodeURIComponent(prefix) + '[' + encodeURIComponent(prop) + ']=' + encodeURIComponent( + object[prop]); + } + + continue; + } + + // Objects need special handling + encodedString += plgSystemWebauthnInterpolateParameters(object[prop], prop); + } + } + return encodedString; +} diff --git a/build/media_source/plg_system_webauthn/js/management.es6.js b/build/media_source/plg_system_webauthn/js/management.es6.js new file mode 100644 index 0000000000000..f1668a0a09b2d --- /dev/null +++ b/build/media_source/plg_system_webauthn/js/management.es6.js @@ -0,0 +1,346 @@ +/** + * @package Joomla.Plugin + * @subpackage System.updatenotification + * + * @copyright Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved. + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +/** + * Ask the user to link an authenticator using the provided public key (created server-side). Posts + * the credentials to the URL defined in post_url using AJAX. That URL must re-render the management + * interface. These contents will replace the element identified by the interface_selector CSS + * selector. + * + * @param {String} store_id CSS ID for the element storing the configuration in its + * data properties + * @param {String} interface_selector CSS selector for the GUI container + */ +function plgSystemWebauthnCreateCredentials(store_id, interface_selector) { + // Make sure the browser supports Webauthn + if (!('credentials' in navigator)) { + alert(Joomla.JText._('PLG_SYSTEM_WEBAUTHN_ERR_NO_BROWSER_SUPPORT')); + + console.log('This browser does not support Webauthn'); + return; + } + + // Extract the configuration from the store + let elStore = document.getElementById(store_id); + + if (!elStore) { + return; + } + + let publicKey = JSON.parse(atob(elStore.dataset.public_key)); + let post_url = atob(elStore.dataset.postback_url); + + // Utility function to convert array data to base64 strings + function arrayToBase64String(a) { + return btoa(String.fromCharCode(...a)); + } + + // Convert the public key information to a format usable by the browser's credentials manager + publicKey.challenge = Uint8Array.from( + window.atob(publicKey.challenge), + c => c.charCodeAt(0)) + ; + publicKey.user.id = Uint8Array.from( + window.atob(publicKey.user.id), + c => c.charCodeAt(0) + ); + + if (publicKey.excludeCredentials) { + publicKey.excludeCredentials = publicKey.excludeCredentials.map(function (data) { + return { + ...data, + 'id': Uint8Array.from(window.atob(data.id), c => c.charCodeAt(0)) + }; + }); + } + + // Ask the browser to prompt the user for their authenticator + navigator.credentials.create({publicKey}) + .then(function (data) { + let publicKeyCredential = { + id: data.id, + type: data.type, + rawId: arrayToBase64String(new Uint8Array(data.rawId)), + response: { + clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)), + attestationObject: arrayToBase64String(new Uint8Array(data.response.attestationObject)) + } + }; + + let postBackData = { + 'option': 'com_ajax', + 'group': 'system', + 'plugin': 'webauthn', + 'format': 'raw', + 'akaction': 'create', + 'encoding': 'raw', + 'data': btoa(JSON.stringify(publicKeyCredential)) + }; + + Joomla.request({ + url: post_url, + method: 'POST', + data: plgSystemWebauthnInterpolateParameters(postBackData), + onSuccess(responseHTML) { + let elements = document.querySelectorAll(interface_selector); + + if (!elements) { + return; + } + + let elContainer = elements[0]; + + elContainer.outerHTML = responseHTML; + }, + onError: (xhr) => { + plgSystemWebauthnHandleCreationError(xhr.status + ' ' + xhr.statusText); + } + }); + + }, function (error) { + // An error occurred: timeout, request to provide the authenticator refused, hardware / + // software error... + plgSystemWebauthnHandleCreationError(error); + }); +} + +/** + * A simple error handler + * + * @param {String} message + */ +function plgSystemWebauthnHandleCreationError(message) { + alert(message); + + console.log(message); +} + +/** + * Edit label button + * + * @param {Element} that The button being clicked + * @param {String} store_id CSS ID for the element storing the configuration in its data + * properties + */ +function plgSystemWebauthnEditLabel(that, store_id) { + // Extract the configuration from the store + let elStore = document.getElementById(store_id); + + if (!elStore) { + return; + } + + let post_url = atob(elStore.dataset.postback_url); + + // Find the UI elements + let elTR = that.parentElement.parentElement; + let credentialId = elTR.dataset.credential_id; + let elTDs = elTR.querySelectorAll('td'); + let elLabelTD = elTDs[0]; + let elButtonsTD = elTDs[1]; + let elButtons = elButtonsTD.querySelectorAll('button'); + let elEdit = elButtons[0]; + let elDelete = elButtons[1]; + + // Show the editor + let oldLabel = elLabelTD.innerText; + + let elInput = document.createElement('input'); + elInput.type = 'text'; + elInput.name = 'label'; + elInput.defaultValue = oldLabel; + + let elSave = document.createElement('button'); + elSave.className = 'btn btn-success btn-sm'; + elSave.innerText = Joomla.JText._('PLG_SYSTEM_WEBAUTHN_MANAGE_BTN_SAVE_LABEL'); + elSave.addEventListener('click', function (e) { + let elNewLabel = elInput.value; + + if (elNewLabel !== '') { + let postBackData = { + 'option': 'com_ajax', + 'group': 'system', + 'plugin': 'webauthn', + 'format': 'json', + 'encoding': 'json', + 'akaction': 'savelabel', + 'credential_id': credentialId, + 'new_label': elNewLabel + }; + + Joomla.request({ + url: post_url, + method: 'POST', + data: plgSystemWebauthnInterpolateParameters(postBackData), + onSuccess(rawResponse) { + let result = false; + + try { + result = JSON.parse(rawResponse); + } catch (e) { + result = (rawResponse === 'true'); + } + + if (result !== true) { + plgSystemWebauthnHandleCreationError( + Joomla.JText._('PLG_SYSTEM_WEBAUTHN_ERR_LABEL_NOT_SAVED') + ); + } + + //alert(Joomla.JText._('PLG_SYSTEM_WEBAUTHN_MSG_SAVED_LABEL')); + }, + onError: (xhr) => { + plgSystemWebauthnHandleCreationError( + Joomla.JText._('PLG_SYSTEM_WEBAUTHN_ERR_LABEL_NOT_SAVED') + + ' -- ' + xhr.status + ' ' + xhr.statusText + ); + } + }); + } + + elLabelTD.innerText = elNewLabel; + elEdit.disabled = false; + elDelete.disabled = false; + + return false; + }, false); + + let elCancel = document.createElement('button'); + elCancel.className = 'btn btn-danger btn-sm'; + elCancel.innerText = Joomla.JText._('PLG_SYSTEM_WEBAUTHN_MANAGE_BTN_CANCEL_LABEL'); + elCancel.addEventListener('click', function (e) { + elLabelTD.innerText = oldLabel; + elEdit.disabled = false; + elDelete.disabled = false; + + return false; + }, false); + + elLabelTD.innerHTML = ''; + elLabelTD.appendChild(elInput); + elLabelTD.appendChild(elSave); + elLabelTD.appendChild(elCancel); + elEdit.disabled = true; + elDelete.disabled = true; + + return false; +} + +/** + * Delete button + * + * @param {Element} that The button being clicked + * @param {String} store_id CSS ID for the element storing the configuration in its data + * properties + */ +function plgSystemWebauthnDelete(that, store_id) { + // Extract the configuration from the store + let elStore = document.getElementById(store_id); + + if (!elStore) { + return; + } + + let post_url = atob(elStore.dataset.postback_url); + + // Find the UI elements + let elTR = that.parentElement.parentElement; + let credentialId = elTR.dataset.credential_id; + let elTDs = elTR.querySelectorAll('td'); + let elButtonsTD = elTDs[1]; + let elButtons = elButtonsTD.querySelectorAll('button'); + let elEdit = elButtons[0]; + let elDelete = elButtons[1]; + + elEdit.disabled = true; + elDelete.disabled = true; + + // Delete the record + let postBackData = { + 'option': 'com_ajax', + 'group': 'system', + 'plugin': 'webauthn', + 'format': 'json', + 'encoding': 'json', + 'akaction': 'delete', + 'credential_id': credentialId + }; + + Joomla.request({ + url: post_url, + method: 'POST', + data: plgSystemWebauthnInterpolateParameters(postBackData), + onSuccess(rawResponse) { + let result = false; + + try { + result = JSON.parse(rawResponse); + } catch (e) { + result = (rawResponse === 'true'); + } + + if (result !== true) { + plgSystemWebauthnHandleCreationError( + Joomla.JText._('PLG_SYSTEM_WEBAUTHN_ERR_NOT_DELETED') + ); + + return; + } + + elTR.parentElement.removeChild(elTR); + }, + onError: (xhr) => { + elEdit.disabled = false; + elDelete.disabled = false; + plgSystemWebauthnHandleCreationError( + Joomla.JText._('PLG_SYSTEM_WEBAUTHN_ERR_NOT_DELETED') + + ' -- ' + xhr.status + ' ' + xhr.statusText + ); + } + }); + + return false; +} + +/** + * Converts a simple object containing query string parameters to a single, escaped query string. + * This method is a necessary evil since Joomla.request can only accept data as a string. + * + * @param object {object} A plain object containing the query parameters to pass + * @param prefix {string} Prefix for array-type parameters + * + * @returns {string} + */ +function plgSystemWebauthnInterpolateParameters(object, prefix) { + prefix = prefix || ''; + var encodedString = ''; + + for (var prop in object) { + if (object.hasOwnProperty(prop)) { + if (encodedString.length > 0) { + encodedString += '&'; + } + + if (typeof object[prop] !== 'object') { + if (prefix === '') { + encodedString += encodeURIComponent(prop) + '=' + encodeURIComponent(object[prop]); + } else { + encodedString += + encodeURIComponent(prefix) + '[' + encodeURIComponent(prop) + ']=' + encodeURIComponent( + object[prop]); + } + + continue; + } + + // Objects need special handling + encodedString += plgSystemWebauthnInterpolateParameters(object[prop], prop); + } + } + return encodedString; +} diff --git a/build/media_source/plg_system_webauthn/scss/button.scss b/build/media_source/plg_system_webauthn/scss/button.scss new file mode 100644 index 0000000000000..46c0af047e256 --- /dev/null +++ b/build/media_source/plg_system_webauthn/scss/button.scss @@ -0,0 +1,30 @@ +/*! + * @package Joomla.Plugin + * @subpackage System.updatenotification + * + * @copyright Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved. + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +button[class*=plg_system_webauthn_login_button] { + span[class*=icon] { + font-size: 1.25em; + vertical-align: sub; + display: inline-block; + width: 1em; + text-align: center; + } + + img[class*=icon] { + font-size: 1.5em; + vertical-align: middle; + display: inline-block; + width: 1.25em; + max-height: 1.25em; + text-align: center; + } + + span[class*=icon]:not(:last-child) { + margin-right: .5em; + } +} diff --git a/components/com_users/View/Login/HtmlView.php b/components/com_users/View/Login/HtmlView.php index 147978814b88e..d108025f0921e 100644 --- a/components/com_users/View/Login/HtmlView.php +++ b/components/com_users/View/Login/HtmlView.php @@ -70,6 +70,14 @@ class HtmlView extends BaseHtmlView */ protected $tfa = ''; + /** + * Additional buttons to show on the login page + * + * @var array + * @since 4.0.0 + */ + protected $extraButtons = []; + /** * Method to display the view. * @@ -105,6 +113,8 @@ public function display($tpl = null) $tfa = AuthenticationHelper::getTwoFactorMethods(); $this->tfa = is_array($tfa) && count($tfa) > 1; + $this->extraButtons = AuthenticationHelper::getLoginButtons('com-users-login__form'); + // Escape strings for HTML output $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx'), ENT_COMPAT, 'UTF-8'); diff --git a/components/com_users/tmpl/login/default_login.php b/components/com_users/tmpl/login/default_login.php index 429cb749a8946..9f19b9c6543c8 100644 --- a/components/com_users/tmpl/login/default_login.php +++ b/components/com_users/tmpl/login/default_login.php @@ -46,7 +46,7 @@ - +
form->renderFieldset('credentials', ['class' => 'com-users-login__input']); ?> @@ -68,6 +68,28 @@ + extraButtons as $button): ?> + + +