|
34 | 34 | use OCP\IConfig;
|
35 | 35 | use OCP\IURLGenerator;
|
36 | 36 | use OCP\ISession;
|
| 37 | +use OCP\Authentication\IProvideUserSecretBackend; |
37 | 38 | use Symfony\Component\EventDispatcher\GenericEvent;
|
38 | 39 | use OCP\EventDispatcher\IEventDispatcher;
|
39 | 40 | use OCP\User\Events\UserChangedEvent;
|
40 | 41 |
|
41 |
| -class UserBackend implements IApacheBackend, UserInterface, IUserBackend { |
| 42 | +class UserBackend implements IApacheBackend, UserInterface, IUserBackend, IProvideUserSecretBackend { |
42 | 43 | /** @var IConfig */
|
43 | 44 | private $config;
|
44 | 45 | /** @var IURLGenerator */
|
@@ -148,10 +149,63 @@ public function createUserIfNotExists($uid, array $attributes = []) {
|
148 | 149 | }
|
149 | 150 | $qb->execute();
|
150 | 151 |
|
| 152 | + // If we use per-user encryption the keys must be initialized first |
| 153 | + $userSecret = $this->getUserSecret($uid, $attributes); |
| 154 | + if ($userSecret !== null) { |
| 155 | + $this->updateUserSecretHash($uid, $userSecret); |
| 156 | + // Emit a post login action to initialize the encryption module with the user secret provided by the idp. |
| 157 | + \OC_Hook::emit('OC_User', 'post_login', ['run' => true, 'uid' => $uid, 'password' => $userSecret, 'isTokenLogin' => false]); |
| 158 | + } |
151 | 159 | $this->initializeHomeDir($uid);
|
152 | 160 | }
|
153 | 161 | }
|
154 | 162 |
|
| 163 | + private function getUserSecretHash($uid) { |
| 164 | + $qb = $this->db->getQueryBuilder(); |
| 165 | + $qb->select('token') |
| 166 | + ->from('user_saml_auth_token') |
| 167 | + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) |
| 168 | + ->andWhere($qb->expr()->eq('name', $qb->createNamedParameter('sso_secret_hash'))) |
| 169 | + ->setMaxResults(10); |
| 170 | + $result = $qb->execute(); |
| 171 | + $data = $result->fetchAll(); |
| 172 | + $result->closeCursor(); |
| 173 | + return $data; |
| 174 | + } |
| 175 | + |
| 176 | + private function checkUserSecretHash($uid, $userSecret) { |
| 177 | + $data = $this->getUserSecretHash($uid); |
| 178 | + foreach($data as $row) { |
| 179 | + $storedHash = $row['token']; |
| 180 | + if (\OC::$server->getHasher()->verify($userSecret, $storedHash, $newHash)) { |
| 181 | + if (!empty($newHash)) { |
| 182 | + $this->updateUserSecretHash($uid, $userSecret, true); |
| 183 | + } |
| 184 | + return true; |
| 185 | + } |
| 186 | + } |
| 187 | + return false; |
| 188 | + } |
| 189 | + |
| 190 | + private function updateUserSecretHash($uid, $userSecret, $exists = false) { |
| 191 | + $qb = $this->db->getQueryBuilder(); |
| 192 | + $hash = \OC::$server->getHasher()->hash($userSecret); |
| 193 | + if ($exists || count($this->getUserSecretHash($uid)) > 0) { |
| 194 | + $qb->update('user_saml_auth_token') |
| 195 | + ->set('token', $qb->createNamedParameter($hash)) |
| 196 | + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) |
| 197 | + ->andWhere($qb->expr()->eq('name', $qb->createNamedParameter('sso_secret_hash'))); |
| 198 | + } else { |
| 199 | + $qb->insert('user_saml_auth_token') |
| 200 | + ->values([ |
| 201 | + 'uid' => $qb->createNamedParameter($uid), |
| 202 | + 'token' => $qb->createNamedParameter($hash), |
| 203 | + 'name' => $qb->createNamedParameter('sso_secret_hash'), |
| 204 | + ]); |
| 205 | + } |
| 206 | + return $qb->execute(); |
| 207 | + } |
| 208 | + |
155 | 209 | /**
|
156 | 210 | * @param string $uid
|
157 | 211 | * @throws \OCP\Files\NotFoundException
|
@@ -195,23 +249,16 @@ public function implementsActions($actions) {
|
195 | 249 | * @return string
|
196 | 250 | *
|
197 | 251 | * Check if the password is correct without logging in the user
|
198 |
| - * returns the user id or false |
| 252 | + * returns the user id or false. |
| 253 | + * |
| 254 | + * By default user_saml tokens are passwordless and this function |
| 255 | + * is unused. It is only called if we have tokens with passwords, |
| 256 | + * which happens if we have SSO provided user secrets. |
199 | 257 | */
|
200 | 258 | public function checkPassword($uid, $password) {
|
201 | 259 | /* @var $qb IQueryBuilder */
|
202 |
| - $qb = $this->db->getQueryBuilder(); |
203 |
| - $qb->select('token') |
204 |
| - ->from('user_saml_auth_token') |
205 |
| - ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) |
206 |
| - ->setMaxResults(1000); |
207 |
| - $result = $qb->execute(); |
208 |
| - $data = $result->fetchAll(); |
209 |
| - $result->closeCursor(); |
210 |
| - |
211 |
| - foreach ($data as $passwords) { |
212 |
| - if (password_verify($password, $passwords['token'])) { |
213 |
| - return $uid; |
214 |
| - } |
| 260 | + if ($this->checkUserSecretHash($uid, $password)) { |
| 261 | + return $uid; |
215 | 262 | }
|
216 | 263 |
|
217 | 264 | return false;
|
@@ -512,6 +559,16 @@ public function getCurrentUserId() {
|
512 | 559 | return '';
|
513 | 560 | }
|
514 | 561 |
|
| 562 | + /** |
| 563 | + * Optionally returns a stable per-user secret. This secret is for |
| 564 | + * instance used to secure file encryption keys. |
| 565 | + * @return string|null |
| 566 | + * @since 26.0.0 |
| 567 | + */ |
| 568 | + public function getCurrentUserSecret(): string { |
| 569 | + $samlData = $this->session->get('user_saml.samlUserData'); |
| 570 | + return $this->getUserSecret($this->getCurrentUserId(), $samlData); |
| 571 | + } |
515 | 572 |
|
516 | 573 | /**
|
517 | 574 | * Backend name to be shown in user management
|
@@ -612,6 +669,21 @@ private function getAttributeArrayValue($name, array $attributes) {
|
612 | 669 | return $value;
|
613 | 670 | }
|
614 | 671 |
|
| 672 | + private function getUserSecret($uid, array $attributes) { |
| 673 | + try { |
| 674 | + $userSecret = $this->getAttributeValue('saml-attribute-mapping-user_secret_mapping', $attributes); |
| 675 | + if ($userSecret === '') { |
| 676 | + $this->logger->debug('Got no user_secret from idp', ['app' => 'user_saml']); |
| 677 | + } else { |
| 678 | + $this->logger->debug('Got user_secret from idp', ['app' => 'user_saml']); |
| 679 | + return $userSecret; |
| 680 | + } |
| 681 | + } catch (\InvalidArgumentException $e) { |
| 682 | + $this->logger->debug('No user_secret mapping configured', ['app' => 'user_saml']); |
| 683 | + } |
| 684 | + return null; |
| 685 | + } |
| 686 | + |
615 | 687 | public function updateAttributes($uid,
|
616 | 688 | array $attributes) {
|
617 | 689 | $user = $this->userManager->get($uid);
|
@@ -683,11 +755,16 @@ public function updateAttributes($uid,
|
683 | 755 | $groupManager->get($group)->removeUser($user);
|
684 | 756 | }
|
685 | 757 | }
|
| 758 | + |
| 759 | + $userSecret = $this->getUserSecret($uid, $attributes); |
| 760 | + if ($userSecret !== null) { |
| 761 | + if (!$this->checkUserSecretHash($uid, $userSecret)) { |
| 762 | + $this->updateUserSecretHash($uid, $userSecret); |
| 763 | + } |
| 764 | + } |
686 | 765 | }
|
687 | 766 | }
|
688 | 767 |
|
689 |
| - |
690 |
| - |
691 | 768 | public function countUsers() {
|
692 | 769 | $query = $this->db->getQueryBuilder();
|
693 | 770 | $query->select($query->func()->count('uid'))
|
|
0 commit comments