diff --git a/lam/HISTORY b/lam/HISTORY index f6e2038d9..383ed05b4 100644 --- a/lam/HISTORY +++ b/lam/HISTORY @@ -1,7 +1,7 @@ December 2024 9.0 - Unix users: allow to create group with same name via account profile (#332) - Group of (unique) names, organisational roles: added member/owner count to PDF fields - - Usability improvements (342, 350) + - Usability improvements (342, 350, 372) - LAM Pro: -> Request access: added comment field for owners/approvers (339) -> Custom scripts: support custom label for module (329) diff --git a/lam/lib/account.inc b/lam/lib/account.inc index 58ce5d2f3..4ba08b22d 100644 --- a/lam/lib/account.inc +++ b/lam/lib/account.inc @@ -1272,6 +1272,27 @@ function compareLDAPEntriesByDn(array $a, array $b): int { return compareDN($a['dn'], $b['dn']); } +/** + * Does a Base64 encoding that is URL safe. + * + * @param string $data input + * @return string encoded output + */ +function lam_base64url_encode(string $data): string { + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); +} + +/** + * Does a Base64 decoding that is URL safe. + * + * @param string $data encoded input + * @return string decoded output + */ +function lam_base64url_decode(string $data): string { + return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '='), true); +} + + /** * Formats an LDAP time string (e.g. from createTimestamp). * diff --git a/lam/lib/baseModule.inc b/lam/lib/baseModule.inc index 70a48aa2a..1c7bfa619 100644 --- a/lam/lib/baseModule.inc +++ b/lam/lib/baseModule.inc @@ -2160,7 +2160,7 @@ abstract class baseModule { * * Calling this method does not require the existence of an enclosing {@link accountContainer}. * - * @param string $fields input fields + * @param array $fields input fields * @param array $attributes LDAP attributes * @param boolean $passwordChangeOnly indicates that the user is only allowed to change his password and no LDAP content is readable * @param array $readOnlyFields list of read-only fields diff --git a/lam/lib/webauthn.inc b/lam/lib/webauthn.inc index a109a35da..f9bc0ae78 100644 --- a/lam/lib/webauthn.inc +++ b/lam/lib/webauthn.inc @@ -608,6 +608,33 @@ abstract class PublicKeyCredentialSourceRepositoryBase implements PublicKeyCrede return $devices; } + /** + * Performs a full-text search on the user names and returns all devices found. + * + * @param string $userDn user DN + * @return array list of devices array('dn' => ..., 'credentialId' => ..., 'lastUseTime' => ..., 'registrationTime' => ...) + */ + public function getAllUserDevices(string $userDn) { + $pdo = $this->getPDO(); + $statement = $pdo->prepare('select * from ' . $this->getTableName() . ' where userDN = :userDn order by userId,registrationTime'); + $statement->execute([ + ':userDn' => $userDn + ]); + $results = $statement->fetchAll(); + $devices = []; + foreach ($results as $result) { + $name = !empty($result['name']) ? $result['name'] : ''; + $devices[] = [ + 'dn' => $result['userDN'], + 'credentialId' => $result['credentialId'], + 'lastUseTime' => $result['lastUseTime'], + 'registrationTime' => $result['registrationTime'], + 'name' => $name + ]; + } + return $devices; + } + /** * Deletes a single device from the database. * diff --git a/lam/templates/lib/500_lam.js b/lam/templates/lib/500_lam.js index efecfaac8..e5dc1d7c0 100644 --- a/lam/templates/lib/500_lam.js +++ b/lam/templates/lib/500_lam.js @@ -2152,7 +2152,7 @@ window.lam.webauthn.removeOwnDevice = function(event, isSelfService) { if (isSelfService) { action = action + '&selfservice=true&module=webauthn&scope=user'; } - window.lam.webauthn.removeDeviceDialog(element, action, successCallback); + window.lam.webauthn.removeDeviceDialog(element, action, successCallback, isSelfService); return false; } @@ -2162,8 +2162,9 @@ window.lam.webauthn.removeOwnDevice = function(event, isSelfService) { * @param element delete button * @param action action for request (delete|deleteOwn) * @param successCallback callback if all was fine (optional) + * @param isSelfService run in self service or admin context */ -window.lam.webauthn.removeDeviceDialog = function(element, action, successCallback) { +window.lam.webauthn.removeDeviceDialog = function(element, action, successCallback, isSelfService) { const dialogTitle = element.dataset.dialogtitle; const okText = element.dataset.oktext; const cancelText = element.dataset.canceltext; @@ -2178,7 +2179,7 @@ window.lam.webauthn.removeDeviceDialog = function(element, action, successCallba width: 'auto' }).then(result => { if (result.isConfirmed) { - window.lam.webauthn.sendRemoveDeviceRequest(element, action, successCallback); + window.lam.webauthn.sendRemoveDeviceRequest(element, action, successCallback, isSelfService); } }); } @@ -2189,8 +2190,9 @@ window.lam.webauthn.removeDeviceDialog = function(element, action, successCallba * @param element button element * @param action action (delete|deleteOwn) * @param successCallback callback if all was fine (optional) + * @param isSelfService run in self service or admin context */ -window.lam.webauthn.sendRemoveDeviceRequest = function(element, action, successCallback) { +window.lam.webauthn.sendRemoveDeviceRequest = function(element, action, successCallback, isSelfService) { const dn = element.dataset.dn; const credential = element.dataset.credential; const resultDiv = document.getElementById('webauthn_results'); @@ -2200,6 +2202,11 @@ window.lam.webauthn.sendRemoveDeviceRequest = function(element, action, successC data.append('action', 'delete'); data.append('dn', dn); data.append('credentialId', credential); + if (isSelfService) { + document.querySelectorAll('.webauthn_device_name').forEach(item => { + data.append(item.name, item.value); + }); + } fetch('../misc/ajax.php?function=' + action, { method: 'POST', body: data @@ -2222,9 +2229,8 @@ window.lam.webauthn.sendRemoveDeviceRequest = function(element, action, successC * Updates a device name. * * @param event click event - * @param isSelfService run in self service or admin context */ -window.lam.webauthn.updateOwnDeviceName = function(event, isSelfService) { +window.lam.webauthn.updateOwnDeviceName = function(event) { event.preventDefault(); const element = event.currentTarget; const dn = element.dataset.dn; @@ -2241,20 +2247,12 @@ window.lam.webauthn.updateOwnDeviceName = function(event, isSelfService) { data.append('name', name); data.append('credentialId', credential); let action = 'webauthnOwnDevices'; - if (isSelfService) { - action = action + '&selfservice=true&module=webauthn&scope=user'; - } fetch('../misc/ajax.php?function=' + action, { method: 'POST', body: data }) .then(async response => { - if (isSelfService) { - nameElement.classList.add('markPass'); - } - else { - window.location.href = 'webauthn.php?updated=' + encodeURIComponent(credential); - } + window.location.href = 'webauthn.php?updated=' + encodeURIComponent(credential); }) .catch(function(err) { console.log('WebAuthn device name change failed: ' + err.message); @@ -2288,6 +2286,9 @@ window.lam.webauthn.registerOwnDevice = function(event, isSelfService) { data.append('action', 'register'); data.append('dn', dn); data.append('credential', btoa(JSON.stringify(publicKeyCredential))); + document.querySelectorAll('.webauthn_device_name').forEach(item => { + data.append(item.name, item.value); + }); fetch('../misc/ajax.php?selfservice=true&module=webauthn&scope=user', { method: 'POST', body: data diff --git a/lam/templates/tools/webauthn.php b/lam/templates/tools/webauthn.php index 12cb73cd9..bcf958e6c 100644 --- a/lam/templates/tools/webauthn.php +++ b/lam/templates/tools/webauthn.php @@ -119,7 +119,7 @@ $saveButton->addDataAttribute('credential', $credentialId); $saveButton->addDataAttribute('dn', $result['dn']); $saveButton->addDataAttribute('nameelement', 'deviceName_' . $id); - $saveButton->setOnClick('window.lam.webauthn.updateOwnDeviceName(event, false);'); + $saveButton->setOnClick('window.lam.webauthn.updateOwnDeviceName(event);'); $nameField = new htmlInputField('deviceName_' . $id, $result['name']); $nameFieldClasses = ['maxwidth20']; if (!empty($_GET['updated']) && ($_GET['updated'] === $credentialId)) {