Skip to content

Commit 371646d

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 371646d

4 files changed

Lines changed: 111 additions & 3 deletions

File tree

lib/Controller/SAMLController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ public function singleLogoutService(): Http\RedirectResponse {
463463
$pass = false;
464464
}
465465
} else {
466-
// standard request : need read CRSF check
466+
// standard request : need read CSRF check
467467
$pass = $this->request->passesCSRFCheck();
468468
}
469469

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: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
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;
20+
use OCP\HintException;
1921
use OCP\IConfig;
2022
use OCP\IDBConnection;
2123
use OCP\ISession;
@@ -24,21 +26,24 @@
2426
use OCP\IUserBackend;
2527
use OCP\IUserManager;
2628
use OCP\IUserSession;
29+
use OCP\Security\IHasher;
2730
use OCP\Server;
2831
use OCP\User\Backend\ABackend;
32+
use OCP\User\Backend\ICheckPasswordBackend;
2933
use OCP\User\Backend\ICountUsersBackend;
3034
use OCP\User\Backend\ICustomLogout;
3135
use OCP\User\Backend\IGetDisplayNameBackend;
3236
use OCP\User\Backend\IGetHomeBackend;
3337
use OCP\User\Backend\IProvideEnabledStateBackend;
3438
use OCP\User\Backend\ISetDisplayNameBackend;
39+
use OCP\User\Events\PostLoginEvent;
3540
use OCP\User\Events\UserChangedEvent;
3641
use OCP\User\Events\UserFirstTimeLoggedInEvent;
3742
use OCP\UserInterface;
3843
use Override;
3944
use Psr\Log\LoggerInterface;
4045

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

@@ -118,10 +123,70 @@ public function createUserIfNotExists(string $uid, array $attributes = []): void
118123
}
119124
$qb->executeStatement();
120125

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+
}
121137
$this->initializeHomeDir($uid);
122138
}
123139
}
124140

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+
125190
/**
126191
* @throws \OCP\Files\NotFoundException
127192
*/
@@ -165,6 +230,22 @@ public function getHome(string $uid): string|false {
165230
return $users[0]['home'] ?? false;
166231
}
167232

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+
168249
#[Override]
169250
public function getUsers($search = '', $limit = null, $offset = null): array {
170251
// shamelessly duplicated from \OC\User\Database
@@ -388,6 +469,13 @@ public function getBackendName(): string {
388469
return 'user_saml';
389470
}
390471

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+
391479
/**
392480
* Whether autoprovisioning is enabled or not
393481
*/
@@ -573,7 +661,21 @@ public function updateAttributes(string $uid): void {
573661
}
574662
}
575663

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

0 commit comments

Comments
 (0)