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
8 changes: 7 additions & 1 deletion core/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use OC\AppFramework\Http\Request;
use OC\Authentication\Login\Chain;
use OC\Authentication\Login\LoginData;
use OC\Authentication\RememberLogin\RememberLoginTokenMapper;
use OC\Authentication\WebAuthn\Manager as WebAuthnManager;
use OC\User\Session;
use OC_App;
Expand Down Expand Up @@ -68,6 +69,7 @@ public function __construct(
private IManager $manager,
private IL10N $l10n,
private IAppManager $appManager,
private RememberLoginTokenMapper $rememberLoginTokenMapper,
) {
parent::__construct($appName, $request);
}
Expand All @@ -82,7 +84,11 @@ public function logout() {
$loginToken = $this->request->getCookie('nc_token');
$uid = $this->userSession->getUser()?->getUID();
if ($loginToken !== null && $uid !== null) {
$this->config->deleteUserValue($uid, 'login_token', $loginToken);
$affectedRows = $this->rememberLoginTokenMapper->deleteByToken($loginToken);
if ($affectedRows < 1) {
// TODO: remove this after migration to 'remember_login_tokens' table is finished
$this->config->deleteUserValue($uid, 'login_token', $loginToken);
}
}
$this->userSession->logout();

Expand Down
61 changes: 61 additions & 0 deletions core/Migrations/Version36000Date20260908184209.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OC\Core\Migrations;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\Attributes\AddIndex;
use OCP\Migration\Attributes\CreateTable;
use OCP\Migration\Attributes\IndexType;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
use Override;

#[CreateTable(
table: 'remember_login_tokens',
columns: ['uid', 'token'],
description: 'New table to store remember login tokens, replacing the login_token entries kept in oc_preferences',
)]
#[AddIndex(table: 'remember_login_tokens', type: IndexType::PRIMARY)]
#[AddIndex(table: 'remember_login_tokens', type: IndexType::UNIQUE, description: 'Allows to search on token')]
#[AddIndex(table: 'remember_login_tokens', type: IndexType::INDEX, description: 'Allows to search on user ID')]
class Version36000Date20260908184209 extends SimpleMigrationStep {

#[Override]
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

if (!$schema->hasTable('remember_login_tokens')) {
$table = $schema->createTable('remember_login_tokens');
$table->addColumn('id', Types::BIGINT, [
'notnull' => true,
'length' => 20,
'unsigned' => true,
]);
$table->addColumn('uid', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('token', Types::STRING, [
'notnull' => true,
'length' => 200,
]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['token'], 'remember_login_tokens_token');
$table->addIndex(['uid'], 'remember_login_tokens_uid');

return $schema;
}

return null;
}
}
3 changes: 3 additions & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -1324,6 +1324,8 @@
'OC\\Authentication\\Login\\UserDisabledCheckCommand' => $baseDir . '/lib/private/Authentication/Login/UserDisabledCheckCommand.php',
'OC\\Authentication\\Login\\WebAuthnChain' => $baseDir . '/lib/private/Authentication/Login/WebAuthnChain.php',
'OC\\Authentication\\Notifications\\Notifier' => $baseDir . '/lib/private/Authentication/Notifications/Notifier.php',
'OC\\Authentication\\RememberLogin\\RememberLoginToken' => $baseDir . '/lib/private/Authentication/RememberLogin/RememberLoginToken.php',
'OC\\Authentication\\RememberLogin\\RememberLoginTokenMapper' => $baseDir . '/lib/private/Authentication/RememberLogin/RememberLoginTokenMapper.php',
'OC\\Authentication\\Token\\INamedToken' => $baseDir . '/lib/private/Authentication/Token/INamedToken.php',
'OC\\Authentication\\Token\\IProvider' => $baseDir . '/lib/private/Authentication/Token/IProvider.php',
'OC\\Authentication\\Token\\IToken' => $baseDir . '/lib/private/Authentication/Token/IToken.php',
Expand Down Expand Up @@ -1733,6 +1735,7 @@
'OC\\Core\\Migrations\\Version34000Date20260518163022' => $baseDir . '/core/Migrations/Version34000Date20260518163022.php',
'OC\\Core\\Migrations\\Version34000Date20260521110333' => $baseDir . '/core/Migrations/Version34000Date20260521110333.php',
'OC\\Core\\Migrations\\Version35000Date20260527162338' => $baseDir . '/core/Migrations/Version35000Date20260527162338.php',
'OC\\Core\\Migrations\\Version36000Date20260908184209' => $baseDir . '/core/Migrations/Version36000Date20260908184209.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
'OC\\Core\\Service\\CronService' => $baseDir . '/core/Service/CronService.php',
Expand Down
3 changes: 3 additions & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Authentication\\Login\\UserDisabledCheckCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/UserDisabledCheckCommand.php',
'OC\\Authentication\\Login\\WebAuthnChain' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/WebAuthnChain.php',
'OC\\Authentication\\Notifications\\Notifier' => __DIR__ . '/../../..' . '/lib/private/Authentication/Notifications/Notifier.php',
'OC\\Authentication\\RememberLogin\\RememberLoginToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/RememberLogin/RememberLoginToken.php',
'OC\\Authentication\\RememberLogin\\RememberLoginTokenMapper' => __DIR__ . '/../../..' . '/lib/private/Authentication/RememberLogin/RememberLoginTokenMapper.php',
'OC\\Authentication\\Token\\INamedToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/INamedToken.php',
'OC\\Authentication\\Token\\IProvider' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/IProvider.php',
'OC\\Authentication\\Token\\IToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/IToken.php',
Expand Down Expand Up @@ -1774,6 +1776,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Migrations\\Version34000Date20260518163022' => __DIR__ . '/../../..' . '/core/Migrations/Version34000Date20260518163022.php',
'OC\\Core\\Migrations\\Version34000Date20260521110333' => __DIR__ . '/../../..' . '/core/Migrations/Version34000Date20260521110333.php',
'OC\\Core\\Migrations\\Version35000Date20260527162338' => __DIR__ . '/../../..' . '/core/Migrations/Version35000Date20260527162338.php',
'OC\\Core\\Migrations\\Version36000Date20260908184209' => __DIR__ . '/../../..' . '/core/Migrations/Version36000Date20260908184209.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
'OC\\Core\\Service\\CronService' => __DIR__ . '/../../..' . '/core/Service/CronService.php',
Expand Down
29 changes: 29 additions & 0 deletions lib/private/Authentication/RememberLogin/RememberLoginToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OC\Authentication\RememberLogin;

