This commit is contained in:
Roland Gruber 2020-01-06 12:26:50 +01:00
parent 9e1e0634e6
commit 8a014f3a8a
5 changed files with 168 additions and 42 deletions

View File

@ -544,7 +544,7 @@ class PublicKeyCredentialSourceRepositorySQLite implements PublicKeyCredentialSo
*/ */
public function searchDevices(string $searchTerm) { public function searchDevices(string $searchTerm) {
$pdo = $this->getPDO(); $pdo = $this->getPDO();
$statement = $pdo->prepare('select * from ' . self::TABLE_NAME . ' where userId like :searchTerm'); $statement = $pdo->prepare('select * from ' . self::TABLE_NAME . ' where userId like :searchTerm order by userId,registrationTime');
$statement->execute(array( $statement->execute(array(
':searchTerm' => $searchTerm ':searchTerm' => $searchTerm
)); ));

View File

@ -548,7 +548,9 @@ input.markOk {
} }
div.lam-webauthn-results { div.lam-webauthn-results {
max-height: 10rem; max-height: 20rem;
overflow: auto;
padding: 1rem;
} }
/** /**

View File

@ -841,6 +841,14 @@ window.lam.form.autoTrim = function() {
window.lam.dialog = window.lam.dialog || {}; window.lam.dialog = window.lam.dialog || {};
/**
* Shows a dialog message.
*
* @param title dialog title
* @param okText ok button text
* @param divId DIV id with dialog content
* @param callbackFunction callback function (optional)
*/
window.lam.dialog.showMessage = function(title, okText, divId, callbackFunction) { window.lam.dialog.showMessage = function(title, okText, divId, callbackFunction) {
var buttonList = {}; var buttonList = {};
buttonList[okText] = function() { buttonList[okText] = function() {
@ -1388,14 +1396,14 @@ window.lam.webauthn.run = function(prefix) {
form.submit(); form.submit();
return; return;
}); });
var token = jQuery('#sec_token').val(); const token = jQuery('#sec_token').val();
// check for webauthn support // check for webauthn support
if (!navigator.credentials || (typeof(PublicKeyCredential) === "undefined")) { if (!navigator.credentials || (typeof(PublicKeyCredential) === "undefined")) {
jQuery('.webauthn-error').show(); jQuery('.webauthn-error').show();
return; return;
} }
var data = { const data = {
action: 'status', action: 'status',
jsonInput: '', jsonInput: '',
sec_token: token sec_token: token
@ -1407,7 +1415,25 @@ window.lam.webauthn.run = function(prefix) {
}) })
.done(function(jsonData) { .done(function(jsonData) {
if (jsonData.action === 'register') { if (jsonData.action === 'register') {
window.lam.webauthn.register(jsonData.registration); const successCallback = function (publicKeyCredential) {
const form = jQuery("#2faform");
const response = btoa(JSON.stringify(publicKeyCredential));
form.append('<input type="hidden" name="sig_response" value="' + response + '"/>');
form.submit();
};
const errorCallback = function(error) {
let errorDiv = jQuery('#generic-webauthn-error');
let buttonLabel = errorDiv.data('button');
let dialogTitle = errorDiv.data('title');
errorDiv.text(error.message);
window.lam.dialog.showMessage(dialogTitle,
buttonLabel,
'generic-webauthn-error',
function () {
jQuery('#btn_logout').click();
});
};
window.lam.webauthn.register(jsonData.registration, successCallback, errorCallback);
} }
else if (jsonData.action === 'authenticate') { else if (jsonData.action === 'authenticate') {
window.lam.webauthn.authenticate(jsonData.authentication); window.lam.webauthn.authenticate(jsonData.authentication);
@ -1422,23 +1448,27 @@ window.lam.webauthn.run = function(prefix) {
* Performs a webauthn registration. * Performs a webauthn registration.
* *
* @param publicKey registration object * @param publicKey registration object
* @param successCallback callback function in case of all went fine
* @param errorCallback callback function in case of an error
*/ */
window.lam.webauthn.register = function(publicKey) { window.lam.webauthn.register = function(publicKey, successCallback, errorCallback) {
publicKey.challenge = Uint8Array.from(window.atob(publicKey.challenge), c=>c.charCodeAt(0)); if (!(publicKey.challenge instanceof Uint8Array)) {
publicKey.user.id = Uint8Array.from(window.atob(publicKey.user.id), c=>c.charCodeAt(0)); publicKey.challenge = Uint8Array.from(window.atob(publicKey.challenge), c=>c.charCodeAt(0));
publicKey.rp.icon = window.location.href.substring(0, window.location.href.lastIndexOf("/")) + publicKey.rp.icon; publicKey.user.id = Uint8Array.from(window.atob(publicKey.user.id), c=>c.charCodeAt(0));
if (publicKey.excludeCredentials) { publicKey.rp.icon = window.location.href.substring(0, window.location.href.lastIndexOf("/")) + publicKey.rp.icon;
for (let i = 0; i < publicKey.excludeCredentials.length; i++) { if (publicKey.excludeCredentials) {
let idOrig = publicKey.excludeCredentials[i]['id']; for (let i = 0; i < publicKey.excludeCredentials.length; i++) {
idOrig = idOrig.replace(/-/g, "+").replace(/_/g, "/"); let idOrig = publicKey.excludeCredentials[i]['id'];
let idOrigDecoded = atob(idOrig); idOrig = idOrig.replace(/-/g, "+").replace(/_/g, "/");
let idArray = Uint8Array.from(idOrigDecoded, c => c.charCodeAt(0)) let idOrigDecoded = atob(idOrig);
publicKey.excludeCredentials[i]['id'] = idArray; let idArray = Uint8Array.from(idOrigDecoded, c => c.charCodeAt(0))
publicKey.excludeCredentials[i]['id'] = idArray;
}
} }
} }
navigator.credentials.create({publicKey}) navigator.credentials.create({publicKey})
.then(function (data) { .then(function (data) {
let publicKeyCredential = { const publicKeyCredential = {
id: data.id, id: data.id,
type: data.type, type: data.type,
rawId: window.lam.webauthn.arrayToBase64String(new Uint8Array(data.rawId)), rawId: window.lam.webauthn.arrayToBase64String(new Uint8Array(data.rawId)),
@ -1447,22 +1477,10 @@ window.lam.webauthn.register = function(publicKey) {
attestationObject: window.lam.webauthn.arrayToBase64String(new Uint8Array(data.response.attestationObject)) attestationObject: window.lam.webauthn.arrayToBase64String(new Uint8Array(data.response.attestationObject))
} }
}; };
let form = jQuery("#2faform"); successCallback(publicKeyCredential);
let response = btoa(JSON.stringify(publicKeyCredential));
form.append('<input type="hidden" name="sig_response" value="' + response + '"/>');
form.submit();
}, function (error) { }, function (error) {
console.log(error.message); console.log(error.message);
let errorDiv = jQuery('#generic-webauthn-error'); errorCallback(error);
let buttonLabel = errorDiv.data('button');
let dialogTitle = errorDiv.data('title');
errorDiv.text(error.message);
window.lam.dialog.showMessage(dialogTitle,
buttonLabel,
'generic-webauthn-error',
function () {
jQuery('#btn_logout').click();
});
}); });
} }
@ -1598,7 +1616,16 @@ window.lam.webauthn.removeDevice = function(event) {
window.lam.webauthn.removeOwnDevice = function(event) { window.lam.webauthn.removeOwnDevice = function(event) {
event.preventDefault(); event.preventDefault();
const element = jQuery(event.target); const element = jQuery(event.target);
window.lam.webauthn.removeDeviceDialog(element, 'webauthnOwnDevices'); const successCallback = function () {
const form = jQuery("#webauthnform");
jQuery('<input>').attr({
type: 'hidden',
name: 'removed',
value: 'true'
}).appendTo(form);
form.submit();
};
window.lam.webauthn.removeDeviceDialog(element, 'webauthnOwnDevices', successCallback);
return false; return false;
} }
@ -1607,15 +1634,16 @@ window.lam.webauthn.removeOwnDevice = function(event) {
* *
* @param element delete button * @param element delete button
* @param action action for request (delete|deleteOwn) * @param action action for request (delete|deleteOwn)
* @param successCallback callback if all was fine (optional)
*/ */
window.lam.webauthn.removeDeviceDialog = function(element, action) { window.lam.webauthn.removeDeviceDialog = function(element, action, successCallback) {
const dialogTitle = element.data('dialogtitle'); const dialogTitle = element.data('dialogtitle');
const okText = element.data('oktext'); const okText = element.data('oktext');
const cancelText = element.data('canceltext'); const cancelText = element.data('canceltext');
let buttonList = {}; let buttonList = {};
buttonList[okText] = function() { buttonList[okText] = function() {
jQuery('#webauthnDeleteConfirm').dialog('close'); jQuery('#webauthnDeleteConfirm').dialog('close');
window.lam.webauthn.sendRemoveDeviceRequest(element, action); window.lam.webauthn.sendRemoveDeviceRequest(element, action, successCallback);
}; };
buttonList[cancelText] = function() { buttonList[cancelText] = function() {
jQuery(this).dialog("close"); jQuery(this).dialog("close");
@ -1634,8 +1662,9 @@ window.lam.webauthn.removeDeviceDialog = function(element, action) {
* *
* @param element button element * @param element button element
* @param action action (delete|deleteOwn) * @param action action (delete|deleteOwn)
* @param successCallback callback if all was fine (optional)
*/ */
window.lam.webauthn.sendRemoveDeviceRequest = function(element, action) { window.lam.webauthn.sendRemoveDeviceRequest = function(element, action, successCallback) {
const dn = element.data('dn'); const dn = element.data('dn');
const credential = element.data('credential'); const credential = element.data('credential');
const resultDiv = jQuery('#webauthn_results'); const resultDiv = jQuery('#webauthn_results');
@ -1653,13 +1682,51 @@ window.lam.webauthn.sendRemoveDeviceRequest = function(element, action) {
data: data data: data
}) })
.done(function(jsonData) { .done(function(jsonData) {
resultDiv.html(jsonData.content); if (successCallback) {
successCallback();
}
else {
resultDiv.html(jsonData.content);
}
}) })
.fail(function() { .fail(function() {
console.log('Webauthn device deletion failed'); console.log('Webauthn device deletion failed');
}); });
} }
/**
* Registers a user's own webauthn device.
*
* @param event click event
*/
window.lam.webauthn.registerOwnDevice = function(event) {
event.preventDefault();
const element = jQuery(event.target);
const dn = element.data('dn');
const tokenValue = element.data('sec_token_value');
const tokenName = element.data('sec_token_name');
const publicKey = element.data('publickey');
const successCallback = function (publicKeyCredential) {
const form = jQuery("#webauthnform");
const response = btoa(JSON.stringify(publicKeyCredential));
const registrationData = jQuery('#registrationData');
registrationData.val(response);
form.submit();
};
const errorCallback = function (error) {
let errorDiv = jQuery('#generic-webauthn-error');
let buttonLabel = errorDiv.data('button');
let dialogTitle = errorDiv.data('title');
errorDiv.text(error.message);
window.lam.dialog.showMessage(dialogTitle,
buttonLabel,
'generic-webauthn-error'
);
};
window.lam.webauthn.register(publicKey, successCallback, errorCallback);
return false;
}
jQuery(document).ready(function() { jQuery(document).ready(function() {
window.lam.gui.equalHeight(); window.lam.gui.equalHeight();
window.lam.form.autoTrim(); window.lam.form.autoTrim();

View File

@ -342,7 +342,7 @@ class Ajax {
} }
if ($action === 'delete') { if ($action === 'delete') {
$credentialId = $_POST['credentialId']; $credentialId = $_POST['credentialId'];
$this->manageWebauthnDevicesDelete($dn, $credentialId); $this->manageWebauthnDevicesDelete($sessionDn, $credentialId);
} }
} }

View File

@ -1,14 +1,18 @@
<?php <?php
namespace LAM\TOOLS\WEBAUTHN; namespace LAM\TOOLS\WEBAUTHN;
use \htmlButton; use \htmlButton;
use htmlDiv; use \htmlDiv;
use htmlGroup; use \htmlGroup;
use htmlHiddenInput;
use \htmlOutputText; use \htmlOutputText;
use \htmlResponsiveRow; use \htmlResponsiveRow;
use \htmlResponsiveTable; use \htmlResponsiveTable;
use \htmlSpacer;
use \htmlStatusMessage; use \htmlStatusMessage;
use \htmlTitle; use \htmlTitle;
use \LAM\LOGIN\WEBAUTHN\PublicKeyCredentialSourceRepositorySQLite; use \LAM\LOGIN\WEBAUTHN\PublicKeyCredentialSourceRepositorySQLite;
use LAM\LOGIN\WEBAUTHN\WebauthnManager;
use Webauthn\PublicKeyCredentialCreationOptions;
/* /*
@ -48,6 +52,7 @@ include_once __DIR__ . '/../../lib/webauthn.inc';
// start session // start session
startSecureSession(); startSecureSession();
enforceUserIsLoggedIn(); enforceUserIsLoggedIn();
validateSecurityToken();
checkIfToolIsActive('toolWebauthn'); checkIfToolIsActive('toolWebauthn');
@ -55,22 +60,43 @@ setlanguage();
include __DIR__ . '/../../lib/adminHeader.inc'; include __DIR__ . '/../../lib/adminHeader.inc';
echo '<div class="user-bright smallPaddingContent">'; echo '<div class="user-bright smallPaddingContent">';
echo "<form action=\"webauthn.php\" method=\"post\">\n"; echo "<form id='webauthnform' action=\"webauthn.php\" method=\"post\">\n";
$tabindex = 1; $tabindex = 1;
$container = new htmlResponsiveRow(); $container = new htmlResponsiveRow();
$container->add(new htmlTitle(_("Webauthn devices")), 12); $container->add(new htmlTitle(_("Webauthn devices")), 12);
$webauthnManager = new WebauthnManager();
$userDn = $_SESSION['ldap']->getUserName(); $userDn = $_SESSION['ldap']->getUserName();
$database = new PublicKeyCredentialSourceRepositorySQLite(); $database = new PublicKeyCredentialSourceRepositorySQLite();
$results = $database->searchDevices($userDn); showRemoveMessage($container);
addNewDevice($container, $webauthnManager);
$container->addVerticalSpacer('0.5rem'); $container->addVerticalSpacer('0.5rem');
$container->add(new htmlHiddenInput('registrationData', ''), 12);
$errorMessageDiv = new htmlDiv('generic-webauthn-error', new htmlOutputText(''));
$errorMessageDiv->addDataAttribute('button', _('Ok'));
$errorMessageDiv->addDataAttribute('title', _('Webauthn failed'));
$container->add($errorMessageDiv, 12);
$buttonGroup = new htmlGroup(); $buttonGroup = new htmlGroup();
$registerButton = new htmlButton('register', _('Register new device'));
$registerButton->addDataAttribute('dn', $userDn);
$registerButton->addDataAttribute('sec_token_value', getSecurityTokenValue());
$registerButton->addDataAttribute('sec_token_name', getSecurityTokenName());
$registration = $webauthnManager->getRegistrationObject($userDn, false);
$registrationJson = json_encode($registration);
$_SESSION['webauthn_registration'] = $registrationJson;
$registerButton->addDataAttribute('publickey', $registrationJson);
$registerButton->setIconClass('createButton');
$registerButton->setOnClick('window.lam.webauthn.registerOwnDevice(event);');
$buttonGroup->addElement($registerButton);
$buttonGroup->addElement(new htmlSpacer('1rem', null));
$reloadButton = new htmlButton('reload', _('Reload')); $reloadButton = new htmlButton('reload', _('Reload'));
$reloadButton->setIconClass('refreshButton'); $reloadButton->setIconClass('refreshButton');
$buttonGroup->addElement($reloadButton); $buttonGroup->addElement($reloadButton);
$container->add($buttonGroup, 12); $container->add($buttonGroup, 12);
$container->addVerticalSpacer('2rem'); $container->addVerticalSpacer('2rem');
$results = $database->searchDevices($userDn);
if (empty($results)) { if (empty($results)) {
$container->add(new htmlStatusMessage('INFO', _('No devices found.')), 12); $container->add(new htmlStatusMessage('INFO', _('No devices found.')), 12);
} }
@ -107,6 +133,7 @@ $container->addVerticalSpacer('2rem');
$confirmationDiv = new htmlDiv('webauthnDeleteConfirm', new htmlOutputText(_('Do you really want to remove this device?')), array('hidden')); $confirmationDiv = new htmlDiv('webauthnDeleteConfirm', new htmlOutputText(_('Do you really want to remove this device?')), array('hidden'));
$container->add($confirmationDiv, 12); $container->add($confirmationDiv, 12);
addSecurityTokenToMetaHTML($container);
parseHtml(null, $container, array(), false, $tabindex, 'user'); parseHtml(null, $container, array(), false, $tabindex, 'user');
@ -114,4 +141,34 @@ echo '</form>';
echo '</div>'; echo '</div>';
include __DIR__ . '/../../lib/adminFooter.inc'; include __DIR__ . '/../../lib/adminFooter.inc';
?> /**
* Checks if a new device should be registered and adds it.
*
* @param htmlResponsiveRow $container row
* @param WebauthnManager $webauthnManager webauthn manager
*/
function addNewDevice($container, $webauthnManager) {
if (empty($_POST['registrationData'])) {
return;
}
$registrationData = base64_decode($_POST['registrationData']);
$registrationObject = PublicKeyCredentialCreationOptions::createFromString($_SESSION['webauthn_registration']);
$success = $webauthnManager->storeNewRegistration($registrationObject, $registrationData);
if ($success) {
$container->add(new htmlStatusMessage('INFO', _('The device was registered.')), 12);
}
else {
$container->add(new htmlStatusMessage('ERROR', _('The device failed to register.')), 12);
}
}
/**
* Shows the message if a device was removed.
*
* @param htmlResponsiveRow $container row
*/
function showRemoveMessage($container) {
if (!empty($_POST['removed']) && ($_POST['removed'] === 'true')) {
$container->add(new htmlStatusMessage('INFO', _('The device was deleted.')), 12);
}
}