diff --git a/lam/lib/webauthn.inc b/lam/lib/webauthn.inc index 21e4fc6b..e396fec1 100644 --- a/lam/lib/webauthn.inc +++ b/lam/lib/webauthn.inc @@ -544,7 +544,7 @@ class PublicKeyCredentialSourceRepositorySQLite implements PublicKeyCredentialSo */ public function searchDevices(string $searchTerm) { $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( ':searchTerm' => $searchTerm )); diff --git a/lam/style/500_layout.css b/lam/style/500_layout.css index 86a508ea..f881bd7e 100644 --- a/lam/style/500_layout.css +++ b/lam/style/500_layout.css @@ -548,7 +548,9 @@ input.markOk { } div.lam-webauthn-results { - max-height: 10rem; + max-height: 20rem; + overflow: auto; + padding: 1rem; } /** diff --git a/lam/templates/lib/500_lam.js b/lam/templates/lib/500_lam.js index 7a2856b1..2d716ede 100644 --- a/lam/templates/lib/500_lam.js +++ b/lam/templates/lib/500_lam.js @@ -841,6 +841,14 @@ window.lam.form.autoTrim = function() { 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) { var buttonList = {}; buttonList[okText] = function() { @@ -1388,14 +1396,14 @@ window.lam.webauthn.run = function(prefix) { form.submit(); return; }); - var token = jQuery('#sec_token').val(); + const token = jQuery('#sec_token').val(); // check for webauthn support if (!navigator.credentials || (typeof(PublicKeyCredential) === "undefined")) { jQuery('.webauthn-error').show(); return; } - var data = { + const data = { action: 'status', jsonInput: '', sec_token: token @@ -1407,7 +1415,25 @@ window.lam.webauthn.run = function(prefix) { }) .done(function(jsonData) { 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(''); + 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') { window.lam.webauthn.authenticate(jsonData.authentication); @@ -1422,23 +1448,27 @@ window.lam.webauthn.run = function(prefix) { * Performs a webauthn registration. * * @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) { - 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)); - publicKey.rp.icon = window.location.href.substring(0, window.location.href.lastIndexOf("/")) + publicKey.rp.icon; - if (publicKey.excludeCredentials) { - for (let i = 0; i < publicKey.excludeCredentials.length; i++) { - let idOrig = publicKey.excludeCredentials[i]['id']; - idOrig = idOrig.replace(/-/g, "+").replace(/_/g, "/"); - let idOrigDecoded = atob(idOrig); - let idArray = Uint8Array.from(idOrigDecoded, c => c.charCodeAt(0)) - publicKey.excludeCredentials[i]['id'] = idArray; +window.lam.webauthn.register = function(publicKey, successCallback, errorCallback) { + if (!(publicKey.challenge instanceof Uint8Array)) { + 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)); + publicKey.rp.icon = window.location.href.substring(0, window.location.href.lastIndexOf("/")) + publicKey.rp.icon; + if (publicKey.excludeCredentials) { + for (let i = 0; i < publicKey.excludeCredentials.length; i++) { + let idOrig = publicKey.excludeCredentials[i]['id']; + idOrig = idOrig.replace(/-/g, "+").replace(/_/g, "/"); + let idOrigDecoded = atob(idOrig); + let idArray = Uint8Array.from(idOrigDecoded, c => c.charCodeAt(0)) + publicKey.excludeCredentials[i]['id'] = idArray; + } } } navigator.credentials.create({publicKey}) .then(function (data) { - let publicKeyCredential = { + const publicKeyCredential = { id: data.id, type: data.type, 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)) } }; - let form = jQuery("#2faform"); - let response = btoa(JSON.stringify(publicKeyCredential)); - form.append(''); - form.submit(); + successCallback(publicKeyCredential); }, function (error) { console.log(error.message); - 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(); - }); + errorCallback(error); }); } @@ -1598,7 +1616,16 @@ window.lam.webauthn.removeDevice = function(event) { window.lam.webauthn.removeOwnDevice = function(event) { event.preventDefault(); const element = jQuery(event.target); - window.lam.webauthn.removeDeviceDialog(element, 'webauthnOwnDevices'); + const successCallback = function () { + const form = jQuery("#webauthnform"); + jQuery('').attr({ + type: 'hidden', + name: 'removed', + value: 'true' + }).appendTo(form); + form.submit(); + }; + window.lam.webauthn.removeDeviceDialog(element, 'webauthnOwnDevices', successCallback); return false; } @@ -1607,15 +1634,16 @@ window.lam.webauthn.removeOwnDevice = function(event) { * * @param element delete button * @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 okText = element.data('oktext'); const cancelText = element.data('canceltext'); let buttonList = {}; buttonList[okText] = function() { jQuery('#webauthnDeleteConfirm').dialog('close'); - window.lam.webauthn.sendRemoveDeviceRequest(element, action); + window.lam.webauthn.sendRemoveDeviceRequest(element, action, successCallback); }; buttonList[cancelText] = function() { jQuery(this).dialog("close"); @@ -1634,8 +1662,9 @@ window.lam.webauthn.removeDeviceDialog = function(element, action) { * * @param element button element * @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 credential = element.data('credential'); const resultDiv = jQuery('#webauthn_results'); @@ -1653,13 +1682,51 @@ window.lam.webauthn.sendRemoveDeviceRequest = function(element, action) { data: data }) .done(function(jsonData) { - resultDiv.html(jsonData.content); + if (successCallback) { + successCallback(); + } + else { + resultDiv.html(jsonData.content); + } }) .fail(function() { 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() { window.lam.gui.equalHeight(); window.lam.form.autoTrim(); diff --git a/lam/templates/misc/ajax.php b/lam/templates/misc/ajax.php index 3cda46f1..4e86477b 100644 --- a/lam/templates/misc/ajax.php +++ b/lam/templates/misc/ajax.php @@ -342,7 +342,7 @@ class Ajax { } if ($action === 'delete') { $credentialId = $_POST['credentialId']; - $this->manageWebauthnDevicesDelete($dn, $credentialId); + $this->manageWebauthnDevicesDelete($sessionDn, $credentialId); } } diff --git a/lam/templates/tools/webauthn.php b/lam/templates/tools/webauthn.php index 3b81b592..fb07bdda 100644 --- a/lam/templates/tools/webauthn.php +++ b/lam/templates/tools/webauthn.php @@ -1,14 +1,18 @@ '; -echo "
\n"; +echo "\n"; $tabindex = 1; $container = new htmlResponsiveRow(); $container->add(new htmlTitle(_("Webauthn devices")), 12); +$webauthnManager = new WebauthnManager(); + $userDn = $_SESSION['ldap']->getUserName(); $database = new PublicKeyCredentialSourceRepositorySQLite(); -$results = $database->searchDevices($userDn); +showRemoveMessage($container); +addNewDevice($container, $webauthnManager); $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(); +$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->setIconClass('refreshButton'); $buttonGroup->addElement($reloadButton); $container->add($buttonGroup, 12); $container->addVerticalSpacer('2rem'); +$results = $database->searchDevices($userDn); if (empty($results)) { $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')); $container->add($confirmationDiv, 12); +addSecurityTokenToMetaHTML($container); parseHtml(null, $container, array(), false, $tabindex, 'user'); @@ -114,4 +141,34 @@ echo '
'; echo ''; 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); + } +}