diff --git a/config/config.sample.php b/config/config.sample.php index 0633b94aada8a..85edd4cdf0323 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -303,6 +303,16 @@ */ 'auth.bruteforce.protection.enabled' => true, +/** + * Whether the authtoken v1 provider should be skipped + * + * The v1 provider is deprecated and removed in Nextcloud 24 onwards. It can be + * disabled already when the instance was installed after Nextcloud 14. + * + * Defaults to ``false`` + */ +'auth.authtoken.v1.disabled' => false, + /** * By default WebAuthn is available but it can be explicitly disabled by admins */ diff --git a/lib/private/Authentication/Token/DefaultTokenCleanupJob.php b/lib/private/Authentication/Token/DefaultTokenCleanupJob.php index c3d80beac693a..3f9aa490108ca 100644 --- a/lib/private/Authentication/Token/DefaultTokenCleanupJob.php +++ b/lib/private/Authentication/Token/DefaultTokenCleanupJob.php @@ -24,9 +24,22 @@ use OC; use OC\BackgroundJob\Job; +use OCP\IConfig; class DefaultTokenCleanupJob extends Job { + + /** @var IConfig */ + protected $config; + + public function __construct(IConfig $config) { + $this->config = $config; + } + protected function run($argument) { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + return; + } + /* @var $provider IProvider */ $provider = OC::$server->query(IProvider::class); $provider->invalidateOldTokens(); diff --git a/lib/private/Authentication/Token/DefaultTokenMapper.php b/lib/private/Authentication/Token/DefaultTokenMapper.php index 6ceb777c30fc7..34e3aa30ab658 100644 --- a/lib/private/Authentication/Token/DefaultTokenMapper.php +++ b/lib/private/Authentication/Token/DefaultTokenMapper.php @@ -32,13 +32,19 @@ use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IConfig; use OCP\IDBConnection; /** * @template-extends QBMapper */ class DefaultTokenMapper extends QBMapper { - public function __construct(IDBConnection $db) { + + /** @var IConfig */ + protected $config; + + public function __construct(IDBConnection $db, IConfig $config) { + $this->config = $config; parent::__construct($db, 'authtoken'); } @@ -48,6 +54,10 @@ public function __construct(IDBConnection $db) { * @param string $token */ public function invalidate(string $token) { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + return; + } + /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') @@ -61,6 +71,10 @@ public function invalidate(string $token) { * @param int $remember */ public function invalidateOld(int $olderThan, int $remember = IToken::DO_NOT_REMEMBER) { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + return; + } + /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') @@ -79,6 +93,10 @@ public function invalidateOld(int $olderThan, int $remember = IToken::DO_NOT_REM * @return DefaultToken */ public function getToken(string $token): DefaultToken { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + throw new DoesNotExistException('Authtoken v1 disabled'); + } + /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $result = $qb->select('id', 'uid', 'login_name', 'password', 'name', 'token', 'type', 'remember', 'last_activity', 'last_check', 'scope', 'expires', 'version') @@ -103,6 +121,10 @@ public function getToken(string $token): DefaultToken { * @return DefaultToken */ public function getTokenById(int $id): DefaultToken { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + throw new DoesNotExistException('Authtoken v1 disabled'); + } + /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $result = $qb->select('id', 'uid', 'login_name', 'password', 'name', 'token', 'type', 'remember', 'last_activity', 'last_check', 'scope', 'expires', 'version') @@ -129,6 +151,10 @@ public function getTokenById(int $id): DefaultToken { * @return DefaultToken[] */ public function getTokenByUser(string $uid): array { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + return []; + } + /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $qb->select('id', 'uid', 'login_name', 'password', 'name', 'token', 'type', 'remember', 'last_activity', 'last_check', 'scope', 'expires', 'version') @@ -148,6 +174,10 @@ public function getTokenByUser(string $uid): array { } public function deleteById(string $uid, int $id) { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + return; + } + /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') @@ -163,6 +193,10 @@ public function deleteById(string $uid, int $id) { * @param string $name */ public function deleteByName(string $name) { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + return; + } + $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') ->where($qb->expr()->eq('name', $qb->createNamedParameter($name), IQueryBuilder::PARAM_STR)) diff --git a/lib/private/Authentication/Token/DefaultTokenProvider.php b/lib/private/Authentication/Token/DefaultTokenProvider.php index a60c8b85b0807..6845a8d4fc75b 100644 --- a/lib/private/Authentication/Token/DefaultTokenProvider.php +++ b/lib/private/Authentication/Token/DefaultTokenProvider.php @@ -89,6 +89,10 @@ public function generateToken(string $token, string $name, int $type = IToken::TEMPORARY_TOKEN, int $remember = IToken::DO_NOT_REMEMBER): IToken { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + throw new InvalidTokenException('Authtokens v1 disabled'); + } + $dbToken = new DefaultToken(); $dbToken->setUid($uid); $dbToken->setLoginName($loginName); @@ -115,6 +119,10 @@ public function generateToken(string $token, * @throws InvalidTokenException */ public function updateToken(IToken $token) { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + throw new InvalidTokenException('Authtokens v1 disabled'); + } + if (!($token instanceof DefaultToken)) { throw new InvalidTokenException("Invalid token type"); } @@ -128,6 +136,10 @@ public function updateToken(IToken $token) { * @param IToken $token */ public function updateTokenActivity(IToken $token) { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + throw new InvalidTokenException('Authtokens v1 disabled'); + } + if (!($token instanceof DefaultToken)) { throw new InvalidTokenException("Invalid token type"); } @@ -141,6 +153,10 @@ public function updateTokenActivity(IToken $token) { } public function getTokenByUser(string $uid): array { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + return []; + } + return $this->mapper->getTokenByUser($uid); } @@ -153,6 +169,10 @@ public function getTokenByUser(string $uid): array { * @return IToken */ public function getToken(string $tokenId): IToken { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + throw new InvalidTokenException('Authtokens v1 disabled'); + } + try { $token = $this->mapper->getToken($this->hashToken($tokenId)); } catch (DoesNotExistException $ex) { @@ -175,6 +195,10 @@ public function getToken(string $tokenId): IToken { * @return IToken */ public function getTokenById(int $tokenId): IToken { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + throw new InvalidTokenException('Authtokens v1 disabled'); + } + try { $token = $this->mapper->getTokenById($tokenId); } catch (DoesNotExistException $ex) { @@ -195,6 +219,10 @@ public function getTokenById(int $tokenId): IToken { * @return IToken */ public function renewSessionToken(string $oldSessionId, string $sessionId): IToken { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + throw new InvalidTokenException('Authtokens v1 disabled'); + } + $token = $this->getToken($oldSessionId); $newToken = new DefaultToken(); @@ -223,6 +251,10 @@ public function renewSessionToken(string $oldSessionId, string $sessionId): ITok * @return string */ public function getPassword(IToken $savedToken, string $tokenId): string { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + throw new InvalidTokenException('Authtokens v1 disabled'); + } + $password = $savedToken->getPassword(); if ($password === null || $password === '') { throw new PasswordlessTokenException(); @@ -239,6 +271,10 @@ public function getPassword(IToken $savedToken, string $tokenId): string { * @throws InvalidTokenException */ public function setPassword(IToken $token, string $tokenId, string $password) { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + throw new InvalidTokenException('Authtokens v1 disabled'); + } + if (!($token instanceof DefaultToken)) { throw new InvalidTokenException("Invalid token type"); } @@ -253,10 +289,18 @@ public function setPassword(IToken $token, string $tokenId, string $password) { * @param string $token */ public function invalidateToken(string $token) { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + return; + } + $this->mapper->invalidate($this->hashToken($token)); } public function invalidateTokenById(string $uid, int $id) { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + return; + } + $this->mapper->deleteById($uid, $id); } @@ -264,6 +308,10 @@ public function invalidateTokenById(string $uid, int $id) { * Invalidate (delete) old session tokens */ public function invalidateOldTokens() { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + return; + } + $olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24); $this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']); $this->mapper->invalidateOld($olderThan, IToken::DO_NOT_REMEMBER); @@ -281,6 +329,10 @@ public function invalidateOldTokens() { * @return IToken */ public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + throw new InvalidTokenException('Authtokens v1 disabled'); + } + try { $password = $this->getPassword($token, $oldTokenId); $token->setPassword($this->encryptPassword($password, $newTokenId)); @@ -338,6 +390,10 @@ private function decryptPassword(string $password, string $token): string { } public function markPasswordInvalid(IToken $token, string $tokenId) { + if ($this->config->getSystemValueBool('auth.authtoken.v1.disabled')) { + throw new InvalidTokenException('Authtokens v1 disabled'); + } + if (!($token instanceof DefaultToken)) { throw new InvalidTokenException("Invalid token type"); } diff --git a/tests/lib/Authentication/Token/DefaultTokenCleanupJobTest.php b/tests/lib/Authentication/Token/DefaultTokenCleanupJobTest.php index 0991c8b1fc807..a464dc4b5bbfe 100644 --- a/tests/lib/Authentication/Token/DefaultTokenCleanupJobTest.php +++ b/tests/lib/Authentication/Token/DefaultTokenCleanupJobTest.php @@ -25,6 +25,7 @@ use OC\Authentication\Token\DefaultTokenCleanupJob; use OC\Authentication\Token\IProvider; use OC\Authentication\Token\Manager; +use OCP\IConfig; use Test\TestCase; class DefaultTokenCleanupJobTest extends TestCase { @@ -36,11 +37,13 @@ class DefaultTokenCleanupJobTest extends TestCase { protected function setUp(): void { parent::setUp(); + $this->config = $this->createMock(IConfig::class); + $this->tokenProvider = $this->getMockBuilder(Manager::class) ->disableOriginalConstructor() ->getMock(); $this->overwriteService(IProvider::class, $this->tokenProvider); - $this->job = new DefaultTokenCleanupJob(); + $this->job = new DefaultTokenCleanupJob($this->config); } public function testRun() { diff --git a/tests/lib/Authentication/Token/DefaultTokenMapperTest.php b/tests/lib/Authentication/Token/DefaultTokenMapperTest.php index da779be080752..9b9bfaf48212f 100644 --- a/tests/lib/Authentication/Token/DefaultTokenMapperTest.php +++ b/tests/lib/Authentication/Token/DefaultTokenMapperTest.php @@ -27,8 +27,10 @@ use OC\Authentication\Token\DefaultTokenMapper; use OC\Authentication\Token\IToken; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IConfig; use OCP\IDBConnection; use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; /** @@ -44,6 +46,8 @@ class DefaultTokenMapperTest extends TestCase { /** @var IDBConnection */ private $dbConnection; + /** @var IConfig|MockObject */ + private $config; private $time; protected function setUp(): void { @@ -51,9 +55,10 @@ protected function setUp(): void { $this->dbConnection = OC::$server->getDatabaseConnection(); $this->time = time(); + $this->config = $this->createMock(IConfig::class); $this->resetDatabase(); - $this->mapper = new DefaultTokenMapper($this->dbConnection); + $this->mapper = new DefaultTokenMapper($this->dbConnection, $this->config); } private function resetDatabase() {