Skip to content

Commit

Permalink
Passwordless authentication with WebAuthn
Browse files Browse the repository at this point in the history
Simplify the login JS a tad
  • Loading branch information
Nicholas K. Dionysopoulos committed Jul 25, 2019
1 parent d3ac455 commit 9396dee
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 104 deletions.
169 changes: 74 additions & 95 deletions build/media_source/plg_system_webauthn/js/login.es6.js
Expand Up @@ -27,124 +27,103 @@ function plg_system_webauthn_findField(elForm, fieldSelector)
}

/**
* 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.
* Find a form field described the CSS selector fieldSelector. The field must be inside a <form> element which is either
* the outerElement itself or enclosed by outerElement.
*
* @param {Element} innerElement The innerElement that's inside or adjacent to the form.
* @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 plg_system_webauthn_lookInParentElementsForField(innerElement, fieldSelector)
function plg_system_webauthn_lookForField(outerElement, fieldSelector)
{
var elElement = innerElement.parentElement;
var elElement = outerElement.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;
}
}
if (elElement.nodeName === "FORM")
{
elInput = plg_system_webauthn_findField(elElement, fieldSelector);

break;
}
return elInput;
}

if (!elElement.parentElement)
{
break;
}
var elForms = elElement.querySelectorAll("form");

elElement = elElement.parentElement;
}
if (elForms.length)
{
for (var i = 0; i < elForms.length; i++)
{
elInput = plg_system_webauthn_findField(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 {Element} that The button which was clicked
* @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 plg_system_webauthn_login(that, callback_url)
function plg_system_webauthn_login(form_id, 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;
// Get the username
let elFormContainer = document.getElementById(form_id);
let elUsername = plg_system_webauthn_lookForField(elFormContainer, "input[name=username]");
let elReturn = plg_system_webauthn_lookForField(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,
};

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;
}

/**
Expand Down
9 changes: 4 additions & 5 deletions libraries/src/Helper/AuthenticationHelper.php
Expand Up @@ -71,7 +71,7 @@ public static function getTwoFactorMethods()
*
* The onUserLoginButtons event handlers must conform to the following method definition:
*
* public function onUserLoginButtons(string $moduleId, string $usernameFieldId): array
* public function onUserLoginButtons(string $formId): array
*
* The onUserLoginButtons event handlers must return a simple array containing 0 or more button definitions.
*
Expand All @@ -84,14 +84,13 @@ public static function getTwoFactorMethods()
* - image [optional] An image path for an optional icon displayed before the label
* - class [optional] CSS class(es) to be added to the button
*
* @param string $moduleId The HTML ID of the module container.
* @param string $usernameFieldId The HTML ID of the login module's username field.
* @param string $formId The HTML ID of the login form container.
*
* @return array Button definitions.
*
* @since 4.0.0
*/
public static function getLoginButtons(string $moduleId, string $usernameFieldId): array
public static function getLoginButtons(string $formId): array
{
// Get all the User plugins.
PluginHelper::importPlugin('user');
Expand All @@ -107,7 +106,7 @@ public static function getLoginButtons(string $moduleId, string $usernameFieldId
return [];
}

$results = $app->triggerEvent('onUserLoginButtons', [$moduleId, $usernameFieldId]);
$results = $app->triggerEvent('onUserLoginButtons', [$formId]);
$buttons = [];

foreach ($results as $result)
Expand Down
Expand Up @@ -107,16 +107,15 @@ private function mustDisplayButton(): bool
/**
* Creates additional login buttons
*
* @param string $moduleId The HTML ID of the module we are enclosed in
* @param string $usernameFieldId The HTML ID of the username field
* @param string $form The HTML ID of the form we are enclosed in
*
* @return array
*
* @throws Exception
*
* @see AuthenticationHelper::getLoginButtons()
*/
public function onUserLoginButtons(string $moduleId, string $usernameFieldId): array
public function onUserLoginButtons(string $form): array
{
// If we determined we should not inject a button return early
if (!$this->mustDisplayButton())
Expand All @@ -135,7 +134,7 @@ public function onUserLoginButtons(string $moduleId, string $usernameFieldId): a

// Set up the JavaScript callback
$url = $uri->toString();
$onClick = "return plg_system_webauthn_login(this, '{$url}')";
$onClick = "return plg_system_webauthn_login('{$form}', '{$url}')";

return [
[
Expand Down

0 comments on commit 9396dee

Please sign in to comment.