Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Passwordless authentication with WebAuthn
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
Showing
19 changed files
with
1,048 additions
and
642 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |
Oops, something went wrong.