use OCP\AppFramework\ORM\Attribute\Column;
use OCP\AppFramework\ORM\Attribute\Entity;
use OCP\AppFramework\ORM\Attribute\Id;
use OCP\DB\Schema\ColumnType;
use OCP\Snowflake\ISnowflakeGenerator;

#[Entity(name: 'remember_login_tokens')]
final class RememberLoginToken {
#[Id(generatorClass: ISnowflakeGenerator::class)]
#[Column(name: 'id', type: ColumnType::Bigint)]
public ?string $id = null;

#[Column(name: 'uid', type: ColumnType::String, length: 64)]
public string $uid;

#[Column(name: 'token', type: ColumnType::String, length: 200)]
public string $token;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OC\Authentication\RememberLogin;

use OC\AppFramework\ORM\EntityManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\ORM\Repository;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Snowflake\ISnowflakeGenerator;
use Override;

/**
* @template-extends Repository<RememberLoginToken>
*/
class RememberLoginTokenMapper extends Repository {
public const string entityClass = RememberLoginToken::class;

public function __construct(
IDBConnection $connection,
EntityManager $entityManager,
private readonly ISnowflakeGenerator $snowflakeGenerator,
private readonly IConfig $config,
) {
/** @psalm-suppress InternalMethod */
parent::__construct($connection, $entityManager);
}

#[Override]
public function insert(object $entity): object {
/** @var RememberLoginToken $entity */
$entity->token = $this->hashToken($entity->token);

return parent::insert($entity);
}

/**
* @throws DoesNotExistException
*/
public function findByToken(string $token): RememberLoginToken {
return $this->findOneBy(['token' => $this->hashToken($token)]);
}

public function deleteByToken(string $token): int {
return $this->deleteBy(['token' => $this->hashToken($token)]);
}

/**
* Removes every remembered login token for given user
*/
public function deleteByUid(string $uid): int {
return $this->deleteBy(['uid' => $uid]);
}

/**
* Updates old token with the new one and generates a new snowflake ID,
* refreshing the creation timestamp encoded in it
*
* @return int Number of updated rows
*/
public function rotateToken(string $oldToken, string $newToken): int {
$qb = $this->connection->getQueryBuilder();
$qb->update($this->getTableName())
->set('id', $qb->createNamedParameter($this->snowflakeGenerator->nextId()))
->set('token', $qb->createNamedParameter($this->hashToken($newToken)))
->where($qb->expr()->eq('token', $qb->createNamedParameter($this->hashToken($oldToken))));

return $qb->executeStatement();
}

/**
* Removes every remembered login token older than the given timestamp
*/
public function deleteOlderThan(int $timestamp): int {
$qb = $this->connection->getQueryBuilder();
$qb->delete($this->getTableName())
->where($qb->expr()->lt('id', $qb->createNamedParameter($this->snowflakeGenerator->minForTimeId($timestamp))));

return $qb->executeStatement();
}

private function hashToken(string $token): string {
return hash('sha512', $token . $this->config->getSystemValueString('secret'));
}
}
4 changes: 4 additions & 0 deletions lib/private/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use OC\Authentication\Listeners\LoginFailedListener;
use OC\Authentication\Listeners\UserLoggedInListener;
use OC\Authentication\LoginCredentials\Store;
use OC\Authentication\RememberLogin\RememberLoginTokenMapper;
use OC\Authentication\Token\IProvider;
use OC\Authentication\TwoFactorAuth\Registry;
use OC\Avatar\AvatarManager;
Expand Down Expand Up @@ -435,8 +436,10 @@ public function __construct(
// might however be called when Nextcloud is not yet setup.
if (\OCP\Server::get(SystemConfig::class)->getValue('installed', false)) {
$provider = $c->get(IProvider::class);
$rememberLoginTokenMapper = $c->get(RememberLoginTokenMapper::class);
} else {
$provider = null;
$rememberLoginTokenMapper = null;
}

$userSession = new Session(
Expand All @@ -449,6 +452,7 @@ public function __construct(
$c->get(ILockdownManager::class),
$c->get(LoggerInterface::class),
$c->get(IEventDispatcher::class),
$rememberLoginTokenMapper,
);
/** @deprecated 21.0.0 use BeforeUserCreatedEvent event with the IEventDispatcher instead */
$userSession->listen('\OC\User', 'preCreateUser', function ($uid, $password): void {
Expand Down
6 changes: 6 additions & 0 deletions lib/private/User/BackgroundJobs/CleanupLoginTokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace OC\User\BackgroundJobs;

use OC\Authentication\RememberLogin\RememberLoginTokenMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\IConfig;
Expand All @@ -18,6 +19,7 @@ class CleanupLoginTokens extends TimedJob {
public function __construct(
ITimeFactory $time,
private readonly IDBConnection $connection,
private readonly RememberLoginTokenMapper $rememberLoginTokenMapper,
private readonly IConfig $config,
) {
parent::__construct($time);
Expand All @@ -28,6 +30,10 @@ public function __construct(
#[\Override]
protected function run($argument): void {
$rememberMeMaxAge = $this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);

$this->rememberLoginTokenMapper->deleteOlderThan(time() - $rememberMeMaxAge);

// TODO: remove this after migration to 'remember_login_tokens' table is finished
$qb = $this->connection->getQueryBuilder();
$qb
->delete('preferences')
Expand Down
4 changes: 4 additions & 0 deletions lib/private/User/Listeners/BeforeUserDeletedListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace OC\User\Listeners;

use OC\Authentication\RememberLogin\RememberLoginTokenMapper;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\NotFoundException;
Expand All @@ -25,6 +26,7 @@ public function __construct(
private LoggerInterface $logger,
private IAvatarManager $avatarManager,
private ICredentialsManager $credentialsManager,
private RememberLoginTokenMapper $rememberLoginTokenMapper,
) {
}

Expand All @@ -50,5 +52,7 @@ public function handle(Event $event): void {
}
// Delete storages credentials on user deletion
$this->credentialsManager->erase($user->getUID());
// Delete remember login tokens on user deletion
$this->rememberLoginTokenMapper->deleteByUid($user->getUID());
}
}
Loading
Loading