Skip to content

Commit

Permalink
Passwordless authentication with WebAuthn
Browse files Browse the repository at this point in the history
Refactor button logic to spit out button definitions for login modules
and the user component instead of static HTML.
  • Loading branch information
Nicholas K. Dionysopoulos committed Jul 25, 2019
1 parent 36bd1a7 commit f4e36ad
Show file tree
Hide file tree
Showing 19 changed files with 1,048 additions and 642 deletions.
1 change: 0 additions & 1 deletion .gitignore
Expand Up @@ -20,7 +20,6 @@
/.php_cs
/.php_cs.cache
/media
!/media/plg_system_webauthn

# Template CSS files generated by NPM.
/administrator/templates/atum/css
Expand Down
31 changes: 6 additions & 25 deletions administrator/language/en-GB/en-GB.plg_system_webauthn.ini
Expand Up @@ -6,31 +6,12 @@
PLG_SYSTEM_WEBAUTHN="System - WebAuthn Passwordless Login"
PLG_SYSTEM_WEBAUTHN_DESCRIPTION="Enables passwordless authentication using the W3C Web Authentication (WebAuthn) API."

PLG_SYSTEM_WEBAUTHN_CONFIG_LOGINMODULES_LABEL="Frontend Login Modules' Names"
PLG_SYSTEM_WEBAUTHN_CONFIG_LOGINMODULES_DESC="Enter the comma separated list of names of frontend modules. The passwordless login buttons will be appended to them automatically, e.g. <code>mod_login, mod_ajaxlogin</code>. Set this to <code>none</code> to prevent this behavior. In this case you must make template overrides according to the documentation."
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_CONFIG_BACKENDLOGINMODULES_LABEL="Backend Login Modules' Names"
PLG_SYSTEM_WEBAUTHN_CONFIG_BACKENDLOGINMODULES_DESC="Enter the comma separated list of names of backend modules. The passwordless login buttons will be appended to them automatically, e.g. <code>mod_login, mod_ajaxlogin</code>. Set this to <code>none</code> to prevent this behavior. In this case you must make template overrides according to the documentation."

PLG_SYSTEM_WEBAUTHN_CONFIG_INTERCEPTLOGIN_LABEL="Add buttons to login page"
PLG_SYSTEM_WEBAUTHN_CONFIG_INTERCEPTLOGIN_DESC="When enabled the plugin will automatically append the passwordless login buttons to the end of the login page created by Joomla's com_users. Otherwise you must make template overrides according to the documentation."

PLG_SYSTEM_WEBAUTHN_CONFIG_RELOCATE_LABEL="Passwordless Login button placement"
PLG_SYSTEM_WEBAUTHN_CONFIG_RELOCATE_OPT_NEXTTOLOGIN="Next to Login button"
PLG_SYSTEM_WEBAUTHN_CONFIG_RELOCATE_OPT_ENDOFMODULE="End of module content"
PLG_SYSTEM_WEBAUTHN_CONFIG_RELOCATE_DESC="The various Joomla login modules, including Joomla's own, do not offer a way to place additional buttons next to the Login button. If you use the 'Next to Login button' option WebAuthn Passwordless Login will use Javascript to move its button next to what is <i>possibly</i> the Login action button of the module. If that fails, or if you use the 'End of module content' option, the Passwordless Login button will be placed at the end of the module's content."

COM_PLUGINS_WEBAUTHN_EXPERT_FIELDSET_LABEL="Expert Settings"

PLG_SYSTEM_WEBAUTHN_CONFIG_RELOCATESELECTORS_LABEL="CSS Selectors for button placement"
PLG_SYSTEM_WEBAUTHN_CONFIG_RELOCATESELECTORS_DESC="Used with the 'Next to Login button' option in the Basic section. Comma- or newline-separated list of CSS selectors to locate the Login button inside the Login module. The default selectors should work with most login modules."

PLG_SYSTEM_WEBAUTHN_LOGIN_LABEL="WebAuthn Passwordless Login"
PLG_SYSTEM_WEBAUTHN_LOGIN_DESC="Login without a password using the W3C Web Authentication standard. You need to have already set up passwordless authentication in your user profile."

PLG_SYSTEM_WEBAUTHN_HEADER="WebAuthn Passwordless Login"
PLG_SYSTEM_WEBAUTHN_FIELD_LABEL="W3C Web Authentication Passwordless 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 and a FIDO2 certified security key)."
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."
Expand Down Expand Up @@ -62,4 +43,4 @@ 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."
PLG_SYSTEM_WEBAUTHN_ERR_INVALID_USERNAME="The specified username does not correspond to a user account that has enabled passwordless login on this site."
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
214 changes: 214 additions & 0 deletions build/media_source/plg_system_webauthn/js/login.es6.js
@@ -0,0 +1,214 @@
/**
* @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 plg_system_webauthn_findField(elForm, fieldSelector)
{
let elInputs = elForm.querySelectorAll(fieldSelector);

if (!elInputs.length)
{
return null;
}

return elInputs[0];
}

/**
* Walks the DOM outwards (towards the parents) to find the form innerElement is located in. Then it looks inside the
* form for the first element that matches the fieldSelector CSS selector.
*
* @param {Element} innerElement The innerElement that's inside or adjacent to the form.
* @param {String} fieldSelector The CSS selector to locate the field
*
* @returns {null|Element} NULL when no element is found
*/
function plg_system_webauthn_lookInParentElementsForField(innerElement, fieldSelector)
{
var elElement = innerElement.parentElement;
var elInput = null;

while (true)
{
if (elElement === undefined)
{
return null;
}

if (elElement.nodeName === "FORM")
{
elInput = plg_system_webauthn_findField(elElement, fieldSelector);

if (elInput !== null)
{
return elInput;
}

break;
}

var elForms = elElement.querySelectorAll("form");

if (elForms.length)
{
for (var i = 0; i < elForms.length; i++)
{
elInput = plg_system_webauthn_findField(elForms[i], fieldSelector);

if (elInput !== null)
{
return elInput;
}
}

break;
}

if (!elElement.parentElement)
{
break;
}

elElement = elElement.parentElement;
}

return null;
}

/**
* Initialize the passwordless login, going through the server to get the registered certificates for the user.
*
* @param {Element} that The button which was clicked
* @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 plg_system_webauthn_login(that, callback_url)
{
// Get the username
let elUsername = plg_system_webauthn_lookInParentElementsForField(that, "input[name=username]");
let elReturn = plg_system_webauthn_lookInParentElementsForField(that, "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,
};

window.jQuery.ajax({
type: "POST",
url: callback_url,
data: postBackData,
dataType: "json"
})
.done(function (jsonData) {
plg_system_webauthn_handle_login_challenge(jsonData, callback_url);
})
.fail(function (error) {
plg_system_webauthn_handle_login_error(error.status + " " + error.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 plg_system_webauthn_handle_login_challenge(publicKey, callback_url)
{
function arrayToBase64String(a)
{
return btoa(String.fromCharCode(...a));
}

if (!publicKey.challenge)
{
plg_system_webauthn_handle_login_error(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);
plg_system_webauthn_handle_login_error(error);
});
}

/**
* A simple error handler.
*
* @param {String} message
*/
function plg_system_webauthn_handle_login_error(message)
{
alert(message);

console.log(message);
}

0 comments on commit f4e36ad

Please sign in to comment.