diff options
Diffstat (limited to 'lib/private/Authentication/Token')
-rw-r--r-- | lib/private/Authentication/Token/INamedToken.php | 22 | ||||
-rw-r--r-- | lib/private/Authentication/Token/IProvider.php | 98 | ||||
-rw-r--r-- | lib/private/Authentication/Token/IToken.php | 133 | ||||
-rw-r--r-- | lib/private/Authentication/Token/IWipeableToken.php | 23 | ||||
-rw-r--r-- | lib/private/Authentication/Token/Manager.php | 114 | ||||
-rw-r--r-- | lib/private/Authentication/Token/PublicKeyToken.php | 63 | ||||
-rw-r--r-- | lib/private/Authentication/Token/PublicKeyTokenMapper.php | 100 | ||||
-rw-r--r-- | lib/private/Authentication/Token/PublicKeyTokenProvider.php | 456 | ||||
-rw-r--r-- | lib/private/Authentication/Token/RemoteWipe.php | 40 | ||||
-rw-r--r-- | lib/private/Authentication/Token/TokenCleanupJob.php | 26 |
10 files changed, 531 insertions, 544 deletions
diff --git a/lib/private/Authentication/Token/INamedToken.php b/lib/private/Authentication/Token/INamedToken.php index 96a7719fb41..9a90cfc7d76 100644 --- a/lib/private/Authentication/Token/INamedToken.php +++ b/lib/private/Authentication/Token/INamedToken.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Daniel Kesselberg (mail@danielkesselberg.de) - * - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Authentication\Token; diff --git a/lib/private/Authentication/Token/IProvider.php b/lib/private/Authentication/Token/IProvider.php index 0a145bfd7e6..d47427e79bf 100644 --- a/lib/private/Authentication/Token/IProvider.php +++ b/lib/private/Authentication/Token/IProvider.php @@ -3,40 +3,19 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Marcel Waldvogel <marcel.waldvogel@uni-konstanz.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Authentication\Token; -use OC\Authentication\Exceptions\ExpiredTokenException; -use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Exceptions\PasswordlessTokenException; -use OC\Authentication\Exceptions\WipeTokenException; +use OCP\Authentication\Exceptions\ExpiredTokenException; +use OCP\Authentication\Exceptions\InvalidTokenException; +use OCP\Authentication\Exceptions\WipeTokenException; +use OCP\Authentication\Token\IToken as OCPIToken; interface IProvider { - - /** * Create and persist a new token * @@ -47,16 +26,18 @@ interface IProvider { * @param string $name Name will be trimmed to 120 chars when longer * @param int $type token type * @param int $remember whether the session token should be used for remember-me - * @return IToken + * @return OCPIToken * @throws \RuntimeException when OpenSSL reports a problem */ public function generateToken(string $token, - string $uid, - string $loginName, - ?string $password, - string $name, - int $type = IToken::TEMPORARY_TOKEN, - int $remember = IToken::DO_NOT_REMEMBER): IToken; + string $uid, + string $loginName, + ?string $password, + string $name, + int $type = OCPIToken::TEMPORARY_TOKEN, + int $remember = OCPIToken::DO_NOT_REMEMBER, + ?array $scope = null, + ): OCPIToken; /** * Get a token by token id @@ -65,9 +46,9 @@ interface IProvider { * @throws InvalidTokenException * @throws ExpiredTokenException * @throws WipeTokenException - * @return IToken + * @return OCPIToken */ - public function getToken(string $tokenId): IToken; + public function getToken(string $tokenId): OCPIToken; /** * Get a token by token id @@ -76,9 +57,9 @@ interface IProvider { * @throws InvalidTokenException * @throws ExpiredTokenException * @throws WipeTokenException - * @return IToken + * @return OCPIToken */ - public function getTokenById(int $tokenId): IToken; + public function getTokenById(int $tokenId): OCPIToken; /** * Duplicate an existing session token @@ -87,9 +68,9 @@ interface IProvider { * @param string $sessionId * @throws InvalidTokenException * @throws \RuntimeException when OpenSSL reports a problem - * @return IToken The new token + * @return OCPIToken The new token */ - public function renewSessionToken(string $oldSessionId, string $sessionId): IToken; + public function renewSessionToken(string $oldSessionId, string $sessionId): OCPIToken; /** * Invalidate (delete) the given session token @@ -112,18 +93,23 @@ interface IProvider { public function invalidateOldTokens(); /** + * Invalidate (delete) tokens last used before a given date + */ + public function invalidateLastUsedBefore(string $uid, int $before): void; + + /** * Save the updated token * - * @param IToken $token + * @param OCPIToken $token */ - public function updateToken(IToken $token); + public function updateToken(OCPIToken $token); /** * Update token activity timestamp * - * @param IToken $token + * @param OCPIToken $token */ - public function updateTokenActivity(IToken $token); + public function updateTokenActivity(OCPIToken $token); /** * Get all tokens of a user @@ -132,49 +118,49 @@ interface IProvider { * where a high number of (session) tokens is generated * * @param string $uid - * @return IToken[] + * @return OCPIToken[] */ public function getTokenByUser(string $uid): array; /** * Get the (unencrypted) password of the given token * - * @param IToken $savedToken + * @param OCPIToken $savedToken * @param string $tokenId * @throws InvalidTokenException * @throws PasswordlessTokenException * @return string */ - public function getPassword(IToken $savedToken, string $tokenId): string; + public function getPassword(OCPIToken $savedToken, string $tokenId): string; /** * Encrypt and set the password of the given token * - * @param IToken $token + * @param OCPIToken $token * @param string $tokenId * @param string $password * @throws InvalidTokenException */ - public function setPassword(IToken $token, string $tokenId, string $password); + public function setPassword(OCPIToken $token, string $tokenId, string $password); /** - * Rotate the token. Usefull for for example oauth tokens + * Rotate the token. Useful for for example oauth tokens * - * @param IToken $token + * @param OCPIToken $token * @param string $oldTokenId * @param string $newTokenId - * @return IToken + * @return OCPIToken * @throws \RuntimeException when OpenSSL reports a problem */ - public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken; + public function rotate(OCPIToken $token, string $oldTokenId, string $newTokenId): OCPIToken; /** * Marks a token as having an invalid password. * - * @param IToken $token + * @param OCPIToken $token * @param string $tokenId */ - public function markPasswordInvalid(IToken $token, string $tokenId); + public function markPasswordInvalid(OCPIToken $token, string $tokenId); /** * Update all the passwords of $uid if required diff --git a/lib/private/Authentication/Token/IToken.php b/lib/private/Authentication/Token/IToken.php index 5ca4eaea843..2028a0b328c 100644 --- a/lib/private/Authentication/Token/IToken.php +++ b/lib/private/Authentication/Token/IToken.php @@ -1,134 +1,17 @@ <?php declare(strict_types=1); - /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Authentication\Token; -use JsonSerializable; - -interface IToken extends JsonSerializable { - public const TEMPORARY_TOKEN = 0; - public const PERMANENT_TOKEN = 1; - public const WIPE_TOKEN = 2; - public const DO_NOT_REMEMBER = 0; - public const REMEMBER = 1; - - /** - * Get the token ID - * - * @return int - */ - public function getId(): int; - - /** - * Get the user UID - * - * @return string - */ - public function getUID(): string; - - /** - * Get the login name used when generating the token - * - * @return string - */ - public function getLoginName(): string; - - /** - * Get the (encrypted) login password - * - * @return string|null - */ - public function getPassword(); - - /** - * Get the timestamp of the last password check - * - * @return int - */ - public function getLastCheck(): int; - - /** - * Set the timestamp of the last password check - * - * @param int $time - */ - public function setLastCheck(int $time); - - /** - * Get the authentication scope for this token - * - * @return string - */ - public function getScope(): string; +use OCP\Authentication\Token\IToken as OCPIToken; - /** - * Get the authentication scope for this token - * - * @return array - */ - public function getScopeAsArray(): array; - - /** - * Set the authentication scope for this token - * - * @param array $scope - */ - public function setScope($scope); - - /** - * Get the name of the token - * @return string - */ - public function getName(): string; - - /** - * Get the remember state of the token - * - * @return int - */ - public function getRemember(): int; - - /** - * Set the token - * - * @param string $token - */ - public function setToken(string $token); - - /** - * Set the password - * - * @param string $password - */ - public function setPassword(string $password); - - /** - * Set the expiration time of the token - * - * @param int|null $expires - */ - public function setExpires($expires); +/** + * @deprecated 28.0.0 use {@see \OCP\Authentication\Token\IToken} instead + */ +interface IToken extends OCPIToken { } diff --git a/lib/private/Authentication/Token/IWipeableToken.php b/lib/private/Authentication/Token/IWipeableToken.php index 64dbb604ed2..fc1476785cd 100644 --- a/lib/private/Authentication/Token/IWipeableToken.php +++ b/lib/private/Authentication/Token/IWipeableToken.php @@ -3,31 +3,12 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Authentication\Token; interface IWipeableToken extends IToken { - /** * Mark the token for remote wipe */ diff --git a/lib/private/Authentication/Token/Manager.php b/lib/private/Authentication/Token/Manager.php index f8a0fb11c52..6953f47b004 100644 --- a/lib/private/Authentication/Token/Manager.php +++ b/lib/private/Authentication/Token/Manager.php @@ -1,40 +1,22 @@ <?php declare(strict_types=1); - /** - * @copyright Copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Authentication\Token; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; -use OC\Authentication\Exceptions\ExpiredTokenException; -use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Exceptions\InvalidTokenException as OcInvalidTokenException; use OC\Authentication\Exceptions\PasswordlessTokenException; -use OC\Authentication\Exceptions\WipeTokenException; - -class Manager implements IProvider { +use OCP\Authentication\Exceptions\ExpiredTokenException; +use OCP\Authentication\Exceptions\InvalidTokenException; +use OCP\Authentication\Exceptions\WipeTokenException; +use OCP\Authentication\Token\IProvider as OCPIProvider; +use OCP\Authentication\Token\IToken as OCPIToken; +class Manager implements IProvider, OCPIProvider { /** @var PublicKeyTokenProvider */ private $publicKeyTokenProvider; @@ -52,15 +34,17 @@ class Manager implements IProvider { * @param string $name Name will be trimmed to 120 chars when longer * @param int $type token type * @param int $remember whether the session token should be used for remember-me - * @return IToken + * @return OCPIToken */ public function generateToken(string $token, - string $uid, - string $loginName, - $password, - string $name, - int $type = IToken::TEMPORARY_TOKEN, - int $remember = IToken::DO_NOT_REMEMBER): IToken { + string $uid, + string $loginName, + $password, + string $name, + int $type = OCPIToken::TEMPORARY_TOKEN, + int $remember = OCPIToken::DO_NOT_REMEMBER, + ?array $scope = null, + ): OCPIToken { if (mb_strlen($name) > 128) { $name = mb_substr($name, 0, 120) . '…'; } @@ -73,7 +57,8 @@ class Manager implements IProvider { $password, $name, $type, - $remember + $remember, + $scope, ); } catch (UniqueConstraintViolationException $e) { // It's rare, but if two requests of the same session (e.g. env-based SAML) @@ -93,10 +78,10 @@ class Manager implements IProvider { /** * Save the updated token * - * @param IToken $token + * @param OCPIToken $token * @throws InvalidTokenException */ - public function updateToken(IToken $token) { + public function updateToken(OCPIToken $token) { $provider = $this->getProvider($token); $provider->updateToken($token); } @@ -105,16 +90,16 @@ class Manager implements IProvider { * Update token activity timestamp * * @throws InvalidTokenException - * @param IToken $token + * @param OCPIToken $token */ - public function updateTokenActivity(IToken $token) { + public function updateTokenActivity(OCPIToken $token) { $provider = $this->getProvider($token); $provider->updateTokenActivity($token); } /** * @param string $uid - * @return IToken[] + * @return OCPIToken[] */ public function getTokenByUser(string $uid): array { return $this->publicKeyTokenProvider->getTokenByUser($uid); @@ -126,9 +111,9 @@ class Manager implements IProvider { * @param string $tokenId * @throws InvalidTokenException * @throws \RuntimeException when OpenSSL reports a problem - * @return IToken + * @return OCPIToken */ - public function getToken(string $tokenId): IToken { + public function getToken(string $tokenId): OCPIToken { try { return $this->publicKeyTokenProvider->getToken($tokenId); } catch (WipeTokenException $e) { @@ -145,9 +130,9 @@ class Manager implements IProvider { * * @param int $tokenId * @throws InvalidTokenException - * @return IToken + * @return OCPIToken */ - public function getTokenById(int $tokenId): IToken { + public function getTokenById(int $tokenId): OCPIToken { try { return $this->publicKeyTokenProvider->getTokenById($tokenId); } catch (ExpiredTokenException $e) { @@ -163,9 +148,9 @@ class Manager implements IProvider { * @param string $oldSessionId * @param string $sessionId * @throws InvalidTokenException - * @return IToken + * @return OCPIToken */ - public function renewSessionToken(string $oldSessionId, string $sessionId): IToken { + public function renewSessionToken(string $oldSessionId, string $sessionId): OCPIToken { try { return $this->publicKeyTokenProvider->renewSessionToken($oldSessionId, $sessionId); } catch (ExpiredTokenException $e) { @@ -176,18 +161,18 @@ class Manager implements IProvider { } /** - * @param IToken $savedToken + * @param OCPIToken $savedToken * @param string $tokenId session token * @throws InvalidTokenException * @throws PasswordlessTokenException * @return string */ - public function getPassword(IToken $savedToken, string $tokenId): string { + public function getPassword(OCPIToken $savedToken, string $tokenId): string { $provider = $this->getProvider($savedToken); return $provider->getPassword($savedToken, $tokenId); } - public function setPassword(IToken $token, string $tokenId, string $password) { + public function setPassword(OCPIToken $token, string $tokenId, string $password) { $provider = $this->getProvider($token); $provider->setPassword($token, $tokenId, $password); } @@ -204,40 +189,55 @@ class Manager implements IProvider { $this->publicKeyTokenProvider->invalidateOldTokens(); } + public function invalidateLastUsedBefore(string $uid, int $before): void { + $this->publicKeyTokenProvider->invalidateLastUsedBefore($uid, $before); + } + /** - * @param IToken $token + * @param OCPIToken $token * @param string $oldTokenId * @param string $newTokenId - * @return IToken + * @return OCPIToken * @throws InvalidTokenException * @throws \RuntimeException when OpenSSL reports a problem */ - public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken { + public function rotate(OCPIToken $token, string $oldTokenId, string $newTokenId): OCPIToken { if ($token instanceof PublicKeyToken) { return $this->publicKeyTokenProvider->rotate($token, $oldTokenId, $newTokenId); } - throw new InvalidTokenException(); + /** @psalm-suppress DeprecatedClass We have to throw the OC version so both OC and OCP catches catch it */ + throw new OcInvalidTokenException(); } /** - * @param IToken $token + * @param OCPIToken $token * @return IProvider * @throws InvalidTokenException */ - private function getProvider(IToken $token): IProvider { + private function getProvider(OCPIToken $token): IProvider { if ($token instanceof PublicKeyToken) { return $this->publicKeyTokenProvider; } - throw new InvalidTokenException(); + /** @psalm-suppress DeprecatedClass We have to throw the OC version so both OC and OCP catches catch it */ + throw new OcInvalidTokenException(); } - public function markPasswordInvalid(IToken $token, string $tokenId) { + public function markPasswordInvalid(OCPIToken $token, string $tokenId) { $this->getProvider($token)->markPasswordInvalid($token, $tokenId); } public function updatePasswords(string $uid, string $password) { $this->publicKeyTokenProvider->updatePasswords($uid, $password); } + + public function invalidateTokensOfUser(string $uid, ?string $clientName) { + $tokens = $this->getTokenByUser($uid); + foreach ($tokens as $token) { + if ($clientName === null || ($token->getName() === $clientName)) { + $this->invalidateTokenById($uid, $token->getId()); + } + } + } } diff --git a/lib/private/Authentication/Token/PublicKeyToken.php b/lib/private/Authentication/Token/PublicKeyToken.php index d060fe14103..be427ab4839 100644 --- a/lib/private/Authentication/Token/PublicKeyToken.php +++ b/lib/private/Authentication/Token/PublicKeyToken.php @@ -3,31 +3,14 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Authentication\Token; use OCP\AppFramework\Db\Entity; +use OCP\Authentication\Token\IToken; +use OCP\DB\Types; /** * @method void setId(int $id) @@ -45,6 +28,8 @@ use OCP\AppFramework\Db\Entity; * @method void setPublicKey(string $key) * @method void setVersion(int $version) * @method bool getPasswordInvalid() + * @method string getPasswordHash() + * @method setPasswordHash(string $hash) */ class PublicKeyToken extends Entity implements INamedToken, IWipeableToken { public const VERSION = 2; @@ -58,6 +43,9 @@ class PublicKeyToken extends Entity implements INamedToken, IWipeableToken { /** @var string encrypted user password */ protected $password; + /** @var string hashed user password */ + protected $passwordHash; + /** @var string token name (e.g. browser/OS) */ protected $name; @@ -98,18 +86,19 @@ class PublicKeyToken extends Entity implements INamedToken, IWipeableToken { $this->addType('uid', 'string'); $this->addType('loginName', 'string'); $this->addType('password', 'string'); + $this->addType('passwordHash', 'string'); $this->addType('name', 'string'); $this->addType('token', 'string'); - $this->addType('type', 'int'); - $this->addType('remember', 'int'); - $this->addType('lastActivity', 'int'); - $this->addType('lastCheck', 'int'); + $this->addType('type', Types::INTEGER); + $this->addType('remember', Types::INTEGER); + $this->addType('lastActivity', Types::INTEGER); + $this->addType('lastCheck', Types::INTEGER); $this->addType('scope', 'string'); - $this->addType('expires', 'int'); + $this->addType('expires', Types::INTEGER); $this->addType('publicKey', 'string'); $this->addType('privateKey', 'string'); - $this->addType('version', 'int'); - $this->addType('passwordInvalid', 'bool'); + $this->addType('version', Types::INTEGER); + $this->addType('passwordInvalid', Types::BOOLEAN); } public function getId(): int { @@ -131,10 +120,8 @@ class PublicKeyToken extends Entity implements INamedToken, IWipeableToken { /** * Get the (encrypted) login password - * - * @return string|null */ - public function getPassword() { + public function getPassword(): ?string { return parent::getPassword(); } @@ -159,10 +146,8 @@ class PublicKeyToken extends Entity implements INamedToken, IWipeableToken { /** * Get the timestamp of the last password check - * - * @param int $time */ - public function setLastCheck(int $time) { + public function setLastCheck(int $time): void { parent::setLastCheck($time); } @@ -179,13 +164,13 @@ class PublicKeyToken extends Entity implements INamedToken, IWipeableToken { $scope = json_decode($this->getScope(), true); if (!$scope) { return [ - 'filesystem' => true + IToken::SCOPE_FILESYSTEM => true ]; } return $scope; } - public function setScope($scope) { + public function setScope(array|string|null $scope): void { if (is_array($scope)) { parent::setScope(json_encode($scope)); } else { @@ -205,15 +190,15 @@ class PublicKeyToken extends Entity implements INamedToken, IWipeableToken { return parent::getRemember(); } - public function setToken(string $token) { + public function setToken(string $token): void { parent::setToken($token); } - public function setPassword(string $password = null) { + public function setPassword(?string $password = null): void { parent::setPassword($password); } - public function setExpires($expires) { + public function setExpires($expires): void { parent::setExpires($expires); } diff --git a/lib/private/Authentication/Token/PublicKeyTokenMapper.php b/lib/private/Authentication/Token/PublicKeyTokenMapper.php index 7b11ef8adf3..9aabd69e57a 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenMapper.php +++ b/lib/private/Authentication/Token/PublicKeyTokenMapper.php @@ -3,32 +3,14 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Authentication\Token; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\QBMapper; +use OCP\Authentication\Token\IToken; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -42,8 +24,6 @@ class PublicKeyTokenMapper extends QBMapper { /** * Invalidate (delete) a given token - * - * @param string $token */ public function invalidate(string $token) { /* @var $qb IQueryBuilder */ @@ -51,22 +31,34 @@ class PublicKeyTokenMapper extends QBMapper { $qb->delete($this->tableName) ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) - ->execute(); + ->executeStatement(); } /** * @param int $olderThan - * @param int $remember + * @param int $type + * @param int|null $remember */ - public function invalidateOld(int $olderThan, int $remember = IToken::DO_NOT_REMEMBER) { + public function invalidateOld(int $olderThan, int $type = IToken::TEMPORARY_TOKEN, ?int $remember = null) { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->delete($this->tableName) + $delete = $qb->delete($this->tableName) ->where($qb->expr()->lt('last_activity', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) - ->execute(); + ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); + if ($remember !== null) { + $delete->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT))); + } + $delete->executeStatement(); + } + + public function invalidateLastUsedBefore(string $uid, int $before): int { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->tableName) + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->lt('last_activity', $qb->createNamedParameter($before, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); + return $qb->executeStatement(); } /** @@ -81,7 +73,7 @@ class PublicKeyTokenMapper extends QBMapper { ->from($this->tableName) ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) - ->execute(); + ->executeQuery(); $data = $result->fetch(); $result->closeCursor(); @@ -103,7 +95,7 @@ class PublicKeyTokenMapper extends QBMapper { ->from($this->tableName) ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) - ->execute(); + ->executeQuery(); $data = $result->fetch(); $result->closeCursor(); @@ -130,7 +122,7 @@ class PublicKeyTokenMapper extends QBMapper { ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) ->setMaxResults(1000); - $result = $qb->execute(); + $result = $qb->executeQuery(); $data = $result->fetchAll(); $result->closeCursor(); @@ -141,14 +133,15 @@ class PublicKeyTokenMapper extends QBMapper { return $entities; } - public function deleteById(string $uid, int $id) { + public function getTokenByUserAndId(string $uid, int $id): ?string { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->delete($this->tableName) + $qb->select('token') + ->from($this->tableName) ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); - $qb->execute(); + return $qb->executeQuery()->fetchOne() ?: null; } /** @@ -161,7 +154,7 @@ class PublicKeyTokenMapper extends QBMapper { $qb->delete($this->tableName) ->where($qb->expr()->eq('name', $qb->createNamedParameter($name), IQueryBuilder::PARAM_STR)) ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); - $qb->execute(); + $qb->executeStatement(); } public function deleteTempToken(PublicKeyToken $except) { @@ -173,7 +166,7 @@ class PublicKeyTokenMapper extends QBMapper { ->andWhere($qb->expr()->neq('id', $qb->createNamedParameter($except->getId()))) ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); - $qb->execute(); + $qb->executeStatement(); } public function hasExpiredTokens(string $uid): bool { @@ -184,7 +177,7 @@ class PublicKeyTokenMapper extends QBMapper { ->andWhere($qb->expr()->eq('password_invalid', $qb->createNamedParameter(true), IQueryBuilder::PARAM_BOOL)) ->setMaxResults(1); - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); $data = $cursor->fetchAll(); $cursor->closeCursor(); @@ -229,4 +222,31 @@ class PublicKeyTokenMapper extends QBMapper { ); $update->executeStatement(); } + + public function updateHashesForUser(string $userId, string $passwordHash): void { + $qb = $this->db->getQueryBuilder(); + $update = $qb->update($this->getTableName()) + ->set('password_hash', $qb->createNamedParameter($passwordHash)) + ->where( + $qb->expr()->eq('uid', $qb->createNamedParameter($userId)) + ); + $update->executeStatement(); + } + + public function getFirstTokenForUser(string $userId): ?PublicKeyToken { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($userId))) + ->setMaxResults(1) + ->orderBy('id'); + $result = $qb->executeQuery(); + + $data = $result->fetch(); + $result->closeCursor(); + if ($data === false) { + return null; + } + return PublicKeyToken::fromRow($data); + } } diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index 26337029d77..12c3a1d535b 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -1,47 +1,37 @@ <?php declare(strict_types=1); - /** - * @copyright Copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Authentication\Token; use OC\Authentication\Exceptions\ExpiredTokenException; use OC\Authentication\Exceptions\InvalidTokenException; -use OC\Authentication\Exceptions\TokenPasswordExpiredException; use OC\Authentication\Exceptions\PasswordlessTokenException; +use OC\Authentication\Exceptions\TokenPasswordExpiredException; use OC\Authentication\Exceptions\WipeTokenException; -use OC\Cache\CappedMemoryCache; use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\TTransactional; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Authentication\Token\IToken as OCPIToken; +use OCP\ICache; +use OCP\ICacheFactory; use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IUserManager; use OCP\Security\ICrypto; +use OCP\Security\IHasher; use Psr\Log\LoggerInterface; class PublicKeyTokenProvider implements IProvider { + public const TOKEN_MIN_LENGTH = 22; + /** Token cache TTL in seconds */ + private const TOKEN_CACHE_TTL = 10; + + use TTransactional; + /** @var PublicKeyTokenMapper */ private $mapper; @@ -51,99 +41,183 @@ class PublicKeyTokenProvider implements IProvider { /** @var IConfig */ private $config; + private IDBConnection $db; + /** @var LoggerInterface */ private $logger; /** @var ITimeFactory */ private $time; - /** @var CappedMemoryCache */ + /** @var ICache */ private $cache; + /** @var IHasher */ + private $hasher; + public function __construct(PublicKeyTokenMapper $mapper, - ICrypto $crypto, - IConfig $config, - LoggerInterface $logger, - ITimeFactory $time) { + ICrypto $crypto, + IConfig $config, + IDBConnection $db, + LoggerInterface $logger, + ITimeFactory $time, + IHasher $hasher, + ICacheFactory $cacheFactory) { $this->mapper = $mapper; $this->crypto = $crypto; $this->config = $config; + $this->db = $db; $this->logger = $logger; $this->time = $time; - $this->cache = new CappedMemoryCache(); + $this->cache = $cacheFactory->isLocalCacheAvailable() + ? $cacheFactory->createLocal('authtoken_') + : $cacheFactory->createInMemory(); + $this->hasher = $hasher; } /** * {@inheritDoc} */ public function generateToken(string $token, - string $uid, - string $loginName, - ?string $password, - string $name, - int $type = IToken::TEMPORARY_TOKEN, - int $remember = IToken::DO_NOT_REMEMBER): IToken { + string $uid, + string $loginName, + ?string $password, + string $name, + int $type = OCPIToken::TEMPORARY_TOKEN, + int $remember = OCPIToken::DO_NOT_REMEMBER, + ?array $scope = null, + ): OCPIToken { + if (strlen($token) < self::TOKEN_MIN_LENGTH) { + $exception = new InvalidTokenException('Token is too short, minimum of ' . self::TOKEN_MIN_LENGTH . ' characters is required, ' . strlen($token) . ' characters given'); + $this->logger->error('Invalid token provided when generating new token', ['exception' => $exception]); + throw $exception; + } + if (mb_strlen($name) > 128) { - throw new InvalidTokenException('The given name is too long'); + $name = mb_substr($name, 0, 120) . '…'; } + // We need to check against one old token to see if there is a password + // hash that we can reuse for detecting outdated passwords + $randomOldToken = $this->mapper->getFirstTokenForUser($uid); + $oldTokenMatches = $randomOldToken && $randomOldToken->getPasswordHash() && $password !== null && $this->hasher->verify(sha1($password) . $password, $randomOldToken->getPasswordHash()); + $dbToken = $this->newToken($token, $uid, $loginName, $password, $name, $type, $remember); + + if ($oldTokenMatches) { + $dbToken->setPasswordHash($randomOldToken->getPasswordHash()); + } + + if ($scope !== null) { + $dbToken->setScope($scope); + } + $this->mapper->insert($dbToken); + if (!$oldTokenMatches && $password !== null) { + $this->updatePasswords($uid, $password); + } + // Add the token to the cache - $this->cache[$dbToken->getToken()] = $dbToken; + $this->cacheToken($dbToken); return $dbToken; } - public function getToken(string $tokenId): IToken { + public function getToken(string $tokenId): OCPIToken { + /** + * Token length: 72 + * @see \OC\Core\Controller\ClientFlowLoginController::generateAppPassword + * @see \OC\Core\Controller\AppPasswordController::getAppPassword + * @see \OC\Core\Command\User\AddAppPassword::execute + * @see \OC\Core\Service\LoginFlowV2Service::flowDone + * @see \OCA\Talk\MatterbridgeManager::generatePassword + * @see \OCA\Preferred_Providers\Controller\PasswordController::generateAppPassword + * @see \OCA\GlobalSiteSelector\TokenHandler::generateAppPassword + * + * Token length: 22-256 - https://www.php.net/manual/en/session.configuration.php#ini.session.sid-length + * @see \OC\User\Session::createSessionToken + * + * Token length: 29 + * @see \OCA\Settings\Controller\AuthSettingsController::generateRandomDeviceToken + * @see \OCA\Registration\Service\RegistrationService::generateAppPassword + */ + if (strlen($tokenId) < self::TOKEN_MIN_LENGTH) { + throw new InvalidTokenException('Token is too short for a generated token, should be the password during basic auth'); + } + $tokenHash = $this->hashToken($tokenId); + if ($token = $this->getTokenFromCache($tokenHash)) { + $this->checkToken($token); + return $token; + } - if (isset($this->cache[$tokenHash])) { - if ($this->cache[$tokenHash] instanceof DoesNotExistException) { - $ex = $this->cache[$tokenHash]; - throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex); - } - $token = $this->cache[$tokenHash]; - } else { + try { + $token = $this->mapper->getToken($tokenHash); + $this->cacheToken($token); + } catch (DoesNotExistException $ex) { try { - $token = $this->mapper->getToken($this->hashToken($tokenId)); - $this->cache[$token->getToken()] = $token; - } catch (DoesNotExistException $ex) { - $this->cache[$tokenHash] = $ex; - throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex); + $token = $this->mapper->getToken($this->hashTokenWithEmptySecret($tokenId)); + $this->rotate($token, $tokenId, $tokenId); + } catch (DoesNotExistException) { + $this->cacheInvalidHash($tokenHash); + throw new InvalidTokenException('Token does not exist: ' . $ex->getMessage(), 0, $ex); } } - if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) { - throw new ExpiredTokenException($token); - } + $this->checkToken($token); - if ($token->getType() === IToken::WIPE_TOKEN) { - throw new WipeTokenException($token); + return $token; + } + + /** + * @throws InvalidTokenException when token doesn't exist + */ + private function getTokenFromCache(string $tokenHash): ?PublicKeyToken { + $serializedToken = $this->cache->get($tokenHash); + if ($serializedToken === false) { + return null; } - if ($token->getPasswordInvalid() === true) { - //The password is invalid we should throw an TokenPasswordExpiredException - throw new TokenPasswordExpiredException($token); + if ($serializedToken === null) { + return null; } - return $token; + $token = unserialize($serializedToken, [ + 'allowed_classes' => [PublicKeyToken::class], + ]); + + return $token instanceof PublicKeyToken ? $token : null; + } + + private function cacheToken(PublicKeyToken $token): void { + $this->cache->set($token->getToken(), serialize($token), self::TOKEN_CACHE_TTL); + } + + private function cacheInvalidHash(string $tokenHash): void { + // Invalid entries can be kept longer in cache since it’s unlikely to reuse them + $this->cache->set($tokenHash, false, self::TOKEN_CACHE_TTL * 2); } - public function getTokenById(int $tokenId): IToken { + public function getTokenById(int $tokenId): OCPIToken { try { $token = $this->mapper->getTokenById($tokenId); } catch (DoesNotExistException $ex) { throw new InvalidTokenException("Token with ID $tokenId does not exist: " . $ex->getMessage(), 0, $ex); } + $this->checkToken($token); + + return $token; + } + + private function checkToken($token): void { if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) { throw new ExpiredTokenException($token); } - if ($token->getType() === IToken::WIPE_TOKEN) { + if ($token->getType() === OCPIToken::WIPE_TOKEN) { throw new WipeTokenException($token); } @@ -151,77 +225,92 @@ class PublicKeyTokenProvider implements IProvider { //The password is invalid we should throw an TokenPasswordExpiredException throw new TokenPasswordExpiredException($token); } - - return $token; } - public function renewSessionToken(string $oldSessionId, string $sessionId): IToken { - $this->cache->clear(); + public function renewSessionToken(string $oldSessionId, string $sessionId): OCPIToken { + return $this->atomic(function () use ($oldSessionId, $sessionId) { + $token = $this->getToken($oldSessionId); - $token = $this->getToken($oldSessionId); - - if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); - } - - $password = null; - if (!is_null($token->getPassword())) { - $privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId); - $password = $this->decryptPassword($token->getPassword(), $privateKey); - } - - $newToken = $this->generateToken( - $sessionId, - $token->getUID(), - $token->getLoginName(), - $password, - $token->getName(), - IToken::TEMPORARY_TOKEN, - $token->getRemember() - ); + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException('Invalid token type'); + } - $this->mapper->delete($token); + $password = null; + if (!is_null($token->getPassword())) { + $privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId); + $password = $this->decryptPassword($token->getPassword(), $privateKey); + } - return $newToken; + $scope = $token->getScope() === '' ? null : $token->getScopeAsArray(); + $newToken = $this->generateToken( + $sessionId, + $token->getUID(), + $token->getLoginName(), + $password, + $token->getName(), + OCPIToken::TEMPORARY_TOKEN, + $token->getRemember(), + $scope, + ); + $this->cacheToken($newToken); + + $this->cacheInvalidHash($token->getToken()); + $this->mapper->delete($token); + + return $newToken; + }, $this->db); } public function invalidateToken(string $token) { - $this->cache->clear(); - + $tokenHash = $this->hashToken($token); $this->mapper->invalidate($this->hashToken($token)); + $this->mapper->invalidate($this->hashTokenWithEmptySecret($token)); + $this->cacheInvalidHash($tokenHash); } public function invalidateTokenById(string $uid, int $id) { - $this->cache->clear(); + $token = $this->mapper->getTokenById($id); + if ($token->getUID() !== $uid) { + return; + } + $this->mapper->invalidate($token->getToken()); + $this->cacheInvalidHash($token->getToken()); - $this->mapper->deleteById($uid, $id); } public function invalidateOldTokens() { - $this->cache->clear(); - - $olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24); + $olderThan = $this->time->getTime() - $this->config->getSystemValueInt('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); - $rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); + $this->mapper->invalidateOld($olderThan, OCPIToken::TEMPORARY_TOKEN, OCPIToken::DO_NOT_REMEMBER); + + $rememberThreshold = $this->time->getTime() - $this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); $this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']); - $this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER); + $this->mapper->invalidateOld($rememberThreshold, OCPIToken::TEMPORARY_TOKEN, OCPIToken::REMEMBER); + + $wipeThreshold = $this->time->getTime() - $this->config->getSystemValueInt('token_auth_wipe_token_retention', 60 * 60 * 24 * 60); + $this->logger->debug('Invalidating auth tokens marked for remote wipe older than ' . date('c', $wipeThreshold), ['app' => 'cron']); + $this->mapper->invalidateOld($wipeThreshold, OCPIToken::WIPE_TOKEN); + + $authTokenThreshold = $this->time->getTime() - $this->config->getSystemValueInt('token_auth_token_retention', 60 * 60 * 24 * 365); + $this->logger->debug('Invalidating auth tokens older than ' . date('c', $authTokenThreshold), ['app' => 'cron']); + $this->mapper->invalidateOld($authTokenThreshold, OCPIToken::PERMANENT_TOKEN); } - public function updateToken(IToken $token) { - $this->cache->clear(); + public function invalidateLastUsedBefore(string $uid, int $before): void { + $this->mapper->invalidateLastUsedBefore($uid, $before); + } + public function updateToken(OCPIToken $token) { if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } $this->mapper->update($token); + $this->cacheToken($token); } - public function updateTokenActivity(IToken $token) { - $this->cache->clear(); - + public function updateTokenActivity(OCPIToken $token) { if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } $activityInterval = $this->config->getSystemValueInt('token_auth_activity_update', 60); @@ -232,6 +321,7 @@ class PublicKeyTokenProvider implements IProvider { if ($token->getLastActivity() < ($now - $activityInterval)) { $token->setLastActivity($now); $this->mapper->updateActivity($token, $now); + $this->cacheToken($token); } } @@ -239,9 +329,9 @@ class PublicKeyTokenProvider implements IProvider { return $this->mapper->getTokenByUser($uid); } - public function getPassword(IToken $savedToken, string $tokenId): string { + public function getPassword(OCPIToken $savedToken, string $tokenId): string { if (!($savedToken instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } if ($savedToken->getPassword() === null) { @@ -255,30 +345,34 @@ class PublicKeyTokenProvider implements IProvider { return $this->decryptPassword($savedToken->getPassword(), $privateKey); } - public function setPassword(IToken $token, string $tokenId, string $password) { - $this->cache->clear(); - + public function setPassword(OCPIToken $token, string $tokenId, string $password) { if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } - // When changing passwords all temp tokens are deleted - $this->mapper->deleteTempToken($token); - - // Update the password for all tokens - $tokens = $this->mapper->getTokenByUser($token->getUID()); - foreach ($tokens as $t) { - $publicKey = $t->getPublicKey(); - $t->setPassword($this->encryptPassword($password, $publicKey)); - $this->updateToken($t); - } + $this->atomic(function () use ($password, $token) { + // When changing passwords all temp tokens are deleted + $this->mapper->deleteTempToken($token); + + // Update the password for all tokens + $tokens = $this->mapper->getTokenByUser($token->getUID()); + $hashedPassword = $this->hashPassword($password); + foreach ($tokens as $t) { + $publicKey = $t->getPublicKey(); + $t->setPassword($this->encryptPassword($password, $publicKey)); + $t->setPasswordHash($hashedPassword); + $this->updateToken($t); + } + }, $this->db); } - public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken { - $this->cache->clear(); + private function hashPassword(string $password): string { + return $this->hasher->hash(sha1($password) . $password); + } + public function rotate(OCPIToken $token, string $oldTokenId, string $newTokenId): OCPIToken { if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } // Decrypt private key with oldTokenId @@ -293,7 +387,7 @@ class PublicKeyTokenProvider implements IProvider { } private function encrypt(string $plaintext, string $token): string { - $secret = $this->config->getSystemValue('secret'); + $secret = $this->config->getSystemValueString('secret'); return $this->crypto->encrypt($plaintext, $token . $secret); } @@ -301,13 +395,18 @@ class PublicKeyTokenProvider implements IProvider { * @throws InvalidTokenException */ private function decrypt(string $cipherText, string $token): string { - $secret = $this->config->getSystemValue('secret'); + $secret = $this->config->getSystemValueString('secret'); try { return $this->crypto->decrypt($cipherText, $token . $secret); } catch (\Exception $ex) { - // Delete the invalid token - $this->invalidateToken($token); - throw new InvalidTokenException("Could not decrypt token password: " . $ex->getMessage(), 0, $ex); + // Retry with empty secret as a fallback for instances where the secret might not have been set by accident + try { + return $this->crypto->decrypt($cipherText, $token); + } catch (\Exception $ex2) { + // Delete the invalid token + $this->invalidateToken($token); + throw new InvalidTokenException('Could not decrypt token password: ' . $ex->getMessage(), 0, $ex2); + } } } @@ -326,27 +425,34 @@ class PublicKeyTokenProvider implements IProvider { } private function hashToken(string $token): string { - $secret = $this->config->getSystemValue('secret'); + $secret = $this->config->getSystemValueString('secret'); return hash('sha512', $token . $secret); } /** + * @deprecated 26.0.0 Fallback for instances where the secret might not have been set by accident + */ + private function hashTokenWithEmptySecret(string $token): string { + return hash('sha512', $token); + } + + /** * @throws \RuntimeException when OpenSSL reports a problem */ private function newToken(string $token, - string $uid, - string $loginName, - $password, - string $name, - int $type, - int $remember): PublicKeyToken { + string $uid, + string $loginName, + $password, + string $name, + int $type, + int $remember): PublicKeyToken { $dbToken = new PublicKeyToken(); $dbToken->setUid($uid); $dbToken->setLoginName($loginName); $config = array_merge([ 'digest_alg' => 'sha512', - 'private_key_bits' => 2048, + 'private_key_bits' => $password !== null && strlen($password) > 250 ? 4096 : 2048, ], $this->config->getSystemValue('openssl', [])); // Generate new key @@ -368,8 +474,12 @@ class PublicKeyTokenProvider implements IProvider { $dbToken->setPublicKey($publicKey); $dbToken->setPrivateKey($this->encrypt($privateKey, $token)); - if (!is_null($password)) { + if (!is_null($password) && $this->config->getSystemValueBool('auth.storeCryptedPassword', true)) { + if (strlen($password) > IUserManager::MAX_PASSWORD_LENGTH) { + throw new \RuntimeException('Trying to save a password with more than 469 characters is not supported. If you want to use big passwords, disable the auth.storeCryptedPassword option in config.php'); + } $dbToken->setPassword($this->encryptPassword($password, $publicKey)); + $dbToken->setPasswordHash($this->hashPassword($password)); } $dbToken->setName($name); @@ -383,33 +493,67 @@ class PublicKeyTokenProvider implements IProvider { return $dbToken; } - public function markPasswordInvalid(IToken $token, string $tokenId) { - $this->cache->clear(); - + public function markPasswordInvalid(OCPIToken $token, string $tokenId) { if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } $token->setPasswordInvalid(true); $this->mapper->update($token); + $this->cacheToken($token); } public function updatePasswords(string $uid, string $password) { - $this->cache->clear(); - // prevent setting an empty pw as result of pw-less-login - if ($password === '') { + if ($password === '' || !$this->config->getSystemValueBool('auth.storeCryptedPassword', true)) { return; } - // Update the password for all tokens - $tokens = $this->mapper->getTokenByUser($uid); - foreach ($tokens as $t) { - $publicKey = $t->getPublicKey(); - $t->setPassword($this->encryptPassword($password, $publicKey)); - $t->setPasswordInvalid(false); - $this->updateToken($t); - } + $this->atomic(function () use ($password, $uid) { + // Update the password for all tokens + $tokens = $this->mapper->getTokenByUser($uid); + $newPasswordHash = null; + + /** + * - true: The password hash could not be verified anymore + * and the token needs to be updated with the newly encrypted password + * - false: The hash could still be verified + * - missing: The hash needs to be verified + */ + $hashNeedsUpdate = []; + + foreach ($tokens as $t) { + if (!isset($hashNeedsUpdate[$t->getPasswordHash()])) { + if ($t->getPasswordHash() === null) { + $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true; + } elseif (!$this->hasher->verify(sha1($password) . $password, $t->getPasswordHash())) { + $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true; + } else { + $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = false; + } + } + $needsUpdating = $hashNeedsUpdate[$t->getPasswordHash() ?: ''] ?? true; + + if ($needsUpdating) { + if ($newPasswordHash === null) { + $newPasswordHash = $this->hashPassword($password); + } + + $publicKey = $t->getPublicKey(); + $t->setPassword($this->encryptPassword($password, $publicKey)); + $t->setPasswordHash($newPasswordHash); + $t->setPasswordInvalid(false); + $this->updateToken($t); + } + } + + // If password hashes are different we update them all to be equal so + // that the next execution only needs to verify once + if (count($hashNeedsUpdate) > 1) { + $newPasswordHash = $this->hashPassword($password); + $this->mapper->updateHashesForUser($uid, $newPasswordHash); + } + }, $this->db); } private function logOpensslError() { diff --git a/lib/private/Authentication/Token/RemoteWipe.php b/lib/private/Authentication/Token/RemoteWipe.php index 9e152d16a04..80ba330b66d 100644 --- a/lib/private/Authentication/Token/RemoteWipe.php +++ b/lib/private/Authentication/Token/RemoteWipe.php @@ -3,41 +3,21 @@ declare(strict_types=1); /** - * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Authentication\Token; -use Psr\Log\LoggerInterface; -use function array_filter; use OC\Authentication\Events\RemoteWipeFinished; use OC\Authentication\Events\RemoteWipeStarted; -use OC\Authentication\Exceptions\InvalidTokenException; -use OC\Authentication\Exceptions\WipeTokenException; +use OCP\Authentication\Exceptions\InvalidTokenException; +use OCP\Authentication\Exceptions\WipeTokenException; use OCP\EventDispatcher\IEventDispatcher; use OCP\IUser; +use Psr\Log\LoggerInterface; +use function array_filter; class RemoteWipe { - /** @var IProvider */ private $tokenProvider; @@ -48,8 +28,8 @@ class RemoteWipe { private $logger; public function __construct(IProvider $tokenProvider, - IEventDispatcher $eventDispatcher, - LoggerInterface $logger) { + IEventDispatcher $eventDispatcher, + LoggerInterface $logger) { $this->tokenProvider = $tokenProvider; $this->eventDispatcher = $eventDispatcher; $this->logger = $logger; @@ -118,7 +98,7 @@ class RemoteWipe { $dbToken = $e->getToken(); - $this->logger->info("user " . $dbToken->getUID() . " started a remote wipe"); + $this->logger->info('user ' . $dbToken->getUID() . ' started a remote wipe'); $this->eventDispatcher->dispatch(RemoteWipeStarted::class, new RemoteWipeStarted($dbToken)); @@ -146,7 +126,7 @@ class RemoteWipe { $this->tokenProvider->invalidateToken($token); - $this->logger->info("user " . $dbToken->getUID() . " finished a remote wipe"); + $this->logger->info('user ' . $dbToken->getUID() . ' finished a remote wipe'); $this->eventDispatcher->dispatch(RemoteWipeFinished::class, new RemoteWipeFinished($dbToken)); return true; diff --git a/lib/private/Authentication/Token/TokenCleanupJob.php b/lib/private/Authentication/Token/TokenCleanupJob.php new file mode 100644 index 00000000000..e6d1e69e9b4 --- /dev/null +++ b/lib/private/Authentication/Token/TokenCleanupJob.php @@ -0,0 +1,26 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Authentication\Token; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; + +class TokenCleanupJob extends TimedJob { + private IProvider $provider; + + public function __construct(ITimeFactory $time, IProvider $provider) { + parent::__construct($time); + $this->provider = $provider; + // Run once a day at off-peak time + $this->setInterval(24 * 60 * 60); + $this->setTimeSensitivity(self::TIME_INSENSITIVE); + } + + protected function run($argument) { + $this->provider->invalidateOldTokens(); + } +} |