Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/Controller/SAMLController.php
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ public function singleLogoutService(): Http\RedirectResponse {
$pass = false;
}
} else {
// standard request : need read CRSF check
// standard request : need read CSRF check
$pass = $this->request->passesCSRFCheck();
}

Expand Down
1 change: 1 addition & 0 deletions lib/SAMLSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class SAMLSettings {
'saml-attribute-mapping-mfa_mapping',
'saml-attribute-mapping-user_id_ldap_mapping',
'saml-attribute-mapping-group_mapping_prefix',
'saml-attribute-mapping-user_secret_mapping',
'saml-user-filter-reject_groups',
'saml-user-filter-require_groups',
'sp-entityId',
Expand Down
5 changes: 5 additions & 0 deletions lib/Settings/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ public function getForm(): TemplateResponse {
'type' => 'line',
'required' => false,
],
'user_secret_mapping' => [
'text' => $this->l10n->t('Attribute to use as user secret e.g. for the encryption app.'),
'type' => 'line',
'required' => false,
],
];

if (version_compare($this->config->getSystemValueString('version', '0.0.0'), '34.0.0', '<')) {
Expand Down
106 changes: 104 additions & 2 deletions lib/UserBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
use OCA\User_SAML\Model\SessionData;
use OCP\AppFramework\Services\IAppConfig;
use OCP\Authentication\IApacheBackend;
use OCP\Authentication\IProvideUserSecretBackend;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\IRootFolder;
use OCP\Files\NotPermittedException;
use OCP\HintException;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\ISession;
Expand All @@ -24,21 +26,24 @@
use OCP\IUserBackend;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Security\IHasher;
use OCP\Server;
use OCP\User\Backend\ABackend;
use OCP\User\Backend\ICheckPasswordBackend;
use OCP\User\Backend\ICountUsersBackend;
use OCP\User\Backend\ICustomLogout;
use OCP\User\Backend\IGetDisplayNameBackend;
use OCP\User\Backend\IGetHomeBackend;
use OCP\User\Backend\IProvideEnabledStateBackend;
use OCP\User\Backend\ISetDisplayNameBackend;
use OCP\User\Events\PostLoginEvent;
use OCP\User\Events\UserChangedEvent;
use OCP\User\Events\UserFirstTimeLoggedInEvent;
use OCP\UserInterface;
use Override;
use Psr\Log\LoggerInterface;

class UserBackend extends ABackend implements IApacheBackend, IUserBackend, IGetDisplayNameBackend, ICountUsersBackend, IGetHomeBackend, ICustomLogout, ISetDisplayNameBackend, IProvideEnabledStateBackend {
class UserBackend extends ABackend implements IApacheBackend, IUserBackend, IGetDisplayNameBackend, ICountUsersBackend, IGetHomeBackend, ICustomLogout, ISetDisplayNameBackend, IProvideEnabledStateBackend, IProvideUserSecretBackend, ICheckPasswordBackend {

Check failure on line 46 in lib/UserBackend.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable34

MethodSignatureMismatch

lib/UserBackend.php:46:7: MethodSignatureMismatch: Method OCA\User_SAML\UserBackend::getCurrentUserSecret with return type 'null|string' is different to return type 'string' of inherited method OCP\Authentication\IProvideUserSecretBackend::getCurrentUserSecret (see https://psalm.dev/042)

Check failure on line 46 in lib/UserBackend.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable32

MethodSignatureMismatch

lib/UserBackend.php:46:7: MethodSignatureMismatch: Method OCA\User_SAML\UserBackend::getCurrentUserSecret with return type 'null|string' is different to return type 'string' of inherited method OCP\Authentication\IProvideUserSecretBackend::getCurrentUserSecret (see https://psalm.dev/042)

Check failure on line 46 in lib/UserBackend.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable33

MethodSignatureMismatch

lib/UserBackend.php:46:7: MethodSignatureMismatch: Method OCA\User_SAML\UserBackend::getCurrentUserSecret with return type 'null|string' is different to return type 'string' of inherited method OCP\Authentication\IProvideUserSecretBackend::getCurrentUserSecret (see https://psalm.dev/042)
/** @var \OCP\UserInterface[] */
private static array $backends = [];

Expand Down Expand Up @@ -118,10 +123,70 @@
}
$qb->executeStatement();

// If we use per-user encryption the keys must be initialized first
$userSecret = $this->getUserSecret($attributes);
if ($userSecret !== null) {
$this->updateUserSecretHash($uid, $userSecret);
$user = $this->userManager->get($uid);
if ($user === null) {
throw new \RuntimeException('New user doesn\'t exists.');
}
// Emit a post login action to initialize the encryption module with the user secret provided by the idp.
$this->eventDispatcher->dispatchTyped(new PostLoginEvent($user, $uid, $userSecret, false));
}
$this->initializeHomeDir($uid);
}
}

/**
* @return list<string>
*/
private function getUserSecretHashes(string $uid): array {
$qb = $this->db->getQueryBuilder();
$qb->select('token')
->from('user_saml_auth_token')
->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid)))
->andWhere($qb->expr()->eq('name', $qb->createNamedParameter('sso_secret_hash')))
->setMaxResults(10);
$result = $qb->executeQuery();
/** @var list<string> $data */
$data = $result->fetchAll(\PDO::FETCH_COLUMN);
$result->closeCursor();
return $data;
}

