Skip to content

Commit 34462cd

Browse files
perf: move remember login tokens out of oc_preferences
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
1 parent 6212bb6 commit 34462cd

12 files changed

Lines changed: 370 additions & 72 deletions

File tree

core/Controller/LoginController.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use OC\AppFramework\Http\Request;
1414
use OC\Authentication\Login\Chain;
1515
use OC\Authentication\Login\LoginData;
16+
use OC\Authentication\RememberLogin\RememberLoginTokenMapper;
1617
use OC\Authentication\WebAuthn\Manager as WebAuthnManager;
1718
use OC\User\Session;
1819
use OC_App;
@@ -68,6 +69,7 @@ public function __construct(
6869
private IManager $manager,
6970
private IL10N $l10n,
7071
private IAppManager $appManager,
72+
private RememberLoginTokenMapper $rememberLoginTokenMapper,
7173
) {
7274
parent::__construct($appName, $request);
7375
}
@@ -82,7 +84,11 @@ public function logout() {
8284
$loginToken = $this->request->getCookie('nc_token');
8385
$uid = $this->userSession->getUser()?->getUID();
8486
if ($loginToken !== null && $uid !== null) {
85-
$this->config->deleteUserValue($uid, 'login_token', $loginToken);
87+
$affectedRows = $this->rememberLoginTokenMapper->deleteByToken($loginToken);
88+
if ($affectedRows < 1) {
89+
// TODO: remove this after migration to 'remember_login_tokens' table is finished
90+
$this->config->deleteUserValue($uid, 'login_token', $loginToken);
91+
}
8692
}
8793
$this->userSession->logout();
8894

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OC\Core\Migrations;
11+
12+
use Closure;
13+
use OCP\DB\ISchemaWrapper;
14+
use OCP\DB\Types;
15+
use OCP\Migration\Attributes\AddIndex;
16+
use OCP\Migration\Attributes\CreateTable;
17+
use OCP\Migration\Attributes\IndexType;
18+
use OCP\Migration\IOutput;
19+
use OCP\Migration\SimpleMigrationStep;
20+
use Override;
21+
22+
#[CreateTable(
23+
table: 'remember_login_tokens',
24+
columns: ['uid', 'token', 'created'],
25+
description: 'New table to store remember login tokens, replacing the login_token entries kept in oc_preferences',
26+
)]
27+
#[AddIndex(table: 'remember_login_tokens', type: IndexType::PRIMARY)]
28+
#[AddIndex(table: 'remember_login_tokens', type: IndexType::UNIQUE, description: 'Allows to search on token')]
29+
#[AddIndex(table: 'remember_login_tokens', type: IndexType::INDEX, description: 'Allows to search on user ID')]
30+
class Version36000Date20260908184209 extends SimpleMigrationStep {
31+
32+
#[Override]
33+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
34+
/** @var ISchemaWrapper $schema */
35+
$schema = $schemaClosure();
36+
37+
if (!$schema->hasTable('remember_login_tokens')) {
38+
$table = $schema->createTable('remember_login_tokens');
39+
$table->addColumn('id', Types::BIGINT, [
40+
'autoincrement' => true,
41+
'notnull' => true,
42+
'length' => 20,
43+
'unsigned' => true,
44+
]);
45+
$table->addColumn('uid', Types::STRING, [
46+
'notnull' => true,
47+
'length' => 64,
48+
]);
49+
$table->addColumn('token', Types::STRING, [
50+
'notnull' => true,
51+
'length' => 200,
52+
]);
53+
$table->addColumn('created', Types::BIGINT, [
54+
'notnull' => true,
55+
'length' => 20,
56+
'unsigned' => true,
57+
]);
58+
$table->setPrimaryKey(['id']);
59+
$table->addUniqueIndex(['token'], 'remember_login_tokens_token');
60+
$table->addIndex(['uid'], 'remember_login_tokens_uid');
61+
62+
return $schema;
63+
}
64+
65+
return null;
66+
}
67+
}

lib/composer/composer/autoload_classmap.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,6 +1325,8 @@
13251325
'OC\\Authentication\\Login\\UserDisabledCheckCommand' => $baseDir . '/lib/private/Authentication/Login/UserDisabledCheckCommand.php',
13261326
'OC\\Authentication\\Login\\WebAuthnChain' => $baseDir . '/lib/private/Authentication/Login/WebAuthnChain.php',
13271327
'OC\\Authentication\\Notifications\\Notifier' => $baseDir . '/lib/private/Authentication/Notifications/Notifier.php',
1328+
'OC\\Authentication\\RememberLogin\\RememberLoginToken' => $baseDir . '/lib/private/Authentication/RememberLogin/RememberLoginToken.php',
1329+
'OC\\Authentication\\RememberLogin\\RememberLoginTokenMapper' => $baseDir . '/lib/private/Authentication/RememberLogin/RememberLoginTokenMapper.php',
13281330
'OC\\Authentication\\Token\\INamedToken' => $baseDir . '/lib/private/Authentication/Token/INamedToken.php',
13291331
'OC\\Authentication\\Token\\IProvider' => $baseDir . '/lib/private/Authentication/Token/IProvider.php',
13301332
'OC\\Authentication\\Token\\IToken' => $baseDir . '/lib/private/Authentication/Token/IToken.php',
@@ -1735,6 +1737,7 @@
17351737
'OC\\Core\\Migrations\\Version34000Date20260518163022' => $baseDir . '/core/Migrations/Version34000Date20260518163022.php',
17361738
'OC\\Core\\Migrations\\Version34000Date20260521110333' => $baseDir . '/core/Migrations/Version34000Date20260521110333.php',
17371739
'OC\\Core\\Migrations\\Version35000Date20260527162338' => $baseDir . '/core/Migrations/Version35000Date20260527162338.php',
1740+
'OC\\Core\\Migrations\\Version36000Date20260908184209' => $baseDir . '/core/Migrations/Version36000Date20260908184209.php',
17381741
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
17391742
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
17401743
'OC\\Core\\Service\\CronService' => $baseDir . '/core/Service/CronService.php',

