From 3dd5f3d5f659c391df3d5641b22cf28dc09f2ac8 Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Tue, 15 May 2018 20:03:35 +0200 Subject: [PATCH 01/13] Abstract the Provider via a manager Signed-off-by: Roeland Jago Douma --- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + lib/private/Authentication/Token/Manager.php | 210 +++++++++++++++++++ lib/private/Server.php | 10 +- 4 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 lib/private/Authentication/Token/Manager.php diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 1d319026175..9b51d5fc3e3 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -421,6 +421,7 @@ 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\\TwoFactorAuth\\Manager' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/Manager.php', 'OC\\Avatar' => $baseDir . '/lib/private/Avatar.php', 'OC\\AvatarManager' => $baseDir . '/lib/private/AvatarManager.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index b66a7b18192..546a69b13eb 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -451,6 +451,7 @@ 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\\TwoFactorAuth\\Manager' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/Manager.php', 'OC\\Avatar' => __DIR__ . '/../../..' . '/lib/private/Avatar.php', 'OC\\AvatarManager' => __DIR__ . '/../../..' . '/lib/private/AvatarManager.php', diff --git a/lib/private/Authentication/Token/Manager.php b/lib/private/Authentication/Token/Manager.php new file mode 100644 index 00000000000..4465c288c8c --- /dev/null +++ b/lib/private/Authentication/Token/Manager.php @@ -0,0 +1,210 @@ + + * + * @author Roeland Jago Douma + * + * @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 + * + */ + +namespace OC\Authentication\Token; + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Exceptions\PasswordlessTokenException; +use OCP\IUser; + +class Manager implements IProvider { + + /** @var DefaultTokenProvider */ + private $defaultTokenProvider; + + public function __construct(DefaultTokenProvider $defaultTokenProvider) { + $this->defaultTokenProvider = $defaultTokenProvider; + } + + /** + * 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 { + //TODO switch to new token by default once it is there + return $this->defaultTokenProvider->generateToken( + $token, + $uid, + $loginName, + $password, + $name, + $type, + $remember + ); + } + + /** + * Save the updated token + * + * @param IToken $token + * @throws InvalidTokenException + */ + public function updateToken(IToken $token) { + if ($token instanceof DefaultToken) { + $this->defaultTokenProvider->updateToken($token); + } + + throw new InvalidTokenException(); + } + + /** + * Update token activity timestamp + * + * @throws InvalidTokenException + * @param IToken $token + */ + public function updateTokenActivity(IToken $token) { + if ($token instanceof DefaultToken) { + $this->defaultTokenProvider->updateTokenActivity($token); + } + + throw new InvalidTokenException(); + } + + /** + * 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->defaultTokenProvider->getTokenByUser($user); + } + + /** + * Get a token by token + * + * @param string $tokenId + * @throws InvalidTokenException + * @return IToken + */ + public function getToken(string $tokenId): IToken { + // TODO: first try new token then old token + return $this->defaultTokenProvider->getToken($tokenId); + } + + /** + * Get a token by token id + * + * @param int $tokenId + * @throws InvalidTokenException + * @return IToken + */ + public function getTokenById(int $tokenId): IToken { + // TODO: Find a way to distinguis between tokens + return $this->defaultTokenProvider->getTokenById($tokenId); + } + + /** + * @param string $oldSessionId + * @param string $sessionId + * @throws InvalidTokenException + */ + public function renewSessionToken(string $oldSessionId, string $sessionId) { + // TODO: first try new then old + // TODO: if old move to new token type + $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 { + //TODO convert to new token type + if ($savedToken instanceof DefaultToken) { + return $this->defaultTokenProvider->getPassword($savedToken, $tokenId); + } + } + + /** + * Encrypt and set the password of the given token + * + * @param IToken $token + * @param string $tokenId + * @param string $password + * @throws InvalidTokenException + */ + public function setPassword(IToken $token, string $tokenId, string $password) { + //TODO conver to new token + if ($token instanceof DefaultToken) { + $this->defaultTokenProvider->setPassword($token, $tokenId, $password); + } + } + + /** + * Invalidate (delete) the given session token + * + * @param string $token + */ + public function invalidateToken(string $token) { + // TODO: check both providers + $this->defaultTokenProvider->invalidateToken($token); + } + + /** + * Invalidate (delete) the given token + * + * @param IUser $user + * @param int $id + */ + public function invalidateTokenById(IUser $user, int $id) { + //TODO find way to distinguis between tokens + $this->defaultTokenProvider->invalidateTokenById($user, $id); + } + + /** + * Invalidate (delete) old session tokens + */ + public function invalidateOldTokens() { + //Call on both + $this->defaultTokenProvider->invalidateOldTokens(); + } + + public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken { + // Migrate to new token + return $this->defaultTokenProvider->rotate($token, $oldTokenId, $newTokenId); + } + + +} 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(); From 8eec3a9c9a414ba73785e484cc0d524dfecf8e47 Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Tue, 15 May 2018 21:50:05 +0200 Subject: [PATCH 02/13] Add new authtoken v2 columns to the authtoken table Signed-off-by: Roeland Jago Douma --- .../Version14000Date20180518120534.php | 54 +++++++++++++++++++ lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + version.php | 2 +- 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 core/Migrations/Version14000Date20180518120534.php diff --git a/core/Migrations/Version14000Date20180518120534.php b/core/Migrations/Version14000Date20180518120534.php new file mode 100644 index 00000000000..a738c6baa7e --- /dev/null +++ b/core/Migrations/Version14000Date20180518120534.php @@ -0,0 +1,54 @@ + + * + * @author Roeland Jago Douma + * + * @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 . + * + */ + +namespace OC\Core\Migrations; + +use OCP\DB\ISchemaWrapper; +use OCP\Migration\SimpleMigrationStep; +use OCP\Migration\IOutput; + +class Version14000Date20180518120534 extends SimpleMigrationStep { + + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->getTable('authtoken'); + $table->addColumn('private_key', 'text', [ + 'notnull' => false, + ]); + $table->addColumn('public_key', 'text', [ + 'notnull' => false, + ]); + $table->addColumn('version', 'smallint', [ + 'notnull' => true, + 'default' => 1, + 'unsigned' => true, + ]); + $table->addIndex(['uid'], 'authtoken_uid_index'); + $table->addIndex(['version'], 'authtoken_version_index'); + + return $schema; + } +} diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 9b51d5fc3e3..43e2b75aa6d 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -571,6 +571,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 546a69b13eb..34d9f3a73e1 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -601,6 +601,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/version.php b/version.php index a7b0fdbcc67..0a9aee66adc 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel // when updating major/minor version number. -$OC_Version = array(14, 0, 0, 4); +$OC_Version = array(14, 0, 0, 5); // The human readable string $OC_VersionString = '14.0.0 alpha'; From 02e0af12871ade04c8dc2cc06d683fcb67fa5363 Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Fri, 18 May 2018 19:48:08 +0200 Subject: [PATCH 03/13] Initial PKT implementation Signed-off-by: Roeland Jago Douma --- lib/composer/composer/autoload_classmap.php | 3 + lib/composer/composer/autoload_static.php | 3 + .../Authentication/Token/DefaultToken.php | 7 + .../Token/DefaultTokenMapper.php | 20 +- lib/private/Authentication/Token/Manager.php | 8 +- .../Authentication/Token/PublicKeyToken.php | 216 ++++++++++++++ .../Token/PublicKeyTokenMapper.php | 167 +++++++++++ .../Token/PublicKeyTokenProvider.php | 265 ++++++++++++++++++ 8 files changed, 678 insertions(+), 11 deletions(-) create mode 100644 lib/private/Authentication/Token/PublicKeyToken.php create mode 100644 lib/private/Authentication/Token/PublicKeyTokenMapper.php create mode 100644 lib/private/Authentication/Token/PublicKeyTokenProvider.php diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 43e2b75aa6d..77729886601 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -422,6 +422,9 @@ return array( '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', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 34d9f3a73e1..be9c71d8246 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -452,6 +452,9 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c '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', diff --git a/lib/private/Authentication/Token/DefaultToken.php b/lib/private/Authentication/Token/DefaultToken.php index 67aa89ea66b..29c4b5a74ad 100644 --- a/lib/private/Authentication/Token/DefaultToken.php +++ b/lib/private/Authentication/Token/DefaultToken.php @@ -37,6 +37,7 @@ 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 { @@ -73,6 +74,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 +89,9 @@ class DefaultToken extends Entity implements IToken { $this->addType('lastCheck', 'int'); $this->addType('scope', 'string'); $this->addType('expires', 'int'); + $this->addType('version', 'int'); + + $this->setVersion(1); } public function getId(): int { diff --git a/lib/private/Authentication/Token/DefaultTokenMapper.php b/lib/private/Authentication/Token/DefaultTokenMapper.php index a67d7d151e9..0a3f32ffb46 100644 --- a/lib/private/Authentication/Token/DefaultTokenMapper.php +++ b/lib/private/Authentication/Token/DefaultTokenMapper.php @@ -50,8 +50,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(1, IQueryBuilder::PARAM_INT))) ->execute(); } @@ -66,6 +66,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(1, IQueryBuilder::PARAM_INT))) ->execute(); } @@ -79,9 +80,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(1, IQueryBuilder::PARAM_INT))) ->execute(); $data = $result->fetch(); @@ -102,9 +104,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(1, IQueryBuilder::PARAM_INT))) ->execute(); $data = $result->fetch(); @@ -127,9 +130,10 @@ class DefaultTokenMapper extends QBMapper { public function getTokenByUser(IUser $user): 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()))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))) ->setMaxResults(1000); $result = $qb->execute(); $data = $result->fetchAll(); @@ -151,7 +155,8 @@ class DefaultTokenMapper extends QBMapper { $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($user->getUID()))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))); $qb->execute(); } @@ -163,7 +168,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(1, IQueryBuilder::PARAM_INT))); $qb->execute(); } diff --git a/lib/private/Authentication/Token/Manager.php b/lib/private/Authentication/Token/Manager.php index 4465c288c8c..85fe91cdf14 100644 --- a/lib/private/Authentication/Token/Manager.php +++ b/lib/private/Authentication/Token/Manager.php @@ -76,9 +76,9 @@ class Manager implements IProvider { public function updateToken(IToken $token) { if ($token instanceof DefaultToken) { $this->defaultTokenProvider->updateToken($token); + } else { + throw new InvalidTokenException(); } - - throw new InvalidTokenException(); } /** @@ -90,9 +90,9 @@ class Manager implements IProvider { public function updateTokenActivity(IToken $token) { if ($token instanceof DefaultToken) { $this->defaultTokenProvider->updateTokenActivity($token); + } else { + throw new InvalidTokenException(); } - - 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..18b27075772 --- /dev/null +++ b/lib/private/Authentication/Token/PublicKeyToken.php @@ -0,0 +1,216 @@ + + * + * @author Roeland Jago Douma + * + * @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 . + * + */ + +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 { + + /** @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'); + + $this->setVersion(2); + } + + 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..0d5657cb582 --- /dev/null +++ b/lib/private/Authentication/Token/PublicKeyTokenMapper.php @@ -0,0 +1,167 @@ + + * + * @author Roeland Jago Douma + * + * @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 . + * + */ + +namespace OC\Authentication\Token; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\IUser; + +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(2, 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(2, 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(2, 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(2, 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 IUser $user + * @return DefaultToken[] + */ + public function getTokenByUser(IUser $user): array { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('authtoken') + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($user->getUID()))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(2, 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; + } + + /** + * @param IUser $user + * @param int $id + */ + public function deleteById(IUser $user, 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('version', $qb->createNamedParameter(2, 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(2, 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..d7e9038a076 --- /dev/null +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -0,0 +1,265 @@ + + * + * @author Roeland Jago Douma + * + * @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 + * + */ + +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\IUser; +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 = 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()); + + $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); + + $password = null; + if (!is_null($token->getPassword())) { + $password = $this->decryptPassword($token->getPassword(), $oldSessionId); + } + + $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(IUser $user, int $id) { + $this->mapper->deleteById($user, $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(IUser $user): array { + return $this->mapper->getTokenByUser($user); + } + + public function getPassword(IToken $token, string $tokenId): string { + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + + // 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) { + // Kill all temp tokens except the current token + + // Update pass for all permanent tokens by rencrypting + } + + 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); + + return $encryptedPassword; + } + + private function decryptPassword(string $encryptedPassword, string $privateKey): string { + 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); + } +} From 1f17010e0b4099b41cc72f53e18f4d162ce2e3da Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Tue, 29 May 2018 09:24:20 +0200 Subject: [PATCH 04/13] Add first tests Signed-off-by: Roeland Jago Douma --- .../Token/PublicKeyTokenProvider.php | 11 +- .../Token/PublicKeyTokenMapperTest.php | 266 ++++++++++ .../Token/PublicKeyTokenProviderTest.php | 465 ++++++++++++++++++ .../Token/PublicKeyTokenTest.php | 44 ++ 4 files changed, 785 insertions(+), 1 deletion(-) create mode 100644 tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php create mode 100644 tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php create mode 100644 tests/lib/Authentication/Token/PublicKeyTokenTest.php diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index d7e9038a076..1c5f3da147f 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -134,9 +134,14 @@ class PublicKeyTokenProvider implements IProvider { 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())) { - $password = $this->decryptPassword($token->getPassword(), $oldSessionId); + $privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId); + $password = $this->decryptPassword($token->getPassword(), $privateKey); } $this->generateToken( @@ -198,6 +203,10 @@ class PublicKeyTokenProvider implements IProvider { throw new InvalidTokenException(); } + if ($token->getPassword() === null) { + throw new PasswordlessTokenException(); + } + // Decrypt private key with tokenId $privateKey = $this->decrypt($token->getPrivateKey(), $tokenId); diff --git a/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php b/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php new file mode 100644 index 00000000000..7c4dbabad6c --- /dev/null +++ b/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php @@ -0,0 +1,266 @@ + + * + * @author Roeland Jago Douma + * + * @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 . + * + */ + +namespace Test\Authentication\Token; + +use OC; +use OC\Authentication\Token\PublicKeyToken; +use OC\Authentication\Token\PublicKeyTokenMapper; +use OC\Authentication\Token\IToken; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\IUser; +use Test\TestCase; + +/** + * @group DB + */ +class PublicKeyTokenMapperTest extends TestCase { + + /** @var PublicKeyTokenMapper */ + private $mapper; + + /** @var IDBConnection */ + private $dbConnection; + + /** @var int */ + private $time; + + protected function setUp() { + parent::setUp(); + + $this->dbConnection = OC::$server->getDatabaseConnection(); + $this->time = time(); + $this->resetDatabase(); + + $this->mapper = new PublicKeyTokenMapper($this->dbConnection); + } + + private function resetDatabase() { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->delete('authtoken')->execute(); + $qb->insert('authtoken')->values([ + 'uid' => $qb->createNamedParameter('user1'), + 'login_name' => $qb->createNamedParameter('User1'), + 'password' => $qb->createNamedParameter('a75c7116460c082912d8f6860a850904|3nz5qbG1nNSLLi6V|c55365a0e54cfdfac4a175bcf11a7612aea74492277bba6e5d96a24497fa9272488787cb2f3ad34d8b9b8060934fce02f008d371df3ff3848f4aa61944851ff0'), + 'name' => $qb->createNamedParameter('Firefox on Linux'), + 'token' => $qb->createNamedParameter('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'), + 'type' => $qb->createNamedParameter(IToken::TEMPORARY_TOKEN), + 'last_activity' => $qb->createNamedParameter($this->time - 120, IQueryBuilder::PARAM_INT), // Two minutes ago + 'last_check' => $this->time - 60 * 10, // 10mins ago + 'public_key' => $qb->createNamedParameter('public key'), + 'private_key' => $qb->createNamedParameter('private key'), + 'version' => $qb->createNamedParameter(2), + ])->execute(); + $qb->insert('authtoken')->values([ + 'uid' => $qb->createNamedParameter('user2'), + 'login_name' => $qb->createNamedParameter('User2'), + 'password' => $qb->createNamedParameter('971a337057853344700bbeccf836519f|UwOQwyb34sJHtqPV|036d4890f8c21d17bbc7b88072d8ef049a5c832a38e97f3e3d5f9186e896c2593aee16883f617322fa242728d0236ff32d163caeb4bd45e14ca002c57a88665f'), + 'name' => $qb->createNamedParameter('Firefox on Android'), + 'token' => $qb->createNamedParameter('1504445f1524fc801035448a95681a9378ba2e83930c814546c56e5d6ebde221198792fd900c88ed5ead0555780dad1ebce3370d7e154941cd5de87eb419899b'), + 'type' => $qb->createNamedParameter(IToken::TEMPORARY_TOKEN), + 'last_activity' => $qb->createNamedParameter($this->time - 60 * 60 * 24 * 3, IQueryBuilder::PARAM_INT), // Three days ago + 'last_check' => $this->time - 10, // 10secs ago + 'public_key' => $qb->createNamedParameter('public key'), + 'private_key' => $qb->createNamedParameter('private key'), + 'version' => $qb->createNamedParameter(2), + ])->execute(); + $qb->insert('authtoken')->values([ + 'uid' => $qb->createNamedParameter('user1'), + 'login_name' => $qb->createNamedParameter('User1'), + 'password' => $qb->createNamedParameter('063de945d6f6b26862d9b6f40652f2d5|DZ/z520tfdXPtd0T|395f6b89be8d9d605e409e20b9d9abe477fde1be38a3223f9e508f979bf906e50d9eaa4dca983ca4fb22a241eb696c3f98654e7775f78c4caf13108f98642b53'), + 'name' => $qb->createNamedParameter('Iceweasel on Linux'), + 'token' => $qb->createNamedParameter('47af8697ba590fb82579b5f1b3b6e8066773a62100abbe0db09a289a62f5d980dc300fa3d98b01d7228468d1ab05c1aa14c8d14bd5b6eee9cdf1ac14864680c3'), + 'type' => $qb->createNamedParameter(IToken::TEMPORARY_TOKEN), + 'last_activity' => $qb->createNamedParameter($this->time - 120, IQueryBuilder::PARAM_INT), // Two minutes ago + 'last_check' => $this->time - 60 * 10, // 10mins ago + 'public_key' => $qb->createNamedParameter('public key'), + 'private_key' => $qb->createNamedParameter('private key'), + 'version' => $qb->createNamedParameter(2), + ])->execute(); + } + + private function getNumberOfTokens() { + $qb = $this->dbConnection->getQueryBuilder(); + $result = $qb->select($qb->createFunction('count(*) as `count`')) + ->from('authtoken') + ->execute() + ->fetch(); + return (int) $result['count']; + } + + public function testInvalidate() { + $token = '9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'; + + $this->mapper->invalidate($token); + + $this->assertSame(2, $this->getNumberOfTokens()); + } + + public function testInvalidateInvalid() { + $token = 'youwontfindthisoneinthedatabase'; + + $this->mapper->invalidate($token); + + $this->assertSame(3, $this->getNumberOfTokens()); + } + + public function testInvalidateOld() { + $olderThan = $this->time - 60 * 60; // One hour + + $this->mapper->invalidateOld($olderThan); + + $this->assertSame(2, $this->getNumberOfTokens()); + } + + public function testGetToken() { + $token = new PublicKeyToken(); + $token->setUid('user2'); + $token->setLoginName('User2'); + $token->setPassword('971a337057853344700bbeccf836519f|UwOQwyb34sJHtqPV|036d4890f8c21d17bbc7b88072d8ef049a5c832a38e97f3e3d5f9186e896c2593aee16883f617322fa242728d0236ff32d163caeb4bd45e14ca002c57a88665f'); + $token->setName('Firefox on Android'); + $token->setToken('1504445f1524fc801035448a95681a9378ba2e83930c814546c56e5d6ebde221198792fd900c88ed5ead0555780dad1ebce3370d7e154941cd5de87eb419899b'); + $token->setType(IToken::TEMPORARY_TOKEN); + $token->setRemember(IToken::DO_NOT_REMEMBER); + $token->setLastActivity($this->time - 60 * 60 * 24 * 3); + $token->setLastCheck($this->time - 10); + $token->setPublicKey('public key'); + $token->setPrivateKey('private key'); + + $dbToken = $this->mapper->getToken($token->getToken()); + + $token->setId($dbToken->getId()); // We don't know the ID + $token->resetUpdatedFields(); + + $this->assertEquals($token, $dbToken); + } + + /** + * @expectedException \OCP\AppFramework\Db\DoesNotExistException + */ + public function testGetInvalidToken() { + $token = 'thisisaninvalidtokenthatisnotinthedatabase'; + + $this->mapper->getToken($token); + } + + public function testGetTokenById() { + $token = new PublicKeyToken(); + $token->setUid('user2'); + $token->setLoginName('User2'); + $token->setPassword('971a337057853344700bbeccf836519f|UwOQwyb34sJHtqPV|036d4890f8c21d17bbc7b88072d8ef049a5c832a38e97f3e3d5f9186e896c2593aee16883f617322fa242728d0236ff32d163caeb4bd45e14ca002c57a88665f'); + $token->setName('Firefox on Android'); + $token->setToken('1504445f1524fc801035448a95681a9378ba2e83930c814546c56e5d6ebde221198792fd900c88ed5ead0555780dad1ebce3370d7e154941cd5de87eb419899b'); + $token->setType(IToken::TEMPORARY_TOKEN); + $token->setRemember(IToken::DO_NOT_REMEMBER); + $token->setLastActivity($this->time - 60 * 60 * 24 * 3); + $token->setLastCheck($this->time - 10); + $token->setPublicKey('public key'); + $token->setPrivateKey('private key'); + + $dbToken = $this->mapper->getToken($token->getToken()); + $token->setId($dbToken->getId()); // We don't know the ID + $token->resetUpdatedFields(); + + $dbToken = $this->mapper->getTokenById($token->getId()); + $this->assertEquals($token, $dbToken); + } + + /** + * @expectedException \OCP\AppFramework\Db\DoesNotExistException + */ + public function testGetTokenByIdNotFound() { + $this->mapper->getTokenById(-1); + } + + /** + * @expectedException \OCP\AppFramework\Db\DoesNotExistException + */ + public function testGetInvalidTokenById() { + $id = '42'; + + $this->mapper->getToken($id); + } + + public function testGetTokenByUser() { + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->createMock(IUser::class); + $user->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('user1')); + + $this->assertCount(2, $this->mapper->getTokenByUser($user)); + } + + public function testGetTokenByUserNotFound() { + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->createMock(IUser::class); + $user->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('user1000')); + + $this->assertCount(0, $this->mapper->getTokenByUser($user)); + } + + public function testDeleteById() { + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->createMock(IUser::class); + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('id') + ->from('authtoken') + ->where($qb->expr()->eq('token', $qb->createNamedParameter('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'))); + $result = $qb->execute(); + $id = $result->fetch()['id']; + $user->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('user1')); + + $this->mapper->deleteById($user, (int)$id); + $this->assertEquals(2, $this->getNumberOfTokens()); + } + + public function testDeleteByIdWrongUser() { + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->createMock(IUser::class); + $id = 33; + $user->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('user10000')); + + $this->mapper->deleteById($user, $id); + $this->assertEquals(3, $this->getNumberOfTokens()); + } + + public function testDeleteByName() { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('name') + ->from('authtoken') + ->where($qb->expr()->eq('token', $qb->createNamedParameter('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'))); + $result = $qb->execute(); + $name = $result->fetch()['name']; + $this->mapper->deleteByName($name); + $this->assertEquals(2, $this->getNumberOfTokens()); + } + +} diff --git a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php new file mode 100644 index 00000000000..4901001db99 --- /dev/null +++ b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php @@ -0,0 +1,465 @@ + + * + * @author Roeland Jago Douma + * + * @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 . + * + */ + +namespace Test\Authentication\Token; + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Exceptions\PasswordlessTokenException; +use OC\Authentication\Token\PublicKeyToken; +use OC\Authentication\Token\PublicKeyTokenMapper; +use OC\Authentication\Token\PublicKeyTokenProvider; +use OC\Authentication\Token\ExpiredTokenException; +use OC\Authentication\Token\IToken; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\ILogger; +use OCP\IUser; +use OCP\Security\ICrypto; +use Test\TestCase; + +class PublicKeyTokenProviderTest extends TestCase { + + /** @var PublicKeyTokenProvider|\PHPUnit_Framework_MockObject_MockObject */ + private $tokenProvider; + /** @var PublicKeyTokenMapper|\PHPUnit_Framework_MockObject_MockObject */ + private $mapper; + /** @var ICrypto */ + private $crypto; + /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */ + private $config; + /** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */ + private $logger; + /** @var ITimeFactory|\PHPUnit_Framework_MockObject_MockObject */ + private $timeFactory; + /** @var int */ + private $time; + + protected function setUp() { + parent::setUp(); + + $this->mapper = $this->createMock(PublicKeyTokenMapper::class); + $this->crypto = \OC::$server->getCrypto(); + $this->config = $this->createMock(IConfig::class); + $this->config->method('getSystemValue') + ->will($this->returnValueMap([ + ['session_lifetime', 60 * 60 * 24, 150], + ['remember_login_cookie_lifetime', 60 * 60 * 24 * 15, 300], + ['secret', '', '1f4h9s'], + ])); + $this->logger = $this->createMock(ILogger::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->time = 1313131; + $this->timeFactory->method('getTime') + ->willReturn($this->time); + + $this->tokenProvider = new PublicKeyTokenProvider($this->mapper, $this->crypto, $this->config, $this->logger, + $this->timeFactory); + } + + public function testGenerateToken() { + $token = 'token'; + $uid = 'user'; + $user = 'User'; + $password = 'passme'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $this->assertInstanceOf(PublicKeyToken::class, $actual); + $this->assertSame($uid, $actual->getUID()); + $this->assertSame($user, $actual->getLoginName()); + $this->assertSame($name, $actual->getName()); + $this->assertSame(IToken::DO_NOT_REMEMBER, $actual->getRemember()); + $this->assertSame($password, $this->tokenProvider->getPassword($actual, $token)); + } + + public function testUpdateToken() { + $tk = new PublicKeyToken(); + $tk->setLastActivity($this->time - 200); + $this->mapper->expects($this->once()) + ->method('update') + ->with($tk); + + $this->tokenProvider->updateTokenActivity($tk); + + $this->assertEquals($this->time, $tk->getLastActivity()); + } + + public function testUpdateTokenDebounce() { + $tk = new PublicKeyToken(); + $tk->setLastActivity($this->time - 30); + $this->mapper->expects($this->never()) + ->method('update') + ->with($tk); + + $this->tokenProvider->updateTokenActivity($tk); + } + + public function testGetTokenByUser() { + $user = $this->createMock(IUser::class); + $this->mapper->expects($this->once()) + ->method('getTokenByUser') + ->with($user) + ->will($this->returnValue(['token'])); + + $this->assertEquals(['token'], $this->tokenProvider->getTokenByUser($user)); + } + + public function testGetPassword() { + $token = 'token'; + $uid = 'user'; + $user = 'User'; + $password = 'passme'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $this->assertSame($password, $this->tokenProvider->getPassword($actual, $token)); + } + + /** + * @expectedException \OC\Authentication\Exceptions\PasswordlessTokenException + */ + public function testGetPasswordPasswordLessToken() { + $token = 'token1234'; + $tk = new PublicKeyToken(); + $tk->setPassword(null); + + $this->tokenProvider->getPassword($tk, $token); + } + + /** + * @expectedException \OC\Authentication\Exceptions\InvalidTokenException + */ + public function testGetPasswordInvalidToken() { + $token = 'token'; + $uid = 'user'; + $user = 'User'; + $password = 'passme'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $this->tokenProvider->getPassword($actual, 'wrongtoken'); + } + + public function testSetPassword() { + $token = 'token'; + $uid = 'user'; + $user = 'User'; + $password = 'passme'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $newpass = 'newpass'; + $this->tokenProvider->setPassword($actual, $token, $newpass); + + $this->assertSame($newpass, $this->tokenProvider->getPassword($actual, 'token')); + } + + /** + * @expectedException \OC\Authentication\Exceptions\InvalidTokenException + */ + public function testSetPasswordInvalidToken() { + $token = $this->createMock(IToken::class); + $tokenId = 'token123'; + $password = '123456'; + + $this->tokenProvider->setPassword($token, $tokenId, $password); + } + + public function testInvalidateToken() { + $this->mapper->expects($this->once()) + ->method('invalidate') + ->with(hash('sha512', 'token7'.'1f4h9s')); + + $this->tokenProvider->invalidateToken('token7'); + } + + public function testInvaildateTokenById() { + $id = 123; + $user = $this->createMock(IUser::class); + + $this->mapper->expects($this->once()) + ->method('deleteById') + ->with($user, $id); + + $this->tokenProvider->invalidateTokenById($user, $id); + } + + public function testInvalidateOldTokens() { + $defaultSessionLifetime = 60 * 60 * 24; + $defaultRememberMeLifetime = 60 * 60 * 24 * 15; + $this->config->expects($this->exactly(2)) + ->method('getSystemValue') + ->will($this->returnValueMap([ + ['session_lifetime', $defaultSessionLifetime, 150], + ['remember_login_cookie_lifetime', $defaultRememberMeLifetime, 300], + ])); + $this->mapper->expects($this->at(0)) + ->method('invalidateOld') + ->with($this->time - 150); + $this->mapper->expects($this->at(1)) + ->method('invalidateOld') + ->with($this->time - 300); + + $this->tokenProvider->invalidateOldTokens(); + } + + public function testRenewSessionTokenWithoutPassword() { + $token = 'oldId'; + $uid = 'user'; + $user = 'User'; + $password = null; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $oldToken = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $this->mapper + ->expects($this->at(0)) + ->method('getToken') + ->with(hash('sha512', 'oldId' . '1f4h9s')) + ->willReturn($oldToken); + $this->mapper + ->expects($this->at(1)) + ->method('insert') + ->with($this->callback(function (PublicKeyToken $token) use ($user, $uid, $name) { + return $token->getUID() === $uid && + $token->getLoginName() === $user && + $token->getName() === $name && + $token->getType() === IToken::DO_NOT_REMEMBER && + $token->getLastActivity() === $this->time && + $token->getPassword() === null; + })); + $this->mapper + ->expects($this->at(2)) + ->method('delete') + ->with($this->callback(function($token) use ($oldToken) { + return $token === $oldToken; + })); + + $this->tokenProvider->renewSessionToken('oldId', 'newId'); + } + + public function testRenewSessionTokenWithPassword() { + $token = 'oldId'; + $uid = 'user'; + $user = 'User'; + $password = 'password'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $oldToken = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $this->mapper + ->expects($this->at(0)) + ->method('getToken') + ->with(hash('sha512', 'oldId' . '1f4h9s')) + ->willReturn($oldToken); + $this->mapper + ->expects($this->at(1)) + ->method('insert') + ->with($this->callback(function (PublicKeyToken $token) use ($user, $uid, $name) { + return $token->getUID() === $uid && + $token->getLoginName() === $user && + $token->getName() === $name && + $token->getType() === IToken::DO_NOT_REMEMBER && + $token->getLastActivity() === $this->time && + $token->getPassword() !== null && + $this->tokenProvider->getPassword($token, 'newId') === 'password'; + })); + $this->mapper + ->expects($this->at(2)) + ->method('delete') + ->with($this->callback(function($token) use ($oldToken) { + return $token === $oldToken; + })); + + $this->tokenProvider->renewSessionToken('oldId', 'newId'); + } + + public function testGetToken() { + $token = new PublicKeyToken(); + + $this->config->method('getSystemValue') + ->with('secret') + ->willReturn('mysecret'); + + $this->mapper->method('getToken') + ->with( + $this->callback(function (string $token) { + return hash('sha512', 'unhashedToken'.'1f4h9s') === $token; + }) + )->willReturn($token); + + $this->assertSame($token, $this->tokenProvider->getToken('unhashedToken')); + } + + public function testGetInvalidToken() { + $this->expectException(InvalidTokenException::class); + + $this->mapper->method('getToken') + ->with( + $this->callback(function (string $token) { + return hash('sha512', 'unhashedToken'.'1f4h9s') === $token; + }) + )->willThrowException(new InvalidTokenException()); + + $this->tokenProvider->getToken('unhashedToken'); + } + + public function testGetExpiredToken() { + $token = 'token'; + $uid = 'user'; + $user = 'User'; + $password = 'passme'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + $actual->setExpires(42); + + $this->mapper->method('getToken') + ->with( + $this->callback(function (string $token) { + return hash('sha512', 'token'.'1f4h9s') === $token; + }) + )->willReturn($actual); + + try { + $this->tokenProvider->getToken('token'); + $this->fail(); + } catch (ExpiredTokenException $e) { + $this->assertSame($actual, $e->getToken()); + } + + } + + public function testGetTokenById() { + $token = $this->createMock(PublicKeyToken::class); + + $this->mapper->expects($this->once()) + ->method('getTokenById') + ->with($this->equalTo(42)) + ->willReturn($token); + + $this->assertSame($token, $this->tokenProvider->getTokenById(42)); + } + + public function testGetInvalidTokenById() { + $this->expectException(InvalidTokenException::class); + + $this->mapper->expects($this->once()) + ->method('getTokenById') + ->with($this->equalTo(42)) + ->willThrowException(new DoesNotExistException('nope')); + + $this->tokenProvider->getTokenById(42); + } + + public function testGetExpiredTokenById() { + $token = new PublicKeyToken(); + $token->setExpires(42); + + $this->mapper->expects($this->once()) + ->method('getTokenById') + ->with($this->equalTo(42)) + ->willReturn($token); + + try { + $this->tokenProvider->getTokenById(42); + $this->fail(); + } catch (ExpiredTokenException $e) { + $this->assertSame($token, $e->getToken()); + } + } + + public function testRotate() { + $token = 'oldtoken'; + $uid = 'user'; + $user = 'User'; + $password = 'password'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $new = $this->tokenProvider->rotate($actual, 'oldtoken', 'newtoken'); + + $this->assertSame('password', $this->tokenProvider->getPassword($new, 'newtoken')); + } + + public function testRotateNoPassword() { + $token = 'oldtoken'; + $uid = 'user'; + $user = 'User'; + $password = null; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $oldPrivate = $actual->getPrivateKey(); + + $new = $this->tokenProvider->rotate($actual, 'oldtoken', 'newtoken'); + + $newPrivate = $new->getPrivateKey(); + + $this->assertNotSame($newPrivate, $oldPrivate); + $this->assertNull($new->getPassword()); + } +} diff --git a/tests/lib/Authentication/Token/PublicKeyTokenTest.php b/tests/lib/Authentication/Token/PublicKeyTokenTest.php new file mode 100644 index 00000000000..d0226eb9902 --- /dev/null +++ b/tests/lib/Authentication/Token/PublicKeyTokenTest.php @@ -0,0 +1,44 @@ + + * + * @author Roeland Jago Douma + * + * @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 . + * + */ + +namespace Test\Authentication\Token; + +use OC\Authentication\Token\PublicKeyToken; +use Test\TestCase; + +class PublicKeyTokenTest extends TestCase { + public function testSetScopeAsArray() { + $scope = ['filesystem' => false]; + $token = new PublicKeyToken(); + $token->setScope($scope); + $this->assertEquals(json_encode($scope), $token->getScope()); + $this->assertEquals($scope, $token->getScopeAsArray()); + } + + public function testDefaultScope() { + $scope = ['filesystem' => true]; + $token = new PublicKeyToken(); + $this->assertEquals($scope, $token->getScopeAsArray()); + } +} From 4c0d7104792cb89b8bc013c08b9c9fcb63dcf0da Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Tue, 29 May 2018 09:29:29 +0200 Subject: [PATCH 05/13] Just pass uid to the Token stuff We don't have user objects in the code everywhere Signed-off-by: Roeland Jago Douma --- .../Token/DefaultTokenMapper.php | 15 ++++------ .../Token/DefaultTokenProvider.php | 23 +++------------ .../Authentication/Token/IProvider.php | 9 +++--- lib/private/Authentication/Token/Manager.php | 8 +++--- .../Token/PublicKeyTokenMapper.php | 15 ++++------ .../Token/PublicKeyTokenProvider.php | 8 +++--- .../Controller/AuthSettingsController.php | 13 ++------- .../Controller/AuthSettingsControllerTest.php | 12 ++------ .../Token/DefaultTokenMapperTest.php | 28 +++---------------- 9 files changed, 34 insertions(+), 97 deletions(-) diff --git a/lib/private/Authentication/Token/DefaultTokenMapper.php b/lib/private/Authentication/Token/DefaultTokenMapper.php index 0a3f32ffb46..02964e3f59c 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 { @@ -124,15 +123,15 @@ 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('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(1, IQueryBuilder::PARAM_INT))) ->setMaxResults(1000); $result = $qb->execute(); @@ -146,16 +145,12 @@ 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(1, IQueryBuilder::PARAM_INT))); $qb->execute(); } diff --git a/lib/private/Authentication/Token/DefaultTokenProvider.php b/lib/private/Authentication/Token/DefaultTokenProvider.php index 7a43dbb23e1..ed3c14c1745 100644 --- a/lib/private/Authentication/Token/DefaultTokenProvider.php +++ b/lib/private/Authentication/Token/DefaultTokenProvider.php @@ -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); } /** 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 index 85fe91cdf14..7ec90e92ed0 100644 --- a/lib/private/Authentication/Token/Manager.php +++ b/lib/private/Authentication/Token/Manager.php @@ -104,8 +104,8 @@ class Manager implements IProvider { * @param IUser $user * @return IToken[] */ - public function getTokenByUser(IUser $user): array { - return $this->defaultTokenProvider->getTokenByUser($user); + public function getTokenByUser(string $uid): array { + return $this->defaultTokenProvider->getTokenByUser($uid); } /** @@ -188,9 +188,9 @@ class Manager implements IProvider { * @param IUser $user * @param int $id */ - public function invalidateTokenById(IUser $user, int $id) { + public function invalidateTokenById(string $uid, int $id) { //TODO find way to distinguis between tokens - $this->defaultTokenProvider->invalidateTokenById($user, $id); + $this->defaultTokenProvider->invalidateTokenById($uid, $id); } /** diff --git a/lib/private/Authentication/Token/PublicKeyTokenMapper.php b/lib/private/Authentication/Token/PublicKeyTokenMapper.php index 0d5657cb582..6feb176fb68 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenMapper.php +++ b/lib/private/Authentication/Token/PublicKeyTokenMapper.php @@ -28,7 +28,6 @@ use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; -use OCP\IUser; class PublicKeyTokenMapper extends QBMapper { @@ -115,15 +114,15 @@ class PublicKeyTokenMapper 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('*') ->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(2, IQueryBuilder::PARAM_INT))) ->setMaxResults(1000); $result = $qb->execute(); @@ -137,16 +136,12 @@ class PublicKeyTokenMapper 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(2, IQueryBuilder::PARAM_INT))); $qb->execute(); } diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index 1c5f3da147f..926e3c678d4 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -161,8 +161,8 @@ class PublicKeyTokenProvider implements IProvider { $this->mapper->invalidate($this->hashToken($token)); } - public function invalidateTokenById(IUser $user, int $id) { - $this->mapper->deleteById($user, $id); + public function invalidateTokenById(string $uid, int $id) { + $this->mapper->deleteById($uid, $id); } public function invalidateOldTokens() { @@ -194,8 +194,8 @@ class PublicKeyTokenProvider implements IProvider { } } - public function getTokenByUser(IUser $user): array { - return $this->mapper->getTokenByUser($user); + public function getTokenByUser(string $uid): array { + return $this->mapper->getTokenByUser($uid); } public function getPassword(IToken $token, string $tokenId): string { diff --git a/settings/Controller/AuthSettingsController.php b/settings/Controller/AuthSettingsController.php index 6eaa64cfac2..06cabd00b07 100644 --- a/settings/Controller/AuthSettingsController.php +++ b/settings/Controller/AuthSettingsController.php @@ -83,11 +83,7 @@ class AuthSettingsController extends Controller { * @return JSONResponse|array */ public function index() { - $user = $this->userManager->get($this->uid); - if ($user === null) { - return []; - } - $tokens = $this->tokenProvider->getTokenByUser($user); + $tokens = $this->tokenProvider->getTokenByUser($this->uid); try { $sessionId = $this->session->getId(); @@ -182,12 +178,7 @@ class AuthSettingsController extends Controller { * @return array */ public function destroy($id) { - $user = $this->userManager->get($this->uid); - if (is_null($user)) { - return []; - } - - $this->tokenProvider->invalidateTokenById($user, $id); + $this->tokenProvider->invalidateTokenById($this->uid, $id); return []; } diff --git a/tests/Settings/Controller/AuthSettingsControllerTest.php b/tests/Settings/Controller/AuthSettingsControllerTest.php index 461b32b7a48..1c957299e39 100644 --- a/tests/Settings/Controller/AuthSettingsControllerTest.php +++ b/tests/Settings/Controller/AuthSettingsControllerTest.php @@ -75,13 +75,9 @@ class AuthSettingsControllerTest extends TestCase { $sessionToken = new DefaultToken(); $sessionToken->setId(100); - $this->userManager->expects($this->once()) - ->method('get') - ->with($this->uid) - ->will($this->returnValue($this->user)); $this->tokenProvider->expects($this->once()) ->method('getTokenByUser') - ->with($this->user) + ->with($this->uid) ->will($this->returnValue($tokens)); $this->session->expects($this->once()) ->method('getId') @@ -192,13 +188,9 @@ class AuthSettingsControllerTest extends TestCase { $id = 123; $user = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with($this->uid) - ->will($this->returnValue($user)); $this->tokenProvider->expects($this->once()) ->method('invalidateTokenById') - ->with($user, $id); + ->with($this->uid, $id); $this->assertEquals([], $this->controller->destroy($id)); } diff --git a/tests/lib/Authentication/Token/DefaultTokenMapperTest.php b/tests/lib/Authentication/Token/DefaultTokenMapperTest.php index b5d24a7ab5e..ab09c005297 100644 --- a/tests/lib/Authentication/Token/DefaultTokenMapperTest.php +++ b/tests/lib/Authentication/Token/DefaultTokenMapperTest.php @@ -190,23 +190,11 @@ class DefaultTokenMapperTest extends TestCase { } public function testGetTokenByUser() { - /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->once()) - ->method('getUID') - ->will($this->returnValue('user1')); - - $this->assertCount(2, $this->mapper->getTokenByUser($user)); + $this->assertCount(2, $this->mapper->getTokenByUser('user1')); } public function testGetTokenByUserNotFound() { - /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->once()) - ->method('getUID') - ->will($this->returnValue('user1000')); - - $this->assertCount(0, $this->mapper->getTokenByUser($user)); + $this->assertCount(0, $this->mapper->getTokenByUser('user1000')); } public function testDeleteById() { @@ -218,23 +206,15 @@ class DefaultTokenMapperTest extends TestCase { ->where($qb->expr()->eq('token', $qb->createNamedParameter('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'))); $result = $qb->execute(); $id = $result->fetch()['id']; - $user->expects($this->once()) - ->method('getUID') - ->will($this->returnValue('user1')); - $this->mapper->deleteById($user, $id); + $this->mapper->deleteById('user1', $id); $this->assertEquals(2, $this->getNumberOfTokens()); } public function testDeleteByIdWrongUser() { - /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->createMock(IUser::class); $id = 33; - $user->expects($this->once()) - ->method('getUID') - ->will($this->returnValue('user10000')); - $this->mapper->deleteById($user, $id); + $this->mapper->deleteById('user1000', $id); $this->assertEquals(3, $this->getNumberOfTokens()); } From 4bbc21cb216c51ab22f31089c9c09a3dec8980dc Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Tue, 29 May 2018 12:18:10 +0200 Subject: [PATCH 06/13] SetPassword on PublicKeyTokens Signed-off-by: Roeland Jago Douma --- .../Token/PublicKeyTokenProvider.php | 14 +++++++++++-- .../Token/DefaultTokenProviderTest.php | 10 ++++----- .../Token/PublicKeyTokenProviderTest.php | 21 +++++++++++++------ 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index 926e3c678d4..5c97877e730 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -215,9 +215,19 @@ class PublicKeyTokenProvider implements IProvider { } public function setPassword(IToken $token, string $tokenId, string $password) { - // Kill all temp tokens except the current token + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + + // Update the password for all tokens + $tokens = $this->mapper->getTokenByUser($token->getUID()); + foreach ($tokens as $t) { + $publicKey = $token->getPublicKey(); + $t->setPassword($this->encryptPassword($password, $publicKey)); + $this->updateToken($t); + } - // Update pass for all permanent tokens by rencrypting + //TODO: should we also do this for temp tokens? } public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken { diff --git a/tests/lib/Authentication/Token/DefaultTokenProviderTest.php b/tests/lib/Authentication/Token/DefaultTokenProviderTest.php index 95b5b928559..58e152457fc 100644 --- a/tests/lib/Authentication/Token/DefaultTokenProviderTest.php +++ b/tests/lib/Authentication/Token/DefaultTokenProviderTest.php @@ -132,13 +132,12 @@ class DefaultTokenProviderTest extends TestCase { } public function testGetTokenByUser() { - $user = $this->createMock(IUser::class); $this->mapper->expects($this->once()) ->method('getTokenByUser') - ->with($user) + ->with('uid') ->will($this->returnValue(['token'])); - $this->assertEquals(['token'], $this->tokenProvider->getTokenByUser($user)); + $this->assertEquals(['token'], $this->tokenProvider->getTokenByUser('uid')); } public function testGetPassword() { @@ -243,13 +242,12 @@ class DefaultTokenProviderTest extends TestCase { public function testInvaildateTokenById() { $id = 123; - $user = $this->createMock(IUser::class); $this->mapper->expects($this->once()) ->method('deleteById') - ->with($user, $id); + ->with('uid', $id); - $this->tokenProvider->invalidateTokenById($user, $id); + $this->tokenProvider->invalidateTokenById('uid', $id); } public function testInvalidateOldTokens() { diff --git a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php index 4901001db99..d5cfe5d1ee6 100644 --- a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php +++ b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php @@ -121,13 +121,12 @@ class PublicKeyTokenProviderTest extends TestCase { } public function testGetTokenByUser() { - $user = $this->createMock(IUser::class); $this->mapper->expects($this->once()) ->method('getTokenByUser') - ->with($user) + ->with('uid') ->will($this->returnValue(['token'])); - $this->assertEquals(['token'], $this->tokenProvider->getTokenByUser($user)); + $this->assertEquals(['token'], $this->tokenProvider->getTokenByUser('uid')); } public function testGetPassword() { @@ -189,7 +188,18 @@ class PublicKeyTokenProviderTest extends TestCase { $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + $this->mapper->method('getTokenByUser') + ->with('user') + ->willReturn([$actual]); + $newpass = 'newpass'; + $this->mapper->expects($this->once()) + ->method('update') + ->with($this->callback(function ($token) use ($newpass) { + return $newpass === $this->tokenProvider->getPassword($token, 'token'); + })); + + $this->tokenProvider->setPassword($actual, $token, $newpass); $this->assertSame($newpass, $this->tokenProvider->getPassword($actual, 'token')); @@ -216,13 +226,12 @@ class PublicKeyTokenProviderTest extends TestCase { public function testInvaildateTokenById() { $id = 123; - $user = $this->createMock(IUser::class); $this->mapper->expects($this->once()) ->method('deleteById') - ->with($user, $id); + ->with('uid', $id); - $this->tokenProvider->invalidateTokenById($user, $id); + $this->tokenProvider->invalidateTokenById('uid', $id); } public function testInvalidateOldTokens() { From d03d16a93613337d20dbf68cb0430655d4dadf3e Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Tue, 29 May 2018 13:38:26 +0200 Subject: [PATCH 07/13] Add publickey provider to manager Signed-off-by: Roeland Jago Douma --- lib/private/Authentication/Token/Manager.php | 99 ++++++++++---------- 1 file changed, 52 insertions(+), 47 deletions(-) diff --git a/lib/private/Authentication/Token/Manager.php b/lib/private/Authentication/Token/Manager.php index 7ec90e92ed0..adcd4de3f81 100644 --- a/lib/private/Authentication/Token/Manager.php +++ b/lib/private/Authentication/Token/Manager.php @@ -32,8 +32,12 @@ class Manager implements IProvider { /** @var DefaultTokenProvider */ private $defaultTokenProvider; - public function __construct(DefaultTokenProvider $defaultTokenProvider) { + /** @var PublicKeyTokenProvider */ + private $publicKeyTokenProvider; + + public function __construct(DefaultTokenProvider $defaultTokenProvider, PublicKeyTokenProvider $publicKeyTokenProvider) { $this->defaultTokenProvider = $defaultTokenProvider; + $this->publicKeyTokenProvider = $publicKeyTokenProvider; } /** @@ -76,6 +80,8 @@ class Manager implements IProvider { public function updateToken(IToken $token) { if ($token instanceof DefaultToken) { $this->defaultTokenProvider->updateToken($token); + } else if ($token instanceof PublicKeyToken) { + $this->publicKeyTokenProvider->updateToken($token); } else { throw new InvalidTokenException(); } @@ -90,22 +96,18 @@ class Manager implements IProvider { public function updateTokenActivity(IToken $token) { if ($token instanceof DefaultToken) { $this->defaultTokenProvider->updateTokenActivity($token); + } else if ($token instanceof PublicKeyToken) { + $this->publicKeyTokenProvider->updateTokenActivity($token); } else { throw new InvalidTokenException(); } } - /** - * 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(string $uid): array { - return $this->defaultTokenProvider->getTokenByUser($uid); + $old = $this->defaultTokenProvider->getTokenByUser($uid); + $new = $this->publicKeyTokenProvider->getTokenByUser($uid); + + return array_merge($old, $new); } /** @@ -116,8 +118,11 @@ class Manager implements IProvider { * @return IToken */ public function getToken(string $tokenId): IToken { - // TODO: first try new token then old token - return $this->defaultTokenProvider->getToken($tokenId); + try { + return $this->publicKeyTokenProvider->getToken($tokenId); + } catch (InvalidTokenException $e) { + return $this->defaultTokenProvider->getToken($tokenId); + } } /** @@ -128,8 +133,11 @@ class Manager implements IProvider { * @return IToken */ public function getTokenById(int $tokenId): IToken { - // TODO: Find a way to distinguis between tokens - return $this->defaultTokenProvider->getTokenById($tokenId); + try { + $this->publicKeyTokenProvider->getTokenById($tokenId); + } catch (InvalidTokenException $e) { + return $this->defaultTokenProvider->getTokenById($tokenId); + } } /** @@ -138,9 +146,12 @@ class Manager implements IProvider { * @throws InvalidTokenException */ public function renewSessionToken(string $oldSessionId, string $sessionId) { - // TODO: first try new then old - // TODO: if old move to new token type - $this->defaultTokenProvider->renewSessionToken($oldSessionId, $sessionId); + try { + $this->publicKeyTokenProvider->renewSessionToken($oldSessionId, $sessionId); + } catch (InvalidTokenException $e) { + //TODO: Move to new token + $this->defaultTokenProvider->renewSessionToken($oldSessionId, $sessionId); + } } /** @@ -151,59 +162,53 @@ class Manager implements IProvider { * @return string */ public function getPassword(IToken $savedToken, string $tokenId): string { - //TODO convert to new token type if ($savedToken instanceof DefaultToken) { + //TODO convert to new token type return $this->defaultTokenProvider->getPassword($savedToken, $tokenId); } + + if ($savedToken instanceof PublicKeyToken) { + return $this->publicKeyTokenProvider->getPassword($savedToken, $tokenId); + } } - /** - * Encrypt and set the password of the given token - * - * @param IToken $token - * @param string $tokenId - * @param string $password - * @throws InvalidTokenException - */ public function setPassword(IToken $token, string $tokenId, string $password) { - //TODO conver to new token + if ($token instanceof DefaultToken) { + //TODO conver to new token $this->defaultTokenProvider->setPassword($token, $tokenId, $password); } + + if ($tokenId instanceof PublicKeyToken) { + $this->publicKeyTokenProvider->setPassword($token, $tokenId, $password); + } } - /** - * Invalidate (delete) the given session token - * - * @param string $token - */ public function invalidateToken(string $token) { - // TODO: check both providers $this->defaultTokenProvider->invalidateToken($token); + $this->publicKeyTokenProvider->invalidateToken($token); } - /** - * Invalidate (delete) the given token - * - * @param IUser $user - * @param int $id - */ public function invalidateTokenById(string $uid, int $id) { - //TODO find way to distinguis between tokens $this->defaultTokenProvider->invalidateTokenById($uid, $id); + $this->publicKeyTokenProvider->invalidateTokenById($uid, $id); } - /** - * Invalidate (delete) old session tokens - */ public function invalidateOldTokens() { - //Call on both $this->defaultTokenProvider->invalidateOldTokens(); + $this->publicKeyTokenProvider->invalidateOldTokens(); } public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken { - // Migrate to new token - return $this->defaultTokenProvider->rotate($token, $oldTokenId, $newTokenId); + + if ($token instanceof DefaultToken) { + //TODO Migrate to new token + return $this->defaultTokenProvider->rotate($token, $oldTokenId, $newTokenId); + } + + if ($token instanceof PublicKeyToken) { + return $this->publicKeyTokenProvider->rotate($token, $oldTokenId, $newTokenId); + } } From f168ecfa7adc484d53a88facdc12a7785583209f Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Thu, 31 May 2018 21:56:17 +0200 Subject: [PATCH 08/13] Actually convert the token * When getting the token * When rotating the token * Also store the encrypted password as base64 to avoid weird binary stuff Signed-off-by: Roeland Jago Douma --- lib/private/Authentication/Token/Manager.php | 27 +++-- .../Token/PublicKeyTokenMapper.php | 8 ++ .../Token/PublicKeyTokenProvider.php | 99 +++++++++++++------ 3 files changed, 94 insertions(+), 40 deletions(-) diff --git a/lib/private/Authentication/Token/Manager.php b/lib/private/Authentication/Token/Manager.php index adcd4de3f81..981ea74f568 100644 --- a/lib/private/Authentication/Token/Manager.php +++ b/lib/private/Authentication/Token/Manager.php @@ -121,8 +121,19 @@ class Manager implements IProvider { try { return $this->publicKeyTokenProvider->getToken($tokenId); } catch (InvalidTokenException $e) { - return $this->defaultTokenProvider->getToken($tokenId); + // 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); } /** @@ -149,7 +160,6 @@ class Manager implements IProvider { try { $this->publicKeyTokenProvider->renewSessionToken($oldSessionId, $sessionId); } catch (InvalidTokenException $e) { - //TODO: Move to new token $this->defaultTokenProvider->renewSessionToken($oldSessionId, $sessionId); } } @@ -163,7 +173,6 @@ class Manager implements IProvider { */ public function getPassword(IToken $savedToken, string $tokenId): string { if ($savedToken instanceof DefaultToken) { - //TODO convert to new token type return $this->defaultTokenProvider->getPassword($savedToken, $tokenId); } @@ -173,9 +182,7 @@ class Manager implements IProvider { } public function setPassword(IToken $token, string $tokenId, string $password) { - if ($token instanceof DefaultToken) { - //TODO conver to new token $this->defaultTokenProvider->setPassword($token, $tokenId, $password); } @@ -200,10 +207,14 @@ class Manager implements IProvider { } public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken { - if ($token instanceof DefaultToken) { - //TODO Migrate to new token - return $this->defaultTokenProvider->rotate($token, $oldTokenId, $newTokenId); + try { + $password = $this->defaultTokenProvider->getPassword($token, $oldTokenId); + } catch (PasswordlessTokenException $e) { + $password = null; + } + + return $this->publicKeyTokenProvider->convertToken($token, $newTokenId, $password); } if ($token instanceof PublicKeyToken) { diff --git a/lib/private/Authentication/Token/PublicKeyTokenMapper.php b/lib/private/Authentication/Token/PublicKeyTokenMapper.php index 6feb176fb68..30349fba31a 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenMapper.php +++ b/lib/private/Authentication/Token/PublicKeyTokenMapper.php @@ -159,4 +159,12 @@ class PublicKeyTokenMapper extends QBMapper { $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(2, IQueryBuilder::PARAM_INT))); + } } diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index 5c97877e730..e512133a962 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -67,36 +67,7 @@ class PublicKeyTokenProvider implements IProvider { string $name, int $type = IToken::TEMPORARY_TOKEN, int $remember = IToken::DO_NOT_REMEMBER): IToken { - $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 = $this->newToken($token, $uid, $loginName, $password, $name, $type, $remember); $this->mapper->insert($dbToken); @@ -219,6 +190,9 @@ class PublicKeyTokenProvider implements IProvider { throw new InvalidTokenException(); } + // When changeing 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) { @@ -226,8 +200,6 @@ class PublicKeyTokenProvider implements IProvider { $t->setPassword($this->encryptPassword($password, $publicKey)); $this->updateToken($t); } - - //TODO: should we also do this for temp tokens? } public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken { @@ -267,11 +239,13 @@ class PublicKeyTokenProvider implements IProvider { 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; @@ -281,4 +255,65 @@ class PublicKeyTokenProvider implements IProvider { $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->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()); + + return $dbToken; + } } From 1999f7ce7bd438f9b545fff8956ea4ed24d3dc8d Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Thu, 31 May 2018 21:57:28 +0200 Subject: [PATCH 09/13] Generate the new publicKey tokens by default! Signed-off-by: Roeland Jago Douma --- lib/private/Authentication/Token/Manager.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/private/Authentication/Token/Manager.php b/lib/private/Authentication/Token/Manager.php index 981ea74f568..e4f0ead862b 100644 --- a/lib/private/Authentication/Token/Manager.php +++ b/lib/private/Authentication/Token/Manager.php @@ -59,8 +59,7 @@ class Manager implements IProvider { string $name, int $type = IToken::TEMPORARY_TOKEN, int $remember = IToken::DO_NOT_REMEMBER): IToken { - //TODO switch to new token by default once it is there - return $this->defaultTokenProvider->generateToken( + return $this->publicKeyTokenProvider->generateToken( $token, $uid, $loginName, From 9e7a95fe58ee8ace0ae30ff2ebda993018542187 Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Thu, 31 May 2018 22:56:26 +0200 Subject: [PATCH 10/13] Add more tests * Add a lot of tests * Fixes related to those tests * Fix tests Signed-off-by: Roeland Jago Douma --- lib/private/Authentication/Token/Manager.php | 12 +- .../Token/PublicKeyTokenMapper.php | 2 + .../Token/PublicKeyTokenProvider.php | 3 +- .../Token/DefaultTokenCleanupJobTest.php | 12 +- .../lib/Authentication/Token/ManagerTest.php | 451 ++++++++++++++++++ .../Token/PublicKeyTokenMapperTest.php | 26 +- .../Token/PublicKeyTokenProviderTest.php | 34 +- 7 files changed, 506 insertions(+), 34 deletions(-) create mode 100644 tests/lib/Authentication/Token/ManagerTest.php diff --git a/lib/private/Authentication/Token/Manager.php b/lib/private/Authentication/Token/Manager.php index e4f0ead862b..5e30afbc92e 100644 --- a/lib/private/Authentication/Token/Manager.php +++ b/lib/private/Authentication/Token/Manager.php @@ -144,7 +144,7 @@ class Manager implements IProvider { */ public function getTokenById(int $tokenId): IToken { try { - $this->publicKeyTokenProvider->getTokenById($tokenId); + return $this->publicKeyTokenProvider->getTokenById($tokenId); } catch (InvalidTokenException $e) { return $this->defaultTokenProvider->getTokenById($tokenId); } @@ -178,16 +178,22 @@ class Manager implements IProvider { if ($savedToken instanceof PublicKeyToken) { return $this->publicKeyTokenProvider->getPassword($savedToken, $tokenId); } + + throw new InvalidTokenException(); } public function setPassword(IToken $token, string $tokenId, string $password) { if ($token instanceof DefaultToken) { $this->defaultTokenProvider->setPassword($token, $tokenId, $password); + return; } - if ($tokenId instanceof PublicKeyToken) { + if ($token instanceof PublicKeyToken) { $this->publicKeyTokenProvider->setPassword($token, $tokenId, $password); + return; } + + throw new InvalidTokenException(); } public function invalidateToken(string $token) { @@ -219,6 +225,8 @@ class Manager implements IProvider { if ($token instanceof PublicKeyToken) { return $this->publicKeyTokenProvider->rotate($token, $oldTokenId, $newTokenId); } + + throw new InvalidTokenException(); } diff --git a/lib/private/Authentication/Token/PublicKeyTokenMapper.php b/lib/private/Authentication/Token/PublicKeyTokenMapper.php index 30349fba31a..23982c6ba09 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenMapper.php +++ b/lib/private/Authentication/Token/PublicKeyTokenMapper.php @@ -166,5 +166,7 @@ class PublicKeyTokenMapper extends QBMapper { ->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(2, IQueryBuilder::PARAM_INT))); + + $qb->execute(); } } diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index e512133a962..ca7a7e37e1e 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -196,7 +196,7 @@ class PublicKeyTokenProvider implements IProvider { // Update the password for all tokens $tokens = $this->mapper->getTokenByUser($token->getUID()); foreach ($tokens as $t) { - $publicKey = $token->getPublicKey(); + $publicKey = $t->getPublicKey(); $t->setPassword($this->encryptPassword($password, $publicKey)); $this->updateToken($t); } @@ -271,6 +271,7 @@ class PublicKeyTokenProvider implements IProvider { $defaultToken->getRemember() ); + $pkToken->setExpires($defaultToken->getExpires()); $pkToken->setId($defaultToken->getId()); return $this->mapper->update($pkToken); diff --git a/tests/lib/Authentication/Token/DefaultTokenCleanupJobTest.php b/tests/lib/Authentication/Token/DefaultTokenCleanupJobTest.php index c9082c08b30..b8074d75b30 100644 --- a/tests/lib/Authentication/Token/DefaultTokenCleanupJobTest.php +++ b/tests/lib/Authentication/Token/DefaultTokenCleanupJobTest.php @@ -23,6 +23,8 @@ namespace Test\Authentication\Token; use OC\Authentication\Token\DefaultTokenCleanupJob; +use OC\Authentication\Token\IProvider; +use OC\Authentication\Token\Manager; use Test\TestCase; class DefaultTokenCleanupJobTest extends TestCase { @@ -34,19 +36,13 @@ class DefaultTokenCleanupJobTest extends TestCase { protected function setUp() { parent::setUp(); - $this->tokenProvider = $this->getMockBuilder('\OC\Authentication\Token\DefaultTokenProvider') + $this->tokenProvider = $this->getMockBuilder(Manager::class) ->disableOriginalConstructor() ->getMock(); - $this->overwriteService('\OC\Authentication\Token\DefaultTokenProvider', $this->tokenProvider); + $this->overwriteService(IProvider::class, $this->tokenProvider); $this->job = new DefaultTokenCleanupJob(); } - protected function tearDown() { - parent::tearDown(); - - $this->restoreService('\OC\Authentication\Token\DefaultTokenProvider'); - } - public function testRun() { $this->tokenProvider->expects($this->once()) ->method('invalidateOldTokens') diff --git a/tests/lib/Authentication/Token/ManagerTest.php b/tests/lib/Authentication/Token/ManagerTest.php new file mode 100644 index 00000000000..8b77bfc4994 --- /dev/null +++ b/tests/lib/Authentication/Token/ManagerTest.php @@ -0,0 +1,451 @@ + + * + * @author Roeland Jago Douma + * + * @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 . + * + */ + +namespace Test\Authentication\Token; + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Exceptions\PasswordlessTokenException; +use OC\Authentication\Token\DefaultToken; +use OC\Authentication\Token\DefaultTokenProvider; +use OC\Authentication\Token\Manager; +use OC\Authentication\Token\PublicKeyToken; +use OC\Authentication\Token\PublicKeyTokenMapper; +use OC\Authentication\Token\PublicKeyTokenProvider; +use OC\Authentication\Token\ExpiredTokenException; +use OC\Authentication\Token\IToken; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\ILogger; +use OCP\IUser; +use OCP\Security\ICrypto; +use Test\TestCase; + +class ManagerTest extends TestCase { + + /** @var PublicKeyTokenProvider|\PHPUnit_Framework_MockObject_MockObject */ + private $publicKeyTokenProvider; + /** @var DefaultTokenProvider|\PHPUnit_Framework_MockObject_MockObject */ + private $defaultTokenProvider; + /** @var Manager */ + private $manager; + + protected function setUp() { + parent::setUp(); + + $this->publicKeyTokenProvider = $this->createMock(PublicKeyTokenProvider::class); + $this->defaultTokenProvider = $this->createMock(DefaultTokenProvider::class); + $this->manager = new Manager( + $this->defaultTokenProvider, + $this->publicKeyTokenProvider + ); + } + + public function testGenerateToken() { + $this->defaultTokenProvider->expects($this->never()) + ->method('generateToken'); + + $token = new PublicKeyToken(); + + $this->publicKeyTokenProvider->expects($this->once()) + ->method('generateToken') + ->with( + 'token', + 'uid', + 'loginName', + 'password', + 'name', + IToken::TEMPORARY_TOKEN, + IToken::REMEMBER + )->willReturn($token); + + $actual = $this->manager->generateToken( + 'token', + 'uid', + 'loginName', + 'password', + 'name', + IToken::TEMPORARY_TOKEN, + IToken::REMEMBER + ); + + $this->assertSame($token, $actual); + } + + public function tokenData(): array { + return [ + [new DefaultToken()], + [new PublicKeyToken()], + [$this->createMock(IToken::class)], + ]; + } + + protected function setNoCall(IToken $token) { + if (!($token instanceof DefaultToken)) { + $this->defaultTokenProvider->expects($this->never()) + ->method($this->anything()); + } + + if (!($token instanceof PublicKeyToken)) { + $this->publicKeyTokenProvider->expects($this->never()) + ->method($this->anything()); + } + } + + protected function setCall(IToken $token, string $function, $return = null) { + if ($token instanceof DefaultToken) { + $this->defaultTokenProvider->expects($this->once()) + ->method($function) + ->with($token) + ->willReturn($return); + } + + if ($token instanceof PublicKeyToken) { + $this->publicKeyTokenProvider->expects($this->once()) + ->method($function) + ->with($token) + ->willReturn($return); + } + } + + protected function setException(IToken $token) { + if (!($token instanceof DefaultToken) && !($token instanceof PublicKeyToken)) { + $this->expectException(InvalidTokenException::class); + } + } + + /** + * @dataProvider tokenData + */ + public function testUpdateToken(IToken $token) { + $this->setNoCall($token); + $this->setCall($token, 'updateToken'); + $this->setException($token); + + $this->manager->updateToken($token); + } + + /** + * @dataProvider tokenData + */ + public function testUpdateTokenActivity(IToken $token) { + $this->setNoCall($token); + $this->setCall($token, 'updateTokenActivity'); + $this->setException($token); + + $this->manager->updateTokenActivity($token); + } + + /** + * @dataProvider tokenData + */ + public function testGetPassword(IToken $token) { + $this->setNoCall($token); + $this->setCall($token, 'getPassword', 'password'); + $this->setException($token); + + $result = $this->manager->getPassword($token, 'tokenId', 'password'); + + $this->assertSame('password', $result); + } + + /** + * @dataProvider tokenData + */ + public function testSetPassword(IToken $token) { + $this->setNoCall($token); + $this->setCall($token, 'setPassword'); + $this->setException($token); + + $this->manager->setPassword($token, 'tokenId', 'password'); + } + + public function testInvalidateTokens() { + $this->defaultTokenProvider->expects($this->once()) + ->method('invalidateToken') + ->with('token'); + + $this->publicKeyTokenProvider->expects($this->once()) + ->method('invalidateToken') + ->with('token'); + + $this->manager->invalidateToken('token'); + } + + public function testInvalidateTokenById() { + $this->defaultTokenProvider->expects($this->once()) + ->method('invalidateTokenById') + ->with('uid', 42); + + $this->publicKeyTokenProvider->expects($this->once()) + ->method('invalidateTokenById') + ->with('uid', 42); + + $this->manager->invalidateTokenById('uid', 42); + } + + public function testInvalidateOldTokens() { + $this->defaultTokenProvider->expects($this->once()) + ->method('invalidateOldTokens'); + + $this->publicKeyTokenProvider->expects($this->once()) + ->method('invalidateOldTokens'); + + $this->manager->invalidateOldTokens(); + } + + public function testGetTokenByUser() { + $t1 = new DefaultToken(); + $t2 = new DefaultToken(); + $t3 = new PublicKeyToken(); + $t4 = new PublicKeyToken(); + + $this->defaultTokenProvider + ->method('getTokenByUser') + ->willReturn([$t1, $t2]); + + $this->publicKeyTokenProvider + ->method('getTokenByUser') + ->willReturn([$t3, $t4]); + + $result = $this->manager->getTokenByUser('uid'); + + $this->assertEquals([$t1, $t2, $t3, $t4], $result); + } + + public function testRenewSessionTokenPublicKey() { + $this->defaultTokenProvider->expects($this->never()) + ->method($this->anything()); + + $this->publicKeyTokenProvider->expects($this->once()) + ->method('renewSessionToken') + ->with('oldId', 'newId'); + + $this->manager->renewSessionToken('oldId', 'newId'); + } + + public function testRenewSessionTokenDefault() { + $this->publicKeyTokenProvider->expects($this->once()) + ->method('renewSessionToken') + ->with('oldId', 'newId') + ->willThrowException(new InvalidTokenException()); + + $this->defaultTokenProvider->expects($this->once()) + ->method('renewSessionToken') + ->with('oldId', 'newId'); + + $this->manager->renewSessionToken('oldId', 'newId'); + } + + public function testRenewSessionInvalid() { + $this->publicKeyTokenProvider->expects($this->once()) + ->method('renewSessionToken') + ->with('oldId', 'newId') + ->willThrowException(new InvalidTokenException()); + + $this->defaultTokenProvider->expects($this->once()) + ->method('renewSessionToken') + ->with('oldId', 'newId') + ->willThrowException(new InvalidTokenException()); + + $this->expectException(InvalidTokenException::class); + $this->manager->renewSessionToken('oldId', 'newId'); + } + + public function testGetTokenByIdPublicKey() { + $token = $this->createMock(IToken::class); + + $this->publicKeyTokenProvider->expects($this->once()) + ->method('getTokenById') + ->with(42) + ->willReturn($token); + + $this->defaultTokenProvider->expects($this->never()) + ->method($this->anything()); + + + $this->assertSame($token, $this->manager->getTokenById(42)); + } + + public function testGetTokenByIdDefault() { + $token = $this->createMock(IToken::class); + + $this->publicKeyTokenProvider->expects($this->once()) + ->method('getTokenById') + ->with(42) + ->willThrowException(new InvalidTokenException()); + + $this->defaultTokenProvider->expects($this->once()) + ->method('getTokenById') + ->with(42) + ->willReturn($token); + + $this->assertSame($token, $this->manager->getTokenById(42)); + } + + public function testGetTokenByIdInvalid() { + $this->publicKeyTokenProvider->expects($this->once()) + ->method('getTokenById') + ->with(42) + ->willThrowException(new InvalidTokenException()); + + $this->defaultTokenProvider->expects($this->once()) + ->method('getTokenById') + ->with(42) + ->willThrowException(new InvalidTokenException()); + + $this->expectException(InvalidTokenException::class); + $this->manager->getTokenById(42); + } + + public function testGetTokenPublicKey() { + $token = new PublicKeyToken(); + + $this->defaultTokenProvider->expects($this->never()) + ->method($this->anything()); + + $this->publicKeyTokenProvider + ->method('getToken') + ->with('tokenId') + ->willReturn($token); + + $this->assertSame($token, $this->manager->getToken('tokenId')); + } + + public function testGetTokenInvalid() { + $this->defaultTokenProvider + ->method('getToken') + ->with('tokenId') + ->willThrowException(new InvalidTokenException()); + + $this->publicKeyTokenProvider + ->method('getToken') + ->with('tokenId') + ->willThrowException(new InvalidTokenException()); + + $this->expectException(InvalidTokenException::class); + $this->manager->getToken('tokenId'); + } + + public function testGetTokenConvertPassword() { + $oldToken = new DefaultToken(); + $newToken = new PublicKeyToken(); + + $this->publicKeyTokenProvider + ->method('getToken') + ->with('tokenId') + ->willThrowException(new InvalidTokenException()); + + $this->defaultTokenProvider + ->method('getToken') + ->willReturn($oldToken); + + $this->defaultTokenProvider + ->method('getPassword') + ->with($oldToken, 'tokenId') + ->willReturn('password'); + + $this->publicKeyTokenProvider + ->method('convertToken') + ->with($oldToken, 'tokenId', 'password') + ->willReturn($newToken); + + $this->assertSame($newToken, $this->manager->getToken('tokenId')); + } + + public function testGetTokenConvertNoPassword() { + $oldToken = new DefaultToken(); + $newToken = new PublicKeyToken(); + + $this->publicKeyTokenProvider + ->method('getToken') + ->with('tokenId') + ->willThrowException(new InvalidTokenException()); + + $this->defaultTokenProvider + ->method('getToken') + ->willReturn($oldToken); + + $this->defaultTokenProvider + ->method('getPassword') + ->with($oldToken, 'tokenId') + ->willThrowException(new PasswordlessTokenException()); + + $this->publicKeyTokenProvider + ->method('convertToken') + ->with($oldToken, 'tokenId', null) + ->willReturn($newToken); + + $this->assertSame($newToken, $this->manager->getToken('tokenId')); + } + + public function testRotateInvalid() { + $this->expectException(InvalidTokenException::class); + $this->manager->rotate($this->createMock(IToken::class), 'oldId', 'newId'); + } + + public function testRotatePublicKey() { + $token = new PublicKeyToken(); + + $this->publicKeyTokenProvider + ->method('rotate') + ->with($token, 'oldId', 'newId') + ->willReturn($token); + + $this->assertSame($token, $this->manager->rotate($token, 'oldId', 'newId')); + } + + public function testRotateConvertPassword() { + $oldToken = new DefaultToken(); + $newToken = new PublicKeyToken(); + + $this->defaultTokenProvider + ->method('getPassword') + ->with($oldToken, 'oldId') + ->willReturn('password'); + + $this->publicKeyTokenProvider + ->method('convertToken') + ->with($oldToken, 'newId', 'password') + ->willReturn($newToken); + + $this->assertSame($newToken, $this->manager->rotate($oldToken, 'oldId', 'newId')); + } + + public function testRotateConvertNoPassword() { + $oldToken = new DefaultToken(); + $newToken = new PublicKeyToken(); + + $this->defaultTokenProvider + ->method('getPassword') + ->with($oldToken, 'oldId') + ->willThrowException(new PasswordlessTokenException()); + + $this->publicKeyTokenProvider + ->method('convertToken') + ->with($oldToken, 'newId', null) + ->willReturn($newToken); + + $this->assertSame($newToken, $this->manager->rotate($oldToken, 'oldId', 'newId')); + } +} diff --git a/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php b/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php index 7c4dbabad6c..ce4de92e193 100644 --- a/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php +++ b/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php @@ -204,23 +204,11 @@ class PublicKeyTokenMapperTest extends TestCase { } public function testGetTokenByUser() { - /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->once()) - ->method('getUID') - ->will($this->returnValue('user1')); - - $this->assertCount(2, $this->mapper->getTokenByUser($user)); + $this->assertCount(2, $this->mapper->getTokenByUser('user1')); } public function testGetTokenByUserNotFound() { - /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->once()) - ->method('getUID') - ->will($this->returnValue('user1000')); - - $this->assertCount(0, $this->mapper->getTokenByUser($user)); + $this->assertCount(0, $this->mapper->getTokenByUser('user1000')); } public function testDeleteById() { @@ -232,11 +220,8 @@ class PublicKeyTokenMapperTest extends TestCase { ->where($qb->expr()->eq('token', $qb->createNamedParameter('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'))); $result = $qb->execute(); $id = $result->fetch()['id']; - $user->expects($this->once()) - ->method('getUID') - ->will($this->returnValue('user1')); - $this->mapper->deleteById($user, (int)$id); + $this->mapper->deleteById('user1', (int)$id); $this->assertEquals(2, $this->getNumberOfTokens()); } @@ -244,11 +229,8 @@ class PublicKeyTokenMapperTest extends TestCase { /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ $user = $this->createMock(IUser::class); $id = 33; - $user->expects($this->once()) - ->method('getUID') - ->will($this->returnValue('user10000')); - $this->mapper->deleteById($user, $id); + $this->mapper->deleteById('user1000', $id); $this->assertEquals(3, $this->getNumberOfTokens()); } diff --git a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php index d5cfe5d1ee6..cd3bcb81ba6 100644 --- a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php +++ b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php @@ -25,6 +25,7 @@ namespace Test\Authentication\Token; use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Exceptions\PasswordlessTokenException; +use OC\Authentication\Token\DefaultToken; use OC\Authentication\Token\PublicKeyToken; use OC\Authentication\Token\PublicKeyTokenMapper; use OC\Authentication\Token\PublicKeyTokenProvider; @@ -357,7 +358,7 @@ class PublicKeyTokenProviderTest extends TestCase { $this->callback(function (string $token) { return hash('sha512', 'unhashedToken'.'1f4h9s') === $token; }) - )->willThrowException(new InvalidTokenException()); + )->willThrowException(new DoesNotExistException('nope')); $this->tokenProvider->getToken('unhashedToken'); } @@ -471,4 +472,35 @@ class PublicKeyTokenProviderTest extends TestCase { $this->assertNotSame($newPrivate, $oldPrivate); $this->assertNull($new->getPassword()); } + + public function testConvertToken() { + $defaultToken = new DefaultToken(); + $defaultToken->setId(42); + $defaultToken->setPassword('oldPass'); + $defaultToken->setExpires(1337); + $defaultToken->setToken('oldToken'); + $defaultToken->setUid('uid'); + $defaultToken->setLoginName('loginName'); + $defaultToken->setLastActivity(999); + $defaultToken->setName('name'); + $defaultToken->setRemember(IToken::REMEMBER); + $defaultToken->setType(IToken::PERMANENT_TOKEN); + + $this->mapper->expects($this->once()) + ->method('update') + ->willReturnArgument(0); + + $newToken = $this->tokenProvider->convertToken($defaultToken, 'newToken', 'newPassword'); + + $this->assertSame(42, $newToken->getId()); + $this->assertSame('newPassword', $this->tokenProvider->getPassword($newToken, 'newToken')); + $this->assertSame(1337, $newToken->getExpires()); + $this->assertSame('uid', $newToken->getUID()); + $this->assertSame('loginName', $newToken->getLoginName()); + $this->assertSame(1313131, $newToken->getLastActivity()); + $this->assertSame(1313131, $newToken->getLastCheck()); + $this->assertSame('name', $newToken->getName()); + $this->assertSame(IToken::REMEMBER, $newToken->getRemember()); + $this->assertSame(IToken::PERMANENT_TOKEN, $newToken->getType()); + } } From df34571d1d888e7232487664629679130fef5e5e Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Mon, 4 Jun 2018 09:51:13 +0200 Subject: [PATCH 11/13] Use constant for token version And don't set the version in the constructor. That would possible cause to many updates. Signed-off-by: Roeland Jago Douma --- .../Authentication/Token/DefaultToken.php | 4 ++-- .../Authentication/Token/DefaultTokenMapper.php | 14 +++++++------- .../Token/DefaultTokenProvider.php | 1 + .../Authentication/Token/PublicKeyToken.php | 4 ++-- .../Token/PublicKeyTokenMapper.php | 16 ++++++++-------- .../Token/PublicKeyTokenProvider.php | 3 ++- .../Token/DefaultTokenMapperTest.php | 2 ++ .../Token/DefaultTokenProviderTest.php | 1 + .../Token/PublicKeyTokenMapperTest.php | 2 ++ 9 files changed, 27 insertions(+), 20 deletions(-) diff --git a/lib/private/Authentication/Token/DefaultToken.php b/lib/private/Authentication/Token/DefaultToken.php index 29c4b5a74ad..85ea0dc4cdd 100644 --- a/lib/private/Authentication/Token/DefaultToken.php +++ b/lib/private/Authentication/Token/DefaultToken.php @@ -41,6 +41,8 @@ use OCP\AppFramework\Db\Entity; */ class DefaultToken extends Entity implements IToken { + const VERSION = 1; + /** @var string user UID */ protected $uid; @@ -90,8 +92,6 @@ class DefaultToken extends Entity implements IToken { $this->addType('scope', 'string'); $this->addType('expires', 'int'); $this->addType('version', 'int'); - - $this->setVersion(1); } public function getId(): int { diff --git a/lib/private/Authentication/Token/DefaultTokenMapper.php b/lib/private/Authentication/Token/DefaultTokenMapper.php index 02964e3f59c..b8df00ff094 100644 --- a/lib/private/Authentication/Token/DefaultTokenMapper.php +++ b/lib/private/Authentication/Token/DefaultTokenMapper.php @@ -50,7 +50,7 @@ class DefaultTokenMapper extends QBMapper { $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') ->where($qb->expr()->eq('token', $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR))) - ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); } @@ -65,7 +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(1, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); } @@ -82,7 +82,7 @@ class DefaultTokenMapper extends QBMapper { $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(1, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); $data = $result->fetch(); @@ -106,7 +106,7 @@ class DefaultTokenMapper extends QBMapper { $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(1, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); $data = $result->fetch(); @@ -132,7 +132,7 @@ class DefaultTokenMapper extends QBMapper { $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($uid))) - ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))) ->setMaxResults(1000); $result = $qb->execute(); $data = $result->fetchAll(); @@ -151,7 +151,7 @@ class DefaultTokenMapper extends QBMapper { $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(1, IQueryBuilder::PARAM_INT))); + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))); $qb->execute(); } @@ -164,7 +164,7 @@ class DefaultTokenMapper extends QBMapper { $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(1, IQueryBuilder::PARAM_INT))); + ->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 ed3c14c1745..078ab4ed8f2 100644 --- a/lib/private/Authentication/Token/DefaultTokenProvider.php +++ b/lib/private/Authentication/Token/DefaultTokenProvider.php @@ -105,6 +105,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); diff --git a/lib/private/Authentication/Token/PublicKeyToken.php b/lib/private/Authentication/Token/PublicKeyToken.php index 18b27075772..9d01fc9ecca 100644 --- a/lib/private/Authentication/Token/PublicKeyToken.php +++ b/lib/private/Authentication/Token/PublicKeyToken.php @@ -45,6 +45,8 @@ use OCP\AppFramework\Db\Entity; */ class PublicKeyToken extends Entity implements IToken { + const VERSION = 2; + /** @var string user UID */ protected $uid; @@ -102,8 +104,6 @@ class PublicKeyToken extends Entity implements IToken { $this->addType('publicKey', 'string'); $this->addType('privateKey', 'string'); $this->addType('version', 'int'); - - $this->setVersion(2); } public function getId(): int { diff --git a/lib/private/Authentication/Token/PublicKeyTokenMapper.php b/lib/private/Authentication/Token/PublicKeyTokenMapper.php index 23982c6ba09..129b2a272bb 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenMapper.php +++ b/lib/private/Authentication/Token/PublicKeyTokenMapper.php @@ -45,7 +45,7 @@ class PublicKeyTokenMapper extends QBMapper { $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) - ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(2, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); } @@ -60,7 +60,7 @@ class PublicKeyTokenMapper 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(2, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); } @@ -75,7 +75,7 @@ class PublicKeyTokenMapper extends QBMapper { $result = $qb->select('*') ->from('authtoken') ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) - ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(2, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); $data = $result->fetch(); @@ -97,7 +97,7 @@ class PublicKeyTokenMapper extends QBMapper { $result = $qb->select('*') ->from('authtoken') ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) - ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(2, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); $data = $result->fetch(); @@ -123,7 +123,7 @@ class PublicKeyTokenMapper extends QBMapper { $qb->select('*') ->from('authtoken') ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) - ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(2, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) ->setMaxResults(1000); $result = $qb->execute(); $data = $result->fetchAll(); @@ -142,7 +142,7 @@ class PublicKeyTokenMapper extends QBMapper { $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(2, IQueryBuilder::PARAM_INT))); + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); $qb->execute(); } @@ -155,7 +155,7 @@ class PublicKeyTokenMapper extends QBMapper { $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(2, IQueryBuilder::PARAM_INT))); + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); $qb->execute(); } @@ -165,7 +165,7 @@ class PublicKeyTokenMapper extends QBMapper { $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(2, IQueryBuilder::PARAM_INT))); + ->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 index ca7a7e37e1e..b7e0d1da332 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -190,7 +190,7 @@ class PublicKeyTokenProvider implements IProvider { throw new InvalidTokenException(); } - // When changeing passwords all temp tokens are deleted + // When changing passwords all temp tokens are deleted $this->mapper->deleteTempToken($token); // Update the password for all tokens @@ -314,6 +314,7 @@ class PublicKeyTokenProvider implements IProvider { $dbToken->setRemember($remember); $dbToken->setLastActivity($this->time->getTime()); $dbToken->setLastCheck($this->time->getTime()); + $dbToken->setVersion(PublicKeyToken::VERSION); return $dbToken; } diff --git a/tests/lib/Authentication/Token/DefaultTokenMapperTest.php b/tests/lib/Authentication/Token/DefaultTokenMapperTest.php index ab09c005297..bebceba62cf 100644 --- a/tests/lib/Authentication/Token/DefaultTokenMapperTest.php +++ b/tests/lib/Authentication/Token/DefaultTokenMapperTest.php @@ -135,6 +135,7 @@ class DefaultTokenMapperTest extends TestCase { $token->setRemember(IToken::DO_NOT_REMEMBER); $token->setLastActivity($this->time - 60 * 60 * 24 * 3); $token->setLastCheck($this->time - 10); + $token->setVersion(DefaultToken::VERSION); $dbToken = $this->mapper->getToken($token->getToken()); @@ -164,6 +165,7 @@ class DefaultTokenMapperTest extends TestCase { $token->setRemember(IToken::DO_NOT_REMEMBER); $token->setLastActivity($this->time - 60 * 60 * 24 * 3); $token->setLastCheck($this->time - 10); + $token->setVersion(DefaultToken::VERSION); $dbToken = $this->mapper->getToken($token->getToken()); $token->setId($dbToken->getId()); // We don't know the ID diff --git a/tests/lib/Authentication/Token/DefaultTokenProviderTest.php b/tests/lib/Authentication/Token/DefaultTokenProviderTest.php index 58e152457fc..3fb11f410ba 100644 --- a/tests/lib/Authentication/Token/DefaultTokenProviderTest.php +++ b/tests/lib/Authentication/Token/DefaultTokenProviderTest.php @@ -91,6 +91,7 @@ class DefaultTokenProviderTest extends TestCase { $toInsert->setRemember(IToken::DO_NOT_REMEMBER); $toInsert->setLastActivity($this->time); $toInsert->setLastCheck($this->time); + $toInsert->setVersion(DefaultToken::VERSION); $this->config->expects($this->any()) ->method('getSystemValue') diff --git a/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php b/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php index ce4de92e193..5a98747ab0d 100644 --- a/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php +++ b/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php @@ -147,6 +147,7 @@ class PublicKeyTokenMapperTest extends TestCase { $token->setLastCheck($this->time - 10); $token->setPublicKey('public key'); $token->setPrivateKey('private key'); + $token->setVersion(PublicKeyToken::VERSION); $dbToken = $this->mapper->getToken($token->getToken()); @@ -178,6 +179,7 @@ class PublicKeyTokenMapperTest extends TestCase { $token->setLastCheck($this->time - 10); $token->setPublicKey('public key'); $token->setPrivateKey('private key'); + $token->setVersion(PublicKeyToken::VERSION); $dbToken = $this->mapper->getToken($token->getToken()); $token->setId($dbToken->getId()); // We don't know the ID From 970dea926422a9d433a53b6932b792723dca3dfd Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Mon, 4 Jun 2018 10:03:49 +0200 Subject: [PATCH 12/13] Add getProvider helper function Signed-off-by: Roeland Jago Douma --- lib/private/Authentication/Token/Manager.php | 52 +++++++------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/lib/private/Authentication/Token/Manager.php b/lib/private/Authentication/Token/Manager.php index 5e30afbc92e..8dd41ce1096 100644 --- a/lib/private/Authentication/Token/Manager.php +++ b/lib/private/Authentication/Token/Manager.php @@ -77,13 +77,8 @@ class Manager implements IProvider { * @throws InvalidTokenException */ public function updateToken(IToken $token) { - if ($token instanceof DefaultToken) { - $this->defaultTokenProvider->updateToken($token); - } else if ($token instanceof PublicKeyToken) { - $this->publicKeyTokenProvider->updateToken($token); - } else { - throw new InvalidTokenException(); - } + $provider = $this->getProvider($token); + $provider->updateToken($token); } /** @@ -93,13 +88,8 @@ class Manager implements IProvider { * @param IToken $token */ public function updateTokenActivity(IToken $token) { - if ($token instanceof DefaultToken) { - $this->defaultTokenProvider->updateTokenActivity($token); - } else if ($token instanceof PublicKeyToken) { - $this->publicKeyTokenProvider->updateTokenActivity($token); - } else { - throw new InvalidTokenException(); - } + $provider = $this->getProvider($token); + $provider->updateTokenActivity($token); } public function getTokenByUser(string $uid): array { @@ -171,29 +161,13 @@ class Manager implements IProvider { * @return string */ public function getPassword(IToken $savedToken, string $tokenId): string { - if ($savedToken instanceof DefaultToken) { - return $this->defaultTokenProvider->getPassword($savedToken, $tokenId); - } - - if ($savedToken instanceof PublicKeyToken) { - return $this->publicKeyTokenProvider->getPassword($savedToken, $tokenId); - } - - throw new InvalidTokenException(); + $provider = $this->getProvider($savedToken); + return $provider->getPassword($savedToken, $tokenId); } public function setPassword(IToken $token, string $tokenId, string $password) { - if ($token instanceof DefaultToken) { - $this->defaultTokenProvider->setPassword($token, $tokenId, $password); - return; - } - - if ($token instanceof PublicKeyToken) { - $this->publicKeyTokenProvider->setPassword($token, $tokenId, $password); - return; - } - - throw new InvalidTokenException(); + $provider = $this->getProvider($token); + $provider->setPassword($token, $tokenId, $password); } public function invalidateToken(string $token) { @@ -229,5 +203,13 @@ class Manager implements IProvider { throw new InvalidTokenException(); } - + private function getProvider(IToken $token): IProvider { + if ($token instanceof DefaultToken) { + return $this->defaultTokenProvider; + } + if ($token instanceof PublicKeyToken) { + return $this->publicKeyTokenProvider; + } + throw new InvalidTokenException(); + } } From 82959ca93e229e1f16e1843cd4a2f7523b8ac0bf Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Thu, 14 Jun 2018 19:39:07 +0200 Subject: [PATCH 13/13] Comments Signed-off-by: Roeland Jago Douma --- .../Token/DefaultTokenProvider.php | 3 +-- lib/private/Authentication/Token/Manager.php | 17 ++++++++++++++++- .../Authentication/Token/PublicKeyToken.php | 3 ++- .../Token/PublicKeyTokenMapper.php | 2 +- .../Token/PublicKeyTokenProvider.php | 1 - 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/private/Authentication/Token/DefaultTokenProvider.php b/lib/private/Authentication/Token/DefaultTokenProvider.php index 078ab4ed8f2..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 { @@ -299,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/Manager.php b/lib/private/Authentication/Token/Manager.php index 8dd41ce1096..254a1598943 100644 --- a/lib/private/Authentication/Token/Manager.php +++ b/lib/private/Authentication/Token/Manager.php @@ -25,7 +25,6 @@ namespace OC\Authentication\Token; use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Exceptions\PasswordlessTokenException; -use OCP\IUser; class Manager implements IProvider { @@ -92,6 +91,10 @@ class Manager implements IProvider { $provider->updateTokenActivity($token); } + /** + * @param string $uid + * @return IToken[] + */ public function getTokenByUser(string $uid): array { $old = $this->defaultTokenProvider->getTokenByUser($uid); $new = $this->publicKeyTokenProvider->getTokenByUser($uid); @@ -185,6 +188,13 @@ class Manager implements IProvider { $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 { @@ -203,6 +213,11 @@ class Manager implements IProvider { throw new InvalidTokenException(); } + /** + * @param IToken $token + * @return IProvider + * @throws InvalidTokenException + */ private function getProvider(IToken $token): IProvider { if ($token instanceof DefaultToken) { return $this->defaultTokenProvider; diff --git a/lib/private/Authentication/Token/PublicKeyToken.php b/lib/private/Authentication/Token/PublicKeyToken.php index 9d01fc9ecca..0e793ce8c7c 100644 --- a/lib/private/Authentication/Token/PublicKeyToken.php +++ b/lib/private/Authentication/Token/PublicKeyToken.php @@ -1,4 +1,5 @@ @@ -180,7 +181,7 @@ class PublicKeyToken extends Entity implements IToken { } public function setScope($scope) { - if (\is_array($scope)) { + if (is_array($scope)) { parent::setScope(json_encode($scope)); } else { parent::setScope((string)$scope); diff --git a/lib/private/Authentication/Token/PublicKeyTokenMapper.php b/lib/private/Authentication/Token/PublicKeyTokenMapper.php index 129b2a272bb..5e5c69dbc46 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenMapper.php +++ b/lib/private/Authentication/Token/PublicKeyTokenMapper.php @@ -115,7 +115,7 @@ class PublicKeyTokenMapper extends QBMapper { * where a high number of (session) tokens is generated * * @param string $uid - * @return DefaultToken[] + * @return PublicKeyToken[] */ public function getTokenByUser(string $uid): array { /* @var $qb IQueryBuilder */ diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index b7e0d1da332..f6a6fc3455f 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -29,7 +29,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 PublicKeyTokenProvider implements IProvider {