private function checkAndUpdateUserSecretHash(string $uid, string $userSecret): bool {
$data = $this->getUserSecretHashes($uid);
foreach ($data as $storedHash) {
if (Server::get(IHasher::class)->verify($userSecret, $storedHash, $newHash)) {
if ($newHash !== null && $newHash !== '') {
$this->updateUserSecretHash($uid, $userSecret, true);
Comment thread
CarlSchwan marked this conversation as resolved.
}
return true;
}
}
return false;
}

private function updateUserSecretHash(string $uid, string $userSecret, bool $exists = false): bool {
$qb = $this->db->getQueryBuilder();
$hash = Server::get(IHasher::class)->hash($userSecret);
if ($exists || count($this->getUserSecretHashes($uid)) > 0) {
$qb->update('user_saml_auth_token')
->set('token', $qb->createNamedParameter($hash))
->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid)))
->andWhere($qb->expr()->eq('name', $qb->createNamedParameter('sso_secret_hash')));
} else {
$qb->insert('user_saml_auth_token')
->values([
'uid' => $qb->createNamedParameter($uid),
'token' => $qb->createNamedParameter($hash),
'name' => $qb->createNamedParameter('sso_secret_hash'),
]);
}
return $qb->executeStatement() > 0;
}

/**
* @throws \OCP\Files\NotFoundException
*/
Expand Down Expand Up @@ -165,6 +230,22 @@
return $users[0]['home'] ?? false;
}

/**
* @inheritDoc
*
* By default, user_saml tokens are passwordless and this function
* is unused. It is only called if we have tokens with passwords,
* which happens if we have SSO provided user secrets.
*/
#[Override]
public function checkPassword(string $loginName, string $password): false|string {
if ($this->checkAndUpdateUserSecretHash($loginName, $password)) {
return $loginName;
}

return false;
}

#[Override]
public function getUsers($search = '', $limit = null, $offset = null): array {
// shamelessly duplicated from \OC\User\Database
Expand Down Expand Up @@ -388,6 +469,13 @@
return 'user_saml';
}

#[Override]
public function getCurrentUserSecret(): ?string {

Check failure on line 473 in lib/UserBackend.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable34

InvalidNullableReturnType

lib/UserBackend.php:473:42: InvalidNullableReturnType: The declared return type 'string' for OCA\User_SAML\UserBackend::getCurrentUserSecret is not nullable, but 'null|string' contains null (see https://psalm.dev/144)

Check failure on line 473 in lib/UserBackend.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable32

InvalidNullableReturnType

lib/UserBackend.php:473:42: InvalidNullableReturnType: The declared return type 'string' for OCA\User_SAML\UserBackend::getCurrentUserSecret is not nullable, but 'null|string' contains null (see https://psalm.dev/144)

Check failure on line 473 in lib/UserBackend.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable33

InvalidNullableReturnType

lib/UserBackend.php:473:42: InvalidNullableReturnType: The declared return type 'string' for OCA\User_SAML\UserBackend::getCurrentUserSecret is not nullable, but 'null|string' contains null (see https://psalm.dev/144)
$samlData = $this->session->get('user_saml.samlUserData');
$userSecret = $this->getUserSecret($samlData);
return $userSecret;

Check failure on line 476 in lib/UserBackend.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable34

NullableReturnStatement

lib/UserBackend.php:476:10: NullableReturnStatement: The declared return type 'string' for OCA\User_SAML\UserBackend::getCurrentUserSecret is not nullable, but the function returns 'null|string' (see https://psalm.dev/139)

Check failure on line 476 in lib/UserBackend.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable32

NullableReturnStatement

lib/UserBackend.php:476:10: NullableReturnStatement: The declared return type 'string' for OCA\User_SAML\UserBackend::getCurrentUserSecret is not nullable, but the function returns 'null|string' (see https://psalm.dev/139)

Check failure on line 476 in lib/UserBackend.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable33

NullableReturnStatement

lib/UserBackend.php:476:10: NullableReturnStatement: The declared return type 'string' for OCA\User_SAML\UserBackend::getCurrentUserSecret is not nullable, but the function returns 'null|string' (see https://psalm.dev/139)
}

/**
* Whether autoprovisioning is enabled or not
*/
Expand Down Expand Up @@ -573,7 +661,21 @@
}
}

#[\Override]
private function getUserSecret(array $attributes): ?string {
try {
$userSecret = $this->getAttributeValue('saml-attribute-mapping-user_secret_mapping', $attributes);
if ($userSecret === '') {
throw new HintException('Got no user_secret from IDP. Make sure that your IDP provides a per-user secrets.');
} else {
return $userSecret;
}
} catch (\InvalidArgumentException $e) {
// ignore no user_secret mapping was configured
}
return null;
}

#[Override]
public function countUsers(): int {
$query = $this->db->getQueryBuilder();
$query->select($query->func()->count('uid'))
Expand Down
Loading