lib/composer/composer/autoload_static.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
13661366
'OC\\Authentication\\Login\\UserDisabledCheckCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/UserDisabledCheckCommand.php',
13671367
'OC\\Authentication\\Login\\WebAuthnChain' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/WebAuthnChain.php',
13681368
'OC\\Authentication\\Notifications\\Notifier' => __DIR__ . '/../../..' . '/lib/private/Authentication/Notifications/Notifier.php',
1369+
'OC\\Authentication\\RememberLogin\\RememberLoginToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/RememberLogin/RememberLoginToken.php',
1370+
'OC\\Authentication\\RememberLogin\\RememberLoginTokenMapper' => __DIR__ . '/../../..' . '/lib/private/Authentication/RememberLogin/RememberLoginTokenMapper.php',
13691371
'OC\\Authentication\\Token\\INamedToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/INamedToken.php',
13701372
'OC\\Authentication\\Token\\IProvider' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/IProvider.php',
13711373
'OC\\Authentication\\Token\\IToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/IToken.php',
@@ -1776,6 +1778,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
17761778
'OC\\Core\\Migrations\\Version34000Date20260518163022' => __DIR__ . '/../../..' . '/core/Migrations/Version34000Date20260518163022.php',
17771779
'OC\\Core\\Migrations\\Version34000Date20260521110333' => __DIR__ . '/../../..' . '/core/Migrations/Version34000Date20260521110333.php',
17781780
'OC\\Core\\Migrations\\Version35000Date20260527162338' => __DIR__ . '/../../..' . '/core/Migrations/Version35000Date20260527162338.php',
1781+
'OC\\Core\\Migrations\\Version36000Date20260908184209' => __DIR__ . '/../../..' . '/core/Migrations/Version36000Date20260908184209.php',
17791782
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
17801783
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
17811784
'OC\\Core\\Service\\CronService' => __DIR__ . '/../../..' . '/core/Service/CronService.php',
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OC\Authentication\RememberLogin;
11+
12+
use OCP\AppFramework\Db\Entity;
13+
use OCP\DB\Types;
14+
15+
/**
16+
* @method void setUid(string $uid)
17+
* @method string getUid()
18+
* @method void setToken(string $token)
19+
* @method string getToken()
20+
* @method void setCreated(int $created)
21+
* @method int getCreated()
22+
*/
23+
class RememberLoginToken extends Entity {
24+
/** @var string */
25+
protected $uid;
26+
27+
/** @var string */
28+
protected $token;
29+
30+
/** @var int */
31+
protected $created;
32+
33+
public function __construct() {
34+
$this->addType('uid', Types::STRING);
35+
$this->addType('token', Types::STRING);
36+
$this->addType('created', Types::INTEGER);
37+
}
38+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OC\Authentication\RememberLogin;
11+
12+
use OCP\AppFramework\Db\DoesNotExistException;
13+
use OCP\AppFramework\Db\Entity;
14+
use OCP\AppFramework\Db\QBMapper;
15+
use OCP\DB\QueryBuilder\IQueryBuilder;
16+
use OCP\IConfig;
17+
use OCP\IDBConnection;
18+
use Override;
19+
20+
/**
21+
* @template-extends QBMapper<RememberLoginToken>
22+
*/
23+
class RememberLoginTokenMapper extends QBMapper {
24+
public function __construct(
25+
IDBConnection $db,
26+
private IConfig $config,
27+
) {
28+
parent::__construct($db, 'remember_login_tokens', RememberLoginToken::class);
29+
}
30+
31+
#[Override]
32+
public function insert(Entity $entity): Entity {
33+
/** @var RememberLoginToken $entity */
34+
$entity->setToken($this->hashToken($entity->getToken()));
35+
36+
return parent::insert($entity);
37+
}
38+
39+
/**
40+
* @throws DoesNotExistException
41+
*/
42+
public function findByToken(string $token): RememberLoginToken {
43+
$query = $this->db->getQueryBuilder();
44+
$query->select('*')
45+
->from($this->getTableName())
46+
->where($query->expr()->eq('token', $query->createNamedParameter($this->hashToken($token))));
47+
48+
return $this->findEntity($query);
49+
}
50+
51+
public function deleteByToken(string $token): int {
52+
$query = $this->db->getQueryBuilder();
53+
$query->delete($this->getTableName())
54+
->where($query->expr()->eq('token', $query->createNamedParameter($this->hashToken($token))));
55+
56+
return $query->executeStatement();
57+
}
58+
59+
/**
60+
* Removes every remembered login token for given user
61+
*/
62+
public function deleteByUid(string $uid): int {
63+
$query = $this->db->getQueryBuilder();
64+
$query->delete($this->getTableName())
65+
->where($query->expr()->eq('uid', $query->createNamedParameter($uid)));
66+
67+
return $query->executeStatement();
68+
}
69+
70+
/**
71+
* Removes every remembered login token older than the given timestamp
72+
*/
73+
public function deleteOlderThan(int $timestamp): int {
74+
$query = $this->db->getQueryBuilder();
75+
$query->delete($this->getTableName())
76+
->where($query->expr()->lt('created', $query->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT)));
77+
78+
return $query->executeStatement();
79+
}
80+
81+
private function hashToken(string $token): string {
82+
return hash('sha512', $token . $this->config->getSystemValueString('secret'));
83+
}
84+
}

