|
13 | 13 | use OCA\User_SAML\Model\SessionData; |
14 | 14 | use OCP\AppFramework\Services\IAppConfig; |
15 | 15 | use OCP\Authentication\IApacheBackend; |
| 16 | +use OCP\Authentication\IProvideUserSecretBackend; |
16 | 17 | use OCP\EventDispatcher\IEventDispatcher; |
17 | 18 | use OCP\Files\IRootFolder; |
18 | 19 | use OCP\Files\NotPermittedException; |
|
24 | 25 | use OCP\IUserBackend; |
25 | 26 | use OCP\IUserManager; |
26 | 27 | use OCP\IUserSession; |
| 28 | +use OCP\Security\IHasher; |
27 | 29 | use OCP\Server; |
28 | 30 | use OCP\User\Backend\ABackend; |
| 31 | +use OCP\User\Backend\ICheckPasswordBackend; |
29 | 32 | use OCP\User\Backend\ICountUsersBackend; |
30 | 33 | use OCP\User\Backend\ICustomLogout; |
31 | 34 | use OCP\User\Backend\IGetDisplayNameBackend; |
32 | 35 | use OCP\User\Backend\IGetHomeBackend; |
33 | 36 | use OCP\User\Backend\IProvideEnabledStateBackend; |
34 | 37 | use OCP\User\Backend\ISetDisplayNameBackend; |
| 38 | +use OCP\User\Events\PostLoginEvent; |
35 | 39 | use OCP\User\Events\UserChangedEvent; |
36 | 40 | use OCP\User\Events\UserFirstTimeLoggedInEvent; |
37 | 41 | use OCP\UserInterface; |
38 | 42 | use Override; |
39 | 43 | use Psr\Log\LoggerInterface; |
40 | 44 |
|
41 | | -class UserBackend extends ABackend implements IApacheBackend, IUserBackend, IGetDisplayNameBackend, ICountUsersBackend, IGetHomeBackend, ICustomLogout, ISetDisplayNameBackend, IProvideEnabledStateBackend { |
| 45 | +class UserBackend extends ABackend implements IApacheBackend, IUserBackend, IGetDisplayNameBackend, ICountUsersBackend, IGetHomeBackend, ICustomLogout, ISetDisplayNameBackend, IProvideEnabledStateBackend, IProvideUserSecretBackend, ICheckPasswordBackend { |
42 | 46 | /** @var \OCP\UserInterface[] */ |
43 | 47 | private static array $backends = []; |
44 | 48 |
|
@@ -118,10 +122,70 @@ public function createUserIfNotExists(string $uid, array $attributes = []): void |
118 | 122 | } |
119 | 123 | $qb->executeStatement(); |
120 | 124 |
|
| 125 | + // If we use per-user encryption the keys must be initialized first |
| 126 | + $userSecret = $this->getUserSecret($attributes); |
| 127 | + if ($userSecret !== null) { |
| 128 | + $this->updateUserSecretHash($uid, $userSecret); |
| 129 | + $user = $this->userManager->get($uid); |
| 130 | + if ($user === null) { |
| 131 | + throw new \RuntimeException('New user doesn\'t exists.'); |
| 132 | + } |
| 133 | + // Emit a post login action to initialize the encryption module with the user secret provided by the idp. |
| 134 | + $this->eventDispatcher->dispatchTyped(new PostLoginEvent($user, $uid, $userSecret, false)); |
| 135 | + } |
121 | 136 | $this->initializeHomeDir($uid); |
122 | 137 | } |
123 | 138 | } |
124 | 139 |
|
| 140 | + /** |
| 141 | + * @return list<string> |
| 142 | + */ |
| 143 | + private function getUserSecretHashes(string $uid): array { |
| 144 | + $qb = $this->db->getQueryBuilder(); |
| 145 | + $qb->select('token') |
| 146 | + ->from('user_saml_auth_token') |
| 147 | + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) |
| 148 | + ->andWhere($qb->expr()->eq('name', $qb->createNamedParameter('sso_secret_hash'))) |
| 149 | + ->setMaxResults(10); |
| 150 | + $result = $qb->executeQuery(); |
| 151 | + /** @var list<string> $data */ |
| 152 | + $data = $result->fetchAll(\PDO::FETCH_COLUMN); |
| 153 | + $result->closeCursor(); |
| 154 | + return $data; |
| 155 | + } |
| 156 | + |
| 157 | + private function checkAndUpdateUserSecretHash(string $uid, string $userSecret): bool { |
| 158 | + $data = $this->getUserSecretHashes($uid); |
| 159 | + foreach ($data as $storedHash) { |
| 160 | + if (Server::get(IHasher::class)->verify($userSecret, $storedHash, $newHash)) { |
| 161 | + if ($newHash !== null && $newHash !== '') { |
| 162 | + $this->updateUserSecretHash($uid, $userSecret, true); |
| 163 | + } |
| 164 | + return true; |
| 165 | + } |
| 166 | + } |
| 167 | + return false; |
| 168 | + } |
| 169 | + |
| 170 | + private function updateUserSecretHash(string $uid, string $userSecret, bool $exists = false): bool { |
| 171 | + $qb = $this->db->getQueryBuilder(); |
| 172 | + $hash = Server::get(IHasher::class)->hash($userSecret); |
| 173 | + if ($exists || count($this->getUserSecretHashes($uid)) > 0) { |
| 174 | + $qb->update('user_saml_auth_token') |
| 175 | + ->set('token', $qb->createNamedParameter($hash)) |
| 176 | + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) |
| 177 | + ->andWhere($qb->expr()->eq('name', $qb->createNamedParameter('sso_secret_hash'))); |
| 178 | + } else { |
| 179 | + $qb->insert('user_saml_auth_token') |
| 180 | + ->values([ |
| 181 | + 'uid' => $qb->createNamedParameter($uid), |
| 182 | + 'token' => $qb->createNamedParameter($hash), |
| 183 | + 'name' => $qb->createNamedParameter('sso_secret_hash'), |
| 184 | + ]); |
| 185 | + } |
| 186 | + return $qb->executeStatement() > 0; |
| 187 | + } |
| 188 | + |
125 | 189 | /** |
126 | 190 | * @throws \OCP\Files\NotFoundException |
127 | 191 | */ |
@@ -165,6 +229,22 @@ public function getHome(string $uid): string|false { |
165 | 229 | return $users[0]['home'] ?? false; |
166 | 230 | } |
167 | 231 |
|
| 232 | + /** |
| 233 | + * @inheritDoc |
| 234 | + * |
| 235 | + * By default, user_saml tokens are passwordless and this function |
| 236 | + * is unused. It is only called if we have tokens with passwords, |
| 237 | + * which happens if we have SSO provided user secrets. |
| 238 | + */ |
| 239 | + #[Override] |
| 240 | + public function checkPassword(string $loginName, string $password): false|string { |
| 241 | + if ($this->checkAndUpdateUserSecretHash($loginName, $password)) { |
| 242 | + return $loginName; |
| 243 | + } |
| 244 | + |
| 245 | + return false; |
| 246 | + } |
| 247 | + |
168 | 248 | #[Override] |
169 | 249 | public function getUsers($search = '', $limit = null, $offset = null): array { |
170 | 250 | // shamelessly duplicated from \OC\User\Database |
@@ -388,6 +468,16 @@ public function getBackendName(): string { |
388 | 468 | return 'user_saml'; |
389 | 469 | } |
390 | 470 |
|
| 471 | + #[Override] |
| 472 | + public function getCurrentUserSecret(): string { |
| 473 | + $samlData = $this->session->get('user_saml.samlUserData'); |
| 474 | + $userSecret = $this->getUserSecret($samlData); |
| 475 | + if ($userSecret === '' || $userSecret === null) { |
| 476 | + throw new \RuntimeException('No valid user secret given, please check your attribute mapping.'); |
| 477 | + } |
| 478 | + return $userSecret; |
| 479 | + } |
| 480 | + |
391 | 481 | /** |
392 | 482 | * Whether autoprovisioning is enabled or not |
393 | 483 | */ |
@@ -573,7 +663,22 @@ public function updateAttributes(string $uid): void { |
573 | 663 | } |
574 | 664 | } |
575 | 665 |
|
576 | | - #[\Override] |
| 666 | + private function getUserSecret(array $attributes): ?string { |
| 667 | + try { |
| 668 | + $userSecret = $this->getAttributeValue('saml-attribute-mapping-user_secret_mapping', $attributes); |
| 669 | + if ($userSecret === '') { |
| 670 | + $this->logger->debug('Got no user_secret from idp', ['app' => 'user_saml']); |
| 671 | + } else { |
| 672 | + $this->logger->debug('Got user_secret from idp', ['app' => 'user_saml']); |
| 673 | + return $userSecret; |
| 674 | + } |
| 675 | + } catch (\InvalidArgumentException $e) { |
| 676 | + $this->logger->debug('No user_secret mapping configured', ['app' => 'user_saml']); |
| 677 | + } |
| 678 | + return null; |
| 679 | + } |
| 680 | + |
| 681 | + #[Override] |
577 | 682 | public function countUsers(): int { |
578 | 683 | $query = $this->db->getQueryBuilder(); |
579 | 684 | $query->select($query->func()->count('uid')) |
|
0 commit comments