From 128a9d313d8abe4534d464e3457aa2c3efd518d3 Mon Sep 17 00:00:00 2001 From: Andrew Summers <18727110+summersab@users.noreply.github.com> Date: Thu, 9 Feb 2023 18:35:31 -0600 Subject: [PATCH] implement `IProvideUserSecretBackend` compatibility Signed-off-by: summersab <18727110+summersab@users.noreply.github.com> perform a little lint cleanup Signed-off-by: summersab <18727110+summersab@users.noreply.github.com> Signed-off-by: Andrew Summers <18727110+summersab@users.noreply.github.com> --- lib/SAMLSettings.php | 1 + lib/Settings/Admin.php | 6 ++- lib/UserBackend.php | 111 ++++++++++++++++++++++++++++++++++------- 3 files changed, 100 insertions(+), 18 deletions(-) diff --git a/lib/SAMLSettings.php b/lib/SAMLSettings.php index b00725eac..13e033b55 100644 --- a/lib/SAMLSettings.php +++ b/lib/SAMLSettings.php @@ -66,6 +66,7 @@ class SAMLSettings { 'saml-attribute-mapping-home_mapping', 'saml-attribute-mapping-quota_mapping', 'saml-attribute-mapping-mfa_mapping', + 'saml-attribute-mapping-user_secret_mapping', 'saml-user-filter-reject_groups', 'saml-user-filter-require_groups', 'sp-x509cert', diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index b3f4b85d2..3999a3d08 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -140,7 +140,11 @@ public function getForm() { 'type' => 'line', 'required' => false, ], - + 'user_secret_mapping' => [ + 'text' => $this->l10n->t('Attribute to use as user secret e.g. for the encryption app.'), + 'type' => 'line', + 'required' => false, + ], ]; $userFilterSettings = [ diff --git a/lib/UserBackend.php b/lib/UserBackend.php index 0adc3869b..94add2c9a 100644 --- a/lib/UserBackend.php +++ b/lib/UserBackend.php @@ -22,6 +22,7 @@ namespace OCA\User_SAML; use OCP\Authentication\IApacheBackend; +use OCP\Authentication\IProvideUserSecretBackend; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\NotPermittedException; @@ -38,7 +39,7 @@ use OCP\UserInterface; use Symfony\Component\EventDispatcher\GenericEvent; -class UserBackend implements IApacheBackend, UserInterface, IUserBackend { +class UserBackend implements IApacheBackend, UserInterface, IUserBackend, IProvideUserSecretBackend { /** @var IConfig */ private $config; /** @var IURLGenerator */ @@ -148,10 +149,63 @@ public function createUserIfNotExists($uid, array $attributes = []) { } $qb->execute(); + // If we use per-user encryption the keys must be initialized first + $userSecret = $this->getUserSecret($uid, $attributes); + if ($userSecret !== null) { + $this->updateUserSecretHash($uid, $userSecret); + // Emit a post login action to initialize the encryption module with the user secret provided by the idp. + \OC_Hook::emit('OC_User', 'post_login', ['run' => true, 'uid' => $uid, 'password' => $userSecret, 'isTokenLogin' => false]); + } $this->initializeHomeDir($uid); } } + private function getUserSecretHash($uid) { + $qb = $this->db->getQueryBuilder(); + $qb->select('token') + ->from('user_saml_auth_token') + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->eq('name', $qb->createNamedParameter('sso_secret_hash'))) + ->setMaxResults(10); + $result = $qb->execute(); + $data = $result->fetchAll(); + $result->closeCursor(); + return $data; + } + + private function checkUserSecretHash($uid, $userSecret) { + $data = $this->getUserSecretHash($uid); + foreach($data as $row) { + $storedHash = $row['token']; + if (\OC::$server->getHasher()->verify($userSecret, $storedHash, $newHash)) { + if (!empty($newHash)) { + $this->updateUserSecretHash($uid, $userSecret, true); + } + return true; + } + } + return false; + } + + private function updateUserSecretHash($uid, $userSecret, $exists = false) { + $qb = $this->db->getQueryBuilder(); + $hash = \OC::$server->getHasher()->hash($userSecret); + if ($exists || count($this->getUserSecretHash($uid)) > 0) { + $qb->update('user_saml_auth_token') + ->set('token', $qb->createNamedParameter($hash)) + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->eq('name', $qb->createNamedParameter('sso_secret_hash'))); + } else { + $qb->insert('user_saml_auth_token') + ->values([ + 'uid' => $qb->createNamedParameter($uid), + 'token' => $qb->createNamedParameter($hash), + 'name' => $qb->createNamedParameter('sso_secret_hash'), + ]); + } + return $qb->execute(); + } + /** * @param string $uid * @throws \OCP\Files\NotFoundException @@ -195,23 +249,16 @@ public function implementsActions($actions) { * @return string * * Check if the password is correct without logging in the user - * returns the user id or false + * returns the user id or false. + * + * By default user_saml tokens are passwordless and this function + * is unused. It is only called if we have tokens with passwords, + * which happens if we have SSO provided user secrets. */ public function checkPassword($uid, $password) { /* @var $qb IQueryBuilder */ - $qb = $this->db->getQueryBuilder(); - $qb->select('token') - ->from('user_saml_auth_token') - ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) - ->setMaxResults(1000); - $result = $qb->execute(); - $data = $result->fetchAll(); - $result->closeCursor(); - - foreach ($data as $passwords) { - if (password_verify($password, $passwords['token'])) { - return $uid; - } + if ($this->checkUserSecretHash($uid, $password)) { + return $uid; } return false; @@ -512,6 +559,16 @@ public function getCurrentUserId() { return ''; } + /** + * Optionally returns a stable per-user secret. This secret is for + * instance used to secure file encryption keys. + * @return string|null + * @since 26.0.0 + */ + public function getCurrentUserSecret(): string { + $samlData = $this->session->get('user_saml.samlUserData'); + return $this->getUserSecret($this->getCurrentUserId(), $samlData); + } /** * Backend name to be shown in user management @@ -612,6 +669,21 @@ private function getAttributeArrayValue($name, array $attributes) { return $value; } + private function getUserSecret($uid, array $attributes) { + try { + $userSecret = $this->getAttributeValue('saml-attribute-mapping-user_secret_mapping', $attributes); + if ($userSecret === '') { + $this->logger->debug('Got no user_secret from idp', ['app' => 'user_saml']); + } else { + $this->logger->debug('Got user_secret from idp', ['app' => 'user_saml']); + return $userSecret; + } + } catch (\InvalidArgumentException $e) { + $this->logger->debug('No user_secret mapping configured', ['app' => 'user_saml']); + } + return null; + } + public function updateAttributes($uid, array $attributes) { $user = $this->userManager->get($uid); @@ -683,11 +755,16 @@ public function updateAttributes($uid, $groupManager->get($group)->removeUser($user); } } + + $userSecret = $this->getUserSecret($uid, $attributes); + if ($userSecret !== null) { + if (!$this->checkUserSecretHash($uid, $userSecret)) { + $this->updateUserSecretHash($uid, $userSecret); + } + } } } - - public function countUsers() { $query = $this->db->getQueryBuilder(); $query->select($query->func()->count('uid'))