lib/private/Server.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use OC\Authentication\Listeners\LoginFailedListener;
2323
use OC\Authentication\Listeners\UserLoggedInListener;
2424
use OC\Authentication\LoginCredentials\Store;
25+
use OC\Authentication\RememberLogin\RememberLoginTokenMapper;
2526
use OC\Authentication\Token\IProvider;
2627
use OC\Authentication\TwoFactorAuth\Registry;
2728
use OC\Avatar\AvatarManager;
@@ -440,8 +441,10 @@ public function __construct(
440441
// might however be called when Nextcloud is not yet setup.
441442
if (\OCP\Server::get(SystemConfig::class)->getValue('installed', false)) {
442443
$provider = $c->get(IProvider::class);
444+
$rememberLoginTokenMapper = $c->get(RememberLoginTokenMapper::class);
443445
} else {
444446
$provider = null;
447+
$rememberLoginTokenMapper = null;
445448
}
446449

447450
$userSession = new Session(
@@ -454,6 +457,7 @@ public function __construct(
454457
$c->get(ILockdownManager::class),
455458
$c->get(LoggerInterface::class),
456459
$c->get(IEventDispatcher::class),
460+
$rememberLoginTokenMapper,
457461
);
458462
/** @deprecated 21.0.0 use BeforeUserCreatedEvent event with the IEventDispatcher instead */
459463
$userSession->listen('\OC\User', 'preCreateUser', function ($uid, $password): void {

lib/private/User/BackgroundJobs/CleanupLoginTokens.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
namespace OC\User\BackgroundJobs;
1111

12+
use OC\Authentication\RememberLogin\RememberLoginTokenMapper;
1213
use OCP\AppFramework\Utility\ITimeFactory;
1314
use OCP\BackgroundJob\TimedJob;
1415
use OCP\IConfig;
@@ -18,6 +19,7 @@ class CleanupLoginTokens extends TimedJob {
1819
public function __construct(
1920
ITimeFactory $time,
2021
private readonly IDBConnection $connection,
22+
private readonly RememberLoginTokenMapper $rememberLoginTokenMapper,
2123
private readonly IConfig $config,
2224
) {
2325
parent::__construct($time);
@@ -28,6 +30,10 @@ public function __construct(
2830
#[\Override]
2931
protected function run($argument): void {
3032
$rememberMeMaxAge = $this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
33+
34+
$this->rememberLoginTokenMapper->deleteOlderThan(time() - $rememberMeMaxAge);
35+
36+
// TODO: remove this after migration to 'remember_login_tokens' table is finished
3137
$qb = $this->connection->getQueryBuilder();
3238
$qb
3339
->delete('preferences')

lib/private/User/Listeners/BeforeUserDeletedListener.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
namespace OC\User\Listeners;
1111

12+
use OC\Authentication\RememberLogin\RememberLoginTokenMapper;
1213
use OCP\EventDispatcher\Event;
1314
use OCP\EventDispatcher\IEventListener;
1415
use OCP\Files\NotFoundException;
@@ -25,6 +26,7 @@ public function __construct(
2526
private LoggerInterface $logger,
2627
private IAvatarManager $avatarManager,
2728
private ICredentialsManager $credentialsManager,
29+
private RememberLoginTokenMapper $rememberLoginTokenMapper,
2830
) {
2931
}
3032

@@ -50,5 +52,7 @@ public function handle(Event $event): void {
5052
}
5153
// Delete storages credentials on user deletion
5254
$this->credentialsManager->erase($user->getUID());
55+
// Delete remember login tokens on user deletion
56+
$this->rememberLoginTokenMapper->deleteByUid($user->getUID());
5357
}
5458
}

0 commit comments

Comments
 (0)