|
23 | 23 |
|
24 | 24 | use OCP\Authentication\IApacheBackend;
|
25 | 25 | use OCP\EventDispatcher\GenericEvent;
|
| 26 | +use OCP\Authentication\IProvideUserSecretBackend; |
26 | 27 | use OCP\EventDispatcher\IEventDispatcher;
|
27 | 28 | use OCP\Files\NotPermittedException;
|
28 | 29 | use OCP\IConfig;
|
|
40 | 41 | use OCP\User\Events\UserChangedEvent;
|
41 | 42 | use OCP\UserInterface;
|
42 | 43 |
|
43 |
| -class UserBackend implements IApacheBackend, UserInterface, IUserBackend, IGetDisplayNameBackend { |
| 44 | +class UserBackend implements IApacheBackend, UserInterface, IUserBackend, IGetDisplayNameBackend, IProvideUserSecretBackend { |
44 | 45 | /** @var IConfig */
|
45 | 46 | private $config;
|
46 | 47 | /** @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,22 +249,15 @@ 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 |
| - $qb = $this->db->getQueryBuilder(); |
202 |
| - $qb->select('token') |
203 |
| - ->from('user_saml_auth_token') |
204 |
| - ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) |
205 |
| - ->setMaxResults(1000); |
206 |
| - $result = $qb->execute(); |
207 |
| - $data = $result->fetchAll(); |
208 |
| - $result->closeCursor(); |
209 |
| - |
210 |
| - foreach ($data as $passwords) { |
211 |
| - if (password_verify($password, $passwords['token'])) { |
212 |
| - return $uid; |
213 |
| - } |
| 259 | + if ($this->checkUserSecretHash($uid, $password)) { |
| 260 | + return $uid; |
214 | 261 | }
|
215 | 262 |
|
216 | 263 | return false;
|
@@ -508,6 +555,16 @@ public function getCurrentUserId() {
|
508 | 555 | return '';
|
509 | 556 | }
|
510 | 557 |
|
| 558 | + /** |
| 559 | + * Optionally returns a stable per-user secret. This secret is for |
| 560 | + * instance used to secure file encryption keys. |
| 561 | + * @return string|null |
| 562 | + * @since 26.0.0 |
| 563 | + */ |
| 564 | + public function getCurrentUserSecret(): string { |
| 565 | + $samlData = $this->session->get('user_saml.samlUserData'); |
| 566 | + return $this->getUserSecret($this->getCurrentUserId(), $samlData); |
| 567 | + } |
511 | 568 |
|
512 | 569 | /**
|
513 | 570 | * Backend name to be shown in user management
|
@@ -608,6 +665,21 @@ private function getAttributeArrayValue($name, array $attributes) {
|
608 | 665 | return $value;
|
609 | 666 | }
|
610 | 667 |
|
| 668 | + private function getUserSecret($uid, array $attributes) { |
| 669 | + try { |
| 670 | + $userSecret = $this->getAttributeValue('saml-attribute-mapping-user_secret_mapping', $attributes); |
| 671 | + if ($userSecret === '') { |
| 672 | + $this->logger->debug('Got no user_secret from idp', ['app' => 'user_saml']); |
| 673 | + } else { |
| 674 | + $this->logger->debug('Got user_secret from idp', ['app' => 'user_saml']); |
| 675 | + return $userSecret; |
| 676 | + } |
| 677 | + } catch (\InvalidArgumentException $e) { |
| 678 | + $this->logger->debug('No user_secret mapping configured', ['app' => 'user_saml']); |
| 679 | + } |
| 680 | + return null; |
| 681 | + } |
| 682 | + |
611 | 683 | public function updateAttributes($uid,
|
612 | 684 | array $attributes) {
|
613 | 685 | $user = $this->userManager->get($uid);
|
@@ -679,11 +751,16 @@ public function updateAttributes($uid,
|
679 | 751 | $groupManager->get($group)->removeUser($user);
|
680 | 752 | }
|
681 | 753 | }
|
| 754 | + |
| 755 | + $userSecret = $this->getUserSecret($uid, $attributes); |
| 756 | + if ($userSecret !== null) { |
| 757 | + if (!$this->checkUserSecretHash($uid, $userSecret)) { |
| 758 | + $this->updateUserSecretHash($uid, $userSecret); |
| 759 | + } |
| 760 | + } |
682 | 761 | }
|
683 | 762 | }
|
684 | 763 |
|
685 |
| - |
686 |
| - |
687 | 764 | public function countUsers() {
|
688 | 765 | $query = $this->db->getQueryBuilder();
|
689 | 766 | $query->select($query->func()->count('uid'))
|
|
0 commit comments