diff options
author | blizzz <blizzz@arthur-schiwon.de> | 2018-06-19 11:11:09 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-06-19 11:11:09 +0300 |
commit | 7ef722e7a626e07f36b2c2c01d650651b1e4e126 (patch) | |
tree | 6bbff693403e6f394ced4f9eeacb62f6f5c3c86b /lib | |
parent | b8492c694480e210aaf2c794ed18adf5a9a751b2 (diff) | |
parent | 82959ca93e229e1f16e1843cd4a2f7523b8ac0bf (diff) |
Merge pull request #9485 from nextcloud/feature/9441/multiple_token_providers
Add new public key token provider (tokens survive password change)
Diffstat (limited to 'lib')
-rw-r--r-- | lib/composer/composer/autoload_classmap.php | 5 | ||||
-rw-r--r-- | lib/composer/composer/autoload_static.php | 5 | ||||
-rw-r--r-- | lib/private/Authentication/Token/DefaultToken.php | 7 | ||||
-rw-r--r-- | lib/private/Authentication/Token/DefaultTokenMapper.php | 33 | ||||
-rw-r--r-- | lib/private/Authentication/Token/DefaultTokenProvider.php | 27 | ||||
-rw-r--r-- | lib/private/Authentication/Token/IProvider.php | 9 | ||||
-rw-r--r-- | lib/private/Authentication/Token/Manager.php | 230 | ||||
-rw-r--r-- | lib/private/Authentication/Token/PublicKeyToken.php | 217 | ||||
-rw-r--r-- | lib/private/Authentication/Token/PublicKeyTokenMapper.php | 172 | ||||
-rw-r--r-- | lib/private/Authentication/Token/PublicKeyTokenProvider.php | 320 | ||||
-rw-r--r-- | lib/private/Server.php | 10 |
11 files changed, 984 insertions, 51 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 1d319026175..77729886601 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -421,6 +421,10 @@ return array( 'OC\\Authentication\\Token\\ExpiredTokenException' => $baseDir . '/lib/private/Authentication/Exceptions/ExpiredTokenException.php', 'OC\\Authentication\\Token\\IProvider' => $baseDir . '/lib/private/Authentication/Token/IProvider.php', 'OC\\Authentication\\Token\\IToken' => $baseDir . '/lib/private/Authentication/Token/IToken.php', + 'OC\\Authentication\\Token\\Manager' => $baseDir . '/lib/private/Authentication/Token/Manager.php', + 'OC\\Authentication\\Token\\PublicKeyToken' => $baseDir . '/lib/private/Authentication/Token/PublicKeyToken.php', + 'OC\\Authentication\\Token\\PublicKeyTokenMapper' => $baseDir . '/lib/private/Authentication/Token/PublicKeyTokenMapper.php', + 'OC\\Authentication\\Token\\PublicKeyTokenProvider' => $baseDir . '/lib/private/Authentication/Token/PublicKeyTokenProvider.php', 'OC\\Authentication\\TwoFactorAuth\\Manager' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/Manager.php', 'OC\\Avatar' => $baseDir . '/lib/private/Avatar.php', 'OC\\AvatarManager' => $baseDir . '/lib/private/AvatarManager.php', @@ -570,6 +574,7 @@ return array( 'OC\\Core\\Migrations\\Version14000Date20180129121024' => $baseDir . '/core/Migrations/Version14000Date20180129121024.php', 'OC\\Core\\Migrations\\Version14000Date20180404140050' => $baseDir . '/core/Migrations/Version14000Date20180404140050.php', 'OC\\Core\\Migrations\\Version14000Date20180516101403' => $baseDir . '/core/Migrations/Version14000Date20180516101403.php', + 'OC\\Core\\Migrations\\Version14000Date20180518120534' => $baseDir . '/core/Migrations/Version14000Date20180518120534.php', 'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', 'OC\\DB\\AdapterMySQL' => $baseDir . '/lib/private/DB/AdapterMySQL.php', 'OC\\DB\\AdapterOCI8' => $baseDir . '/lib/private/DB/AdapterOCI8.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index b66a7b18192..be9c71d8246 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -451,6 +451,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Authentication\\Token\\ExpiredTokenException' => __DIR__ . '/../../..' . '/lib/private/Authentication/Exceptions/ExpiredTokenException.php', 'OC\\Authentication\\Token\\IProvider' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/IProvider.php', 'OC\\Authentication\\Token\\IToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/IToken.php', + 'OC\\Authentication\\Token\\Manager' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/Manager.php', + 'OC\\Authentication\\Token\\PublicKeyToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyToken.php', + 'OC\\Authentication\\Token\\PublicKeyTokenMapper' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyTokenMapper.php', + 'OC\\Authentication\\Token\\PublicKeyTokenProvider' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyTokenProvider.php', 'OC\\Authentication\\TwoFactorAuth\\Manager' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/Manager.php', 'OC\\Avatar' => __DIR__ . '/../../..' . '/lib/private/Avatar.php', 'OC\\AvatarManager' => __DIR__ . '/../../..' . '/lib/private/AvatarManager.php', @@ -600,6 +604,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Migrations\\Version14000Date20180129121024' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180129121024.php', 'OC\\Core\\Migrations\\Version14000Date20180404140050' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180404140050.php', 'OC\\Core\\Migrations\\Version14000Date20180516101403' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180516101403.php', + 'OC\\Core\\Migrations\\Version14000Date20180518120534' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180518120534.php', 'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', 'OC\\DB\\AdapterMySQL' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterMySQL.php', 'OC\\DB\\AdapterOCI8' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterOCI8.php', diff --git a/lib/private/Authentication/Token/DefaultToken.php b/lib/private/Authentication/Token/DefaultToken.php index 67aa89ea66b..85ea0dc4cdd 100644 --- a/lib/private/Authentication/Token/DefaultToken.php +++ b/lib/private/Authentication/Token/DefaultToken.php @@ -37,9 +37,12 @@ use OCP\AppFramework\Db\Entity; * @method void setRemember(int $remember) * @method void setLastActivity(int $lastactivity) * @method int getLastActivity() + * @method void setVersion(int $version) */ class DefaultToken extends Entity implements IToken { + const VERSION = 1; + /** @var string user UID */ protected $uid; @@ -73,6 +76,9 @@ class DefaultToken extends Entity implements IToken { /** @var int */ protected $expires; + /** @var int */ + protected $version; + public function __construct() { $this->addType('uid', 'string'); $this->addType('loginName', 'string'); @@ -85,6 +91,7 @@ class DefaultToken extends Entity implements IToken { $this->addType('lastCheck', 'int'); $this->addType('scope', 'string'); $this->addType('expires', 'int'); + $this->addType('version', 'int'); } public function getId(): int { diff --git a/lib/private/Authentication/Token/DefaultTokenMapper.php b/lib/private/Authentication/Token/DefaultTokenMapper.php index a67d7d151e9..b8df00ff094 100644 --- a/lib/private/Authentication/Token/DefaultTokenMapper.php +++ b/lib/private/Authentication/Token/DefaultTokenMapper.php @@ -33,7 +33,6 @@ use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; -use OCP\IUser; class DefaultTokenMapper extends QBMapper { @@ -50,8 +49,8 @@ class DefaultTokenMapper extends QBMapper { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') - ->where($qb->expr()->eq('token', $qb->createParameter('token'))) - ->setParameter('token', $token) + ->where($qb->expr()->eq('token', $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); } @@ -66,6 +65,7 @@ class DefaultTokenMapper extends QBMapper { ->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(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); } @@ -79,9 +79,10 @@ class DefaultTokenMapper extends QBMapper { public function getToken(string $token): DefaultToken { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $result = $qb->select('*') + $result = $qb->select('id', 'uid', 'login_name', 'password', 'name', 'token', 'type', 'remember', 'last_activity', 'last_check', 'scope', 'expires', 'version') ->from('authtoken') ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); $data = $result->fetch(); @@ -102,9 +103,10 @@ class DefaultTokenMapper extends QBMapper { public function getTokenById(int $id): DefaultToken { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $result = $qb->select('*') + $result = $qb->select('id', 'uid', 'login_name', 'password', 'name', 'token', 'type', 'remember', 'last_activity', 'last_check', 'scope', 'expires', 'version') ->from('authtoken') ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); $data = $result->fetch(); @@ -121,15 +123,16 @@ class DefaultTokenMapper extends QBMapper { * The provider may limit the number of result rows in case of an abuse * where a high number of (session) tokens is generated * - * @param IUser $user + * @param string $uid * @return DefaultToken[] */ - public function getTokenByUser(IUser $user): array { + public function getTokenByUser(string $uid): array { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->select('*') + $qb->select('id', 'uid', 'login_name', 'password', 'name', 'token', 'type', 'remember', 'last_activity', 'last_check', 'scope', 'expires', 'version') ->from('authtoken') - ->where($qb->expr()->eq('uid', $qb->createNamedParameter($user->getUID()))) + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))) ->setMaxResults(1000); $result = $qb->execute(); $data = $result->fetchAll(); @@ -142,16 +145,13 @@ class DefaultTokenMapper extends QBMapper { return $entities; } - /** - * @param IUser $user - * @param int $id - */ - public function deleteById(IUser $user, int $id) { + public function deleteById(string $uid, int $id) { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) - ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($user->getUID()))); + ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))); $qb->execute(); } @@ -163,7 +163,8 @@ class DefaultTokenMapper extends QBMapper { public function deleteByName(string $name) { $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') - ->where($qb->expr()->eq('name', $qb->createNamedParameter($name), IQueryBuilder::PARAM_STR)); + ->where($qb->expr()->eq('name', $qb->createNamedParameter($name), IQueryBuilder::PARAM_STR)) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))); $qb->execute(); } diff --git a/lib/private/Authentication/Token/DefaultTokenProvider.php b/lib/private/Authentication/Token/DefaultTokenProvider.php index 7a43dbb23e1..8c2d8c33a97 100644 --- a/lib/private/Authentication/Token/DefaultTokenProvider.php +++ b/lib/private/Authentication/Token/DefaultTokenProvider.php @@ -35,7 +35,6 @@ use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; use OCP\ILogger; -use OCP\IUser; use OCP\Security\ICrypto; class DefaultTokenProvider implements IProvider { @@ -105,6 +104,7 @@ class DefaultTokenProvider implements IProvider { $dbToken->setRemember($remember); $dbToken->setLastActivity($this->time->getTime()); $dbToken->setLastCheck($this->time->getTime()); + $dbToken->setVersion(DefaultToken::VERSION); $this->mapper->insert($dbToken); @@ -143,17 +143,8 @@ class DefaultTokenProvider implements IProvider { } } - /** - * Get all tokens of a user - * - * The provider may limit the number of result rows in case of an abuse - * where a high number of (session) tokens is generated - * - * @param IUser $user - * @return IToken[] - */ - public function getTokenByUser(IUser $user): array { - return $this->mapper->getTokenByUser($user); + public function getTokenByUser(string $uid): array { + return $this->mapper->getTokenByUser($uid); } /** @@ -265,14 +256,8 @@ class DefaultTokenProvider implements IProvider { $this->mapper->invalidate($this->hashToken($token)); } - /** - * Invalidate (delete) the given token - * - * @param IUser $user - * @param int $id - */ - public function invalidateTokenById(IUser $user, int $id) { - $this->mapper->deleteById($user, $id); + public function invalidateTokenById(string $uid, int $id) { + $this->mapper->deleteById($uid, $id); } /** @@ -313,7 +298,7 @@ class DefaultTokenProvider implements IProvider { * @param string $token * @return string */ - private function hashToken(string $token) { + private function hashToken(string $token): string { $secret = $this->config->getSystemValue('secret'); return hash('sha512', $token . $secret); } diff --git a/lib/private/Authentication/Token/IProvider.php b/lib/private/Authentication/Token/IProvider.php index 0efffefac68..ab46bd12126 100644 --- a/lib/private/Authentication/Token/IProvider.php +++ b/lib/private/Authentication/Token/IProvider.php @@ -28,7 +28,6 @@ namespace OC\Authentication\Token; use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Exceptions\PasswordlessTokenException; -use OCP\IUser; interface IProvider { @@ -92,10 +91,10 @@ interface IProvider { /** * Invalidate (delete) the given token * - * @param IUser $user + * @param string $uid * @param int $id */ - public function invalidateTokenById(IUser $user, int $id); + public function invalidateTokenById(string $uid, int $id); /** * Invalidate (delete) old session tokens @@ -122,10 +121,10 @@ interface IProvider { * The provider may limit the number of result rows in case of an abuse * where a high number of (session) tokens is generated * - * @param IUser $user + * @param string $uid * @return IToken[] */ - public function getTokenByUser(IUser $user): array; + public function getTokenByUser(string $uid): array; /** * Get the (unencrypted) password of the given token diff --git a/lib/private/Authentication/Token/Manager.php b/lib/private/Authentication/Token/Manager.php new file mode 100644 index 00000000000..254a1598943 --- /dev/null +++ b/lib/private/Authentication/Token/Manager.php @@ -0,0 +1,230 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright 2018, Roeland Jago Douma <roeland@famdouma.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/> + * + */ + +namespace OC\Authentication\Token; + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Exceptions\PasswordlessTokenException; + +class Manager implements IProvider { + + /** @var DefaultTokenProvider */ + private $defaultTokenProvider; + + /** @var PublicKeyTokenProvider */ + private $publicKeyTokenProvider; + + public function __construct(DefaultTokenProvider $defaultTokenProvider, PublicKeyTokenProvider $publicKeyTokenProvider) { + $this->defaultTokenProvider = $defaultTokenProvider; + $this->publicKeyTokenProvider = $publicKeyTokenProvider; + } + + /** + * Create and persist a new token + * + * @param string $token + * @param string $uid + * @param string $loginName + * @param string|null $password + * @param string $name + * @param int $type token type + * @param int $remember whether the session token should be used for remember-me + * @return IToken + */ + public function generateToken(string $token, + string $uid, + string $loginName, + $password, + string $name, + int $type = IToken::TEMPORARY_TOKEN, + int $remember = IToken::DO_NOT_REMEMBER): IToken { + return $this->publicKeyTokenProvider->generateToken( + $token, + $uid, + $loginName, + $password, + $name, + $type, + $remember + ); + } + + /** + * Save the updated token + * + * @param IToken $token + * @throws InvalidTokenException + */ + public function updateToken(IToken $token) { + $provider = $this->getProvider($token); + $provider->updateToken($token); + } + + /** + * Update token activity timestamp + * + * @throws InvalidTokenException + * @param IToken $token + */ + public function updateTokenActivity(IToken $token) { + $provider = $this->getProvider($token); + $provider->updateTokenActivity($token); + } + + /** + * @param string $uid + * @return IToken[] + */ + public function getTokenByUser(string $uid): array { + $old = $this->defaultTokenProvider->getTokenByUser($uid); + $new = $this->publicKeyTokenProvider->getTokenByUser($uid); + + return array_merge($old, $new); + } + + /** + * Get a token by token + * + * @param string $tokenId + * @throws InvalidTokenException + * @return IToken + */ + public function getToken(string $tokenId): IToken { + try { + return $this->publicKeyTokenProvider->getToken($tokenId); + } catch (InvalidTokenException $e) { + // No worries we try to convert it to a PublicKey Token + } + + //Convert! + $token = $this->defaultTokenProvider->getToken($tokenId); + + try { + $password = $this->defaultTokenProvider->getPassword($token, $tokenId); + } catch (PasswordlessTokenException $e) { + $password = null; + } + + return $this->publicKeyTokenProvider->convertToken($token, $tokenId, $password); + } + + /** + * Get a token by token id + * + * @param int $tokenId + * @throws InvalidTokenException + * @return IToken + */ + public function getTokenById(int $tokenId): IToken { + try { + return $this->publicKeyTokenProvider->getTokenById($tokenId); + } catch (InvalidTokenException $e) { + return $this->defaultTokenProvider->getTokenById($tokenId); + } + } + + /** + * @param string $oldSessionId + * @param string $sessionId + * @throws InvalidTokenException + */ + public function renewSessionToken(string $oldSessionId, string $sessionId) { + try { + $this->publicKeyTokenProvider->renewSessionToken($oldSessionId, $sessionId); + } catch (InvalidTokenException $e) { + $this->defaultTokenProvider->renewSessionToken($oldSessionId, $sessionId); + } + } + + /** + * @param IToken $savedToken + * @param string $tokenId session token + * @throws InvalidTokenException + * @throws PasswordlessTokenException + * @return string + */ + public function getPassword(IToken $savedToken, string $tokenId): string { + $provider = $this->getProvider($savedToken); + return $provider->getPassword($savedToken, $tokenId); + } + + public function setPassword(IToken $token, string $tokenId, string $password) { + $provider = $this->getProvider($token); + $provider->setPassword($token, $tokenId, $password); + } + + public function invalidateToken(string $token) { + $this->defaultTokenProvider->invalidateToken($token); + $this->publicKeyTokenProvider->invalidateToken($token); + } + + public function invalidateTokenById(string $uid, int $id) { + $this->defaultTokenProvider->invalidateTokenById($uid, $id); + $this->publicKeyTokenProvider->invalidateTokenById($uid, $id); + } + + public function invalidateOldTokens() { + $this->defaultTokenProvider->invalidateOldTokens(); + $this->publicKeyTokenProvider->invalidateOldTokens(); + } + + /** + * @param IToken $token + * @param string $oldTokenId + * @param string $newTokenId + * @return IToken + * @throws InvalidTokenException + */ + public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken { + if ($token instanceof DefaultToken) { + try { + $password = $this->defaultTokenProvider->getPassword($token, $oldTokenId); + } catch (PasswordlessTokenException $e) { + $password = null; + } + + return $this->publicKeyTokenProvider->convertToken($token, $newTokenId, $password); + } + + if ($token instanceof PublicKeyToken) { + return $this->publicKeyTokenProvider->rotate($token, $oldTokenId, $newTokenId); + } + + throw new InvalidTokenException(); + } + + /** + * @param IToken $token + * @return IProvider + * @throws InvalidTokenException + */ + private function getProvider(IToken $token): IProvider { + if ($token instanceof DefaultToken) { + return $this->defaultTokenProvider; + } + if ($token instanceof PublicKeyToken) { + return $this->publicKeyTokenProvider; + } + throw new InvalidTokenException(); + } +} diff --git a/lib/private/Authentication/Token/PublicKeyToken.php b/lib/private/Authentication/Token/PublicKeyToken.php new file mode 100644 index 00000000000..0e793ce8c7c --- /dev/null +++ b/lib/private/Authentication/Token/PublicKeyToken.php @@ -0,0 +1,217 @@ +<?php +/** @noinspection ALL */ +declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl> + * + * @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/>. + * + */ + +namespace OC\Authentication\Token; + +use OCP\AppFramework\Db\Entity; + +/** + * @method void setId(int $id) + * @method void setUid(string $uid); + * @method void setLoginName(string $loginname) + * @method void setName(string $name) + * @method string getToken() + * @method void setType(int $type) + * @method int getType() + * @method void setRemember(int $remember) + * @method void setLastActivity(int $lastactivity) + * @method int getLastActivity() + * @method string getPrivateKey() + * @method void setPrivateKey(string $key) + * @method string getPublicKey() + * @method void setPublicKey(string $key) + * @method void setVersion(int $version) + */ +class PublicKeyToken extends Entity implements IToken { + + const VERSION = 2; + + /** @var string user UID */ + protected $uid; + + /** @var string login name used for generating the token */ + protected $loginName; + + /** @var string encrypted user password */ + protected $password; + + /** @var string token name (e.g. browser/OS) */ + protected $name; + + /** @var string */ + protected $token; + + /** @var int */ + protected $type; + + /** @var int */ + protected $remember; + + /** @var int */ + protected $lastActivity; + + /** @var int */ + protected $lastCheck; + + /** @var string */ + protected $scope; + + /** @var int */ + protected $expires; + + /** @var string */ + protected $privateKey; + + /** @var string */ + protected $publicKey; + + /** @var int */ + protected $version; + + public function __construct() { + $this->addType('uid', 'string'); + $this->addType('loginName', 'string'); + $this->addType('password', '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('scope', 'string'); + $this->addType('expires', 'int'); + $this->addType('publicKey', 'string'); + $this->addType('privateKey', 'string'); + $this->addType('version', 'int'); + } + + public function getId(): int { + return $this->id; + } + + public function getUID(): string { + return $this->uid; + } + + /** + * Get the login name used when generating the token + * + * @return string + */ + public function getLoginName(): string { + return parent::getLoginName(); + } + + /** + * Get the (encrypted) login password + * + * @return string|null + */ + public function getPassword() { + return parent::getPassword(); + } + + public function jsonSerialize() { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'lastActivity' => $this->lastActivity, + 'type' => $this->type, + 'scope' => $this->getScopeAsArray() + ]; + } + + /** + * Get the timestamp of the last password check + * + * @return int + */ + public function getLastCheck(): int { + return parent::getLastCheck(); + } + + /** + * Get the timestamp of the last password check + * + * @param int $time + */ + public function setLastCheck(int $time) { + parent::setLastCheck($time); + } + + public function getScope(): string { + $scope = parent::getScope(); + if ($scope === null) { + return ''; + } + + return $scope; + } + + public function getScopeAsArray(): array { + $scope = json_decode($this->getScope(), true); + if (!$scope) { + return [ + 'filesystem'=> true + ]; + } + return $scope; + } + + public function setScope($scope) { + if (is_array($scope)) { + parent::setScope(json_encode($scope)); + } else { + parent::setScope((string)$scope); + } + } + + public function getName(): string { + return parent::getName(); + } + + public function getRemember(): int { + return parent::getRemember(); + } + + public function setToken(string $token) { + parent::setToken($token); + } + + public function setPassword(string $password = null) { + parent::setPassword($password); + } + + public function setExpires($expires) { + parent::setExpires($expires); + } + + /** + * @return int|null + */ + public function getExpires() { + return parent::getExpires(); + } +} diff --git a/lib/private/Authentication/Token/PublicKeyTokenMapper.php b/lib/private/Authentication/Token/PublicKeyTokenMapper.php new file mode 100644 index 00000000000..5e5c69dbc46 --- /dev/null +++ b/lib/private/Authentication/Token/PublicKeyTokenMapper.php @@ -0,0 +1,172 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl> + * + * @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/>. + * + */ + +namespace OC\Authentication\Token; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class PublicKeyTokenMapper extends QBMapper { + + public function __construct(IDBConnection $db) { + parent::__construct($db, 'authtoken'); + } + + /** + * Invalidate (delete) a given token + * + * @param string $token + */ + public function invalidate(string $token) { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->delete('authtoken') + ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) + ->execute(); + } + + /** + * @param int $olderThan + * @param int $remember + */ + public function invalidateOld(int $olderThan, int $remember = IToken::DO_NOT_REMEMBER) { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->delete('authtoken') + ->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(); + } + + /** + * Get the user UID for the given token + * + * @throws DoesNotExistException + */ + public function getToken(string $token): PublicKeyToken { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $result = $qb->select('*') + ->from('authtoken') + ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) + ->execute(); + + $data = $result->fetch(); + $result->closeCursor(); + if ($data === false) { + throw new DoesNotExistException('token does not exist'); + } + return PublicKeyToken::fromRow($data); + } + + /** + * Get the token for $id + * + * @throws DoesNotExistException + */ + public function getTokenById(int $id): PublicKeyToken { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $result = $qb->select('*') + ->from('authtoken') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) + ->execute(); + + $data = $result->fetch(); + $result->closeCursor(); + if ($data === false) { + throw new DoesNotExistException('token does not exist'); + } + return PublicKeyToken::fromRow($data); + } + + /** + * Get all tokens of a user + * + * The provider may limit the number of result rows in case of an abuse + * where a high number of (session) tokens is generated + * + * @param string $uid + * @return PublicKeyToken[] + */ + public function getTokenByUser(string $uid): array { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('authtoken') + ->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(); + $data = $result->fetchAll(); + $result->closeCursor(); + + $entities = array_map(function ($row) { + return PublicKeyToken::fromRow($row); + }, $data); + + return $entities; + } + + public function deleteById(string $uid, int $id) { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->delete('authtoken') + ->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(); + } + + /** + * delete all auth token which belong to a specific client if the client was deleted + * + * @param string $name + */ + public function deleteByName(string $name) { + $qb = $this->db->getQueryBuilder(); + $qb->delete('authtoken') + ->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(); + } + + public function deleteTempToken(PublicKeyToken $except) { + $qb = $this->db->getQueryBuilder(); + + $qb->delete('authtoken') + ->where($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN))) + ->andWhere($qb->expr()->neq('id', $qb->createNamedParameter($except->getId()))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); + + $qb->execute(); + } +} diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php new file mode 100644 index 00000000000..f6a6fc3455f --- /dev/null +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -0,0 +1,320 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright 2018, Roeland Jago Douma <roeland@famdouma.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/> + * + */ + +namespace OC\Authentication\Token; + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Exceptions\PasswordlessTokenException; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\ILogger; +use OCP\Security\ICrypto; + +class PublicKeyTokenProvider implements IProvider { + /** @var PublicKeyTokenMapper */ + private $mapper; + + /** @var ICrypto */ + private $crypto; + + /** @var IConfig */ + private $config; + + /** @var ILogger $logger */ + private $logger; + + /** @var ITimeFactory $time */ + private $time; + + public function __construct(PublicKeyTokenMapper $mapper, + ICrypto $crypto, + IConfig $config, + ILogger $logger, + ITimeFactory $time) { + $this->mapper = $mapper; + $this->crypto = $crypto; + $this->config = $config; + $this->logger = $logger; + $this->time = $time; + } + + public function generateToken(string $token, + string $uid, + string $loginName, + $password, + string $name, + int $type = IToken::TEMPORARY_TOKEN, + int $remember = IToken::DO_NOT_REMEMBER): IToken { + $dbToken = $this->newToken($token, $uid, $loginName, $password, $name, $type, $remember); + + $this->mapper->insert($dbToken); + + return $dbToken; + } + + public function getToken(string $tokenId): IToken { + try { + $token = $this->mapper->getToken($this->hashToken($tokenId)); + } catch (DoesNotExistException $ex) { + throw new InvalidTokenException(); + } + + if ($token->getExpires() !== null && $token->getExpires() < $this->time->getTime()) { + throw new ExpiredTokenException($token); + } + + return $token; + } + + public function getTokenById(int $tokenId): IToken { + try { + $token = $this->mapper->getTokenById($tokenId); + } catch (DoesNotExistException $ex) { + throw new InvalidTokenException(); + } + + if ($token->getExpires() !== null && $token->getExpires() < $this->time->getTime()) { + throw new ExpiredTokenException($token); + } + + return $token; + } + + public function renewSessionToken(string $oldSessionId, string $sessionId) { + $token = $this->getToken($oldSessionId); + + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + + $password = null; + if (!is_null($token->getPassword())) { + $privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId); + $password = $this->decryptPassword($token->getPassword(), $privateKey); + } + + $this->generateToken( + $sessionId, + $token->getUID(), + $token->getLoginName(), + $password, + $token->getName(), + IToken::TEMPORARY_TOKEN, + $token->getRemember() + ); + + $this->mapper->delete($token); + } + + public function invalidateToken(string $token) { + $this->mapper->invalidate($this->hashToken($token)); + } + + public function invalidateTokenById(string $uid, int $id) { + $this->mapper->deleteById($uid, $id); + } + + public function invalidateOldTokens() { + $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); + $rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('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); + } + + public function updateToken(IToken $token) { + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + $this->mapper->update($token); + } + + public function updateTokenActivity(IToken $token) { + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + /** @var DefaultToken $token */ + $now = $this->time->getTime(); + if ($token->getLastActivity() < ($now - 60)) { + // Update token only once per minute + $token->setLastActivity($now); + $this->mapper->update($token); + } + } + + public function getTokenByUser(string $uid): array { + return $this->mapper->getTokenByUser($uid); + } + + public function getPassword(IToken $token, string $tokenId): string { + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + + if ($token->getPassword() === null) { + throw new PasswordlessTokenException(); + } + + // Decrypt private key with tokenId + $privateKey = $this->decrypt($token->getPrivateKey(), $tokenId); + + // Decrypt password with private key + return $this->decryptPassword($token->getPassword(), $privateKey); + } + + public function setPassword(IToken $token, string $tokenId, string $password) { + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + + // 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); + } + } + + public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken { + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + + // Decrypt private key with oldTokenId + $privateKey = $this->decrypt($token->getPrivateKey(), $oldTokenId); + // Encrypt with the new token + $token->setPrivateKey($this->encrypt($privateKey, $newTokenId)); + + $token->setToken($this->hashToken($newTokenId)); + $this->updateToken($token); + + return $token; + } + + private function encrypt(string $plaintext, string $token): string { + $secret = $this->config->getSystemValue('secret'); + return $this->crypto->encrypt($plaintext, $token . $secret); + } + + /** + * @throws InvalidTokenException + */ + private function decrypt(string $cipherText, string $token): string { + $secret = $this->config->getSystemValue('secret'); + try { + return $this->crypto->decrypt($cipherText, $token . $secret); + } catch (\Exception $ex) { + // Delete the invalid token + $this->invalidateToken($token); + throw new InvalidTokenException(); + } + } + + private function encryptPassword(string $password, string $publicKey): string { + openssl_public_encrypt($password, $encryptedPassword, $publicKey, OPENSSL_PKCS1_OAEP_PADDING); + $encryptedPassword = base64_encode($encryptedPassword); + + return $encryptedPassword; + } + + private function decryptPassword(string $encryptedPassword, string $privateKey): string { + $encryptedPassword = base64_decode($encryptedPassword); + openssl_private_decrypt($encryptedPassword, $password, $privateKey, OPENSSL_PKCS1_OAEP_PADDING); + + return $password; + } + + private function hashToken(string $token): string { + $secret = $this->config->getSystemValue('secret'); + return hash('sha512', $token . $secret); + } + + /** + * Convert a DefaultToken to a publicKeyToken + * This will also be updated directly in the Database + */ + public function convertToken(DefaultToken $defaultToken, string $token, $password): PublicKeyToken { + $pkToken = $this->newToken( + $token, + $defaultToken->getUID(), + $defaultToken->getLoginName(), + $password, + $defaultToken->getName(), + $defaultToken->getType(), + $defaultToken->getRemember() + ); + + $pkToken->setExpires($defaultToken->getExpires()); + $pkToken->setId($defaultToken->getId()); + + return $this->mapper->update($pkToken); + } + + private function newToken(string $token, + string $uid, + string $loginName, + $password, + string $name, + int $type, + int $remember): PublicKeyToken { + $dbToken = new PublicKeyToken(); + $dbToken->setUid($uid); + $dbToken->setLoginName($loginName); + + $config = [ + 'digest_alg' => 'sha512', + 'private_key_bits' => 2048, + ]; + + // Generate new key + $res = openssl_pkey_new($config); + openssl_pkey_export($res, $privateKey); + + // Extract the public key from $res to $pubKey + $publicKey = openssl_pkey_get_details($res); + $publicKey = $publicKey['key']; + + $dbToken->setPublicKey($publicKey); + $dbToken->setPrivateKey($this->encrypt($privateKey, $token)); + + if (!is_null($password)) { + $dbToken->setPassword($this->encryptPassword($password, $publicKey)); + } + + $dbToken->setName($name); + $dbToken->setToken($this->hashToken($token)); + $dbToken->setType($type); + $dbToken->setRemember($remember); + $dbToken->setLastActivity($this->time->getTime()); + $dbToken->setLastCheck($this->time->getTime()); + $dbToken->setVersion(PublicKeyToken::VERSION); + + return $dbToken; + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index d1818c287e1..31f088ea718 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -339,15 +339,7 @@ class Server extends ServerContainer implements IServerContainer { $dbConnection = $c->getDatabaseConnection(); return new Authentication\Token\DefaultTokenMapper($dbConnection); }); - $this->registerService(Authentication\Token\DefaultTokenProvider::class, function (Server $c) { - $mapper = $c->query(Authentication\Token\DefaultTokenMapper::class); - $crypto = $c->getCrypto(); - $config = $c->getConfig(); - $logger = $c->getLogger(); - $timeFactory = new TimeFactory(); - return new \OC\Authentication\Token\DefaultTokenProvider($mapper, $crypto, $config, $logger, $timeFactory); - }); - $this->registerAlias(IProvider::class, Authentication\Token\DefaultTokenProvider::class); + $this->registerAlias(IProvider::class, Authentication\Token\Manager::class); $this->registerService(\OCP\IUserSession::class, function (Server $c) { $manager = $c->getUserManager(); |