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