Skip to content

Commit d27578f

Browse files
summersabCarlSchwan
authored andcommitted
feat(user-backend): Implement IProvideUserSecretBackend compatibility
Signed-off-by: summersab <18727110+summersab@users.noreply.github.com> Signed-off-by: Andrew Summers <18727110+summersab@users.noreply.github.com> Signed-off-by: Carl Schwan <carlschwan@kde.org>
1 parent 6faea6a commit d27578f

3 files changed

Lines changed: 113 additions & 2 deletions

File tree

lib/SAMLSettings.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class SAMLSettings {
6161
'saml-attribute-mapping-mfa_mapping',
6262
'saml-attribute-mapping-user_id_ldap_mapping',
6363
'saml-attribute-mapping-group_mapping_prefix',
64+
'saml-attribute-mapping-user_secret_mapping',
6465
'saml-user-filter-reject_groups',
6566
'saml-user-filter-require_groups',
6667
'sp-entityId',

lib/Settings/Admin.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ public function getForm(): TemplateResponse {
160160
'type' => 'line',
161161
'required' => false,
162162
],
163+
'user_secret_mapping' => [
164+
'text' => $this->l10n->t('Attribute to use as user secret e.g. for the encryption app.'),
165+
'type' => 'line',
166+
'required' => false,
167+
],
163168
];
164169

165170
if (version_compare($this->config->getSystemValueString('version', '0.0.0'), '34.0.0', '<')) {

lib/UserBackend.php

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use OCA\User_SAML\Model\SessionData;
1414
use OCP\AppFramework\Services\IAppConfig;
1515
use OCP\Authentication\IApacheBackend;
16+
use OCP\Authentication\IProvideUserSecretBackend;
1617
use OCP\EventDispatcher\IEventDispatcher;
1718
use OCP\Files\IRootFolder;
1819
use OCP\Files\NotPermittedException;
@@ -24,21 +25,24 @@
2425
use OCP\IUserBackend;
2526
use OCP\IUserManager;
2627
use OCP\IUserSession;
28+
use OCP\Security\IHasher;
2729
use OCP\Server;
2830
use OCP\User\Backend\ABackend;
31+
use OCP\User\Backend\ICheckPasswordBackend;
2932
use OCP\User\Backend\ICountUsersBackend;
3033
use OCP\User\Backend\ICustomLogout;
3134
use OCP\User\Backend\IGetDisplayNameBackend;
3235
use OCP\User\Backend\IGetHomeBackend;
3336
use OCP\User\Backend\IProvideEnabledStateBackend;
3437
use OCP\User\Backend\ISetDisplayNameBackend;
38+
use OCP\User\Events\PostLoginEvent;
3539
use OCP\User\Events\UserChangedEvent;
3640
use OCP\User\Events\UserFirstTimeLoggedInEvent;
3741
use OCP\UserInterface;
3842
use Override;
3943
use Psr\Log\LoggerInterface;
4044

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 {
4246
/** @var \OCP\UserInterface[] */
4347
private static array $backends = [];
4448

@@ -118,10 +122,70 @@ public function createUserIfNotExists(string $uid, array $attributes = []): void
118122
}
119123
$qb->executeStatement();
120124

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+
}
121136
$this->initializeHomeDir($uid);
122137
}
123138
}
124139

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+
125189
/**
126190
* @throws \OCP\Files\NotFoundException
127191
*/
@@ -165,6 +229,22 @@ public function getHome(string $uid): string|false {
165229
return $users[0]['home'] ?? false;
166230
}
167231

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+
168248
#[Override]
169249
public function getUsers($search = '', $limit = null, $offset = null): array {
170250
// shamelessly duplicated from \OC\User\Database
@@ -388,6 +468,16 @@ public function getBackendName(): string {
388468
return 'user_saml';
389469
}
390470

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+
391481
/**
392482
* Whether autoprovisioning is enabled or not
393483
*/
@@ -573,7 +663,22 @@ public function updateAttributes(string $uid): void {
573663
}
574664
}
575665

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]
577682
public function countUsers(): int {
578683
$query = $this->db->getQueryBuilder();
579684
$query->select($query->func()->count('uid'))

0 commit comments

Comments
 (0)