diff options
author | blizzz <blizzz@arthur-schiwon.de> | 2018-06-19 10:11:09 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-06-19 10:11:09 +0200 |
commit | 7ef722e7a626e07f36b2c2c01d650651b1e4e126 (patch) | |
tree | 6bbff693403e6f394ced4f9eeacb62f6f5c3c86b | |
parent | b8492c694480e210aaf2c794ed18adf5a9a751b2 (diff) | |
parent | 82959ca93e229e1f16e1843cd4a2f7523b8ac0bf (diff) | |
download | nextcloud-server-7ef722e7a626e07f36b2c2c01d650651b1e4e126.tar.gz nextcloud-server-7ef722e7a626e07f36b2c2c01d650651b1e4e126.zip |
Merge pull request #9485 from nextcloud/feature/9441/multiple_token_providers
Add new public key token provider (tokens survive password change)
22 files changed, 2309 insertions, 111 deletions
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 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\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 1d319026175..77729886601 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -421,6 +421,10 @@ return array( 'OC\\Authentication\\Token\\ExpiredTokenException' => $baseDir . '/lib/private/Authentication/Exceptions/ExpiredTokenException.php', 'OC\\Authentication\\Token\\IProvider' => $baseDir . '/lib/private/Authentication/Token/IProvider.php', 'OC\\Authentication\\Token\\IToken' => $baseDir . '/lib/private/Authentication/Token/IToken.php', + 'OC\\Authentication\\Token\\Manager' => $baseDir . '/lib/private/Authentication/Token/Manager.php', + 'OC\\Authentication\\Token\\PublicKeyToken' => $baseDir . '/lib/private/Authentication/Token/PublicKeyToken.php', + 'OC\\Authentication\\Token\\PublicKeyTokenMapper' => $baseDir . '/lib/private/Authentication/Token/PublicKeyTokenMapper.php', + 'OC\\Authentication\\Token\\PublicKeyTokenProvider' => $baseDir . '/lib/private/Authentication/Token/PublicKeyTokenProvider.php', 'OC\\Authentication\\TwoFactorAuth\\Manager' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/Manager.php', 'OC\\Avatar' => $baseDir . '/lib/private/Avatar.php', 'OC\\AvatarManager' => $baseDir . '/lib/private/AvatarManager.php', @@ -570,6 +574,7 @@ return array( 'OC\\Core\\Migrations\\Version14000Date20180129121024' => $baseDir . '/core/Migrations/Version14000Date20180129121024.php', 'OC\\Core\\Migrations\\Version14000Date20180404140050' => $baseDir . '/core/Migrations/Version14000Date20180404140050.php', 'OC\\Core\\Migrations\\Version14000Date20180516101403' => $baseDir . '/core/Migrations/Version14000Date20180516101403.php', + 'OC\\Core\\Migrations\\Version14000Date20180518120534' => $baseDir . '/core/Migrations/Version14000Date20180518120534.php', 'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', 'OC\\DB\\AdapterMySQL' => $baseDir . '/lib/private/DB/AdapterMySQL.php', 'OC\\DB\\AdapterOCI8' => $baseDir . '/lib/private/DB/AdapterOCI8.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index b66a7b18192..be9c71d8246 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -451,6 +451,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Authentication\\Token\\ExpiredTokenException' => __DIR__ . '/../../..' . '/lib/private/Authentication/Exceptions/ExpiredTokenException.php', 'OC\\Authentication\\Token\\IProvider' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/IProvider.php', 'OC\\Authentication\\Token\\IToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/IToken.php', + 'OC\\Authentication\\Token\\Manager' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/Manager.php', + 'OC\\Authentication\\Token\\PublicKeyToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyToken.php', + 'OC\\Authentication\\Token\\PublicKeyTokenMapper' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyTokenMapper.php', + 'OC\\Authentication\\Token\\PublicKeyTokenProvider' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyTokenProvider.php', 'OC\\Authentication\\TwoFactorAuth\\Manager' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/Manager.php', 'OC\\Avatar' => __DIR__ . '/../../..' . '/lib/private/Avatar.php', 'OC\\AvatarManager' => __DIR__ . '/../../..' . '/lib/private/AvatarManager.php', @@ -600,6 +604,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Migrations\\Version14000Date20180129121024' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180129121024.php', 'OC\\Core\\Migrations\\Version14000Date20180404140050' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180404140050.php', 'OC\\Core\\Migrations\\Version14000Date20180516101403' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180516101403.php', + 'OC\\Core\\Migrations\\Version14000Date20180518120534' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180518120534.php', 'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', 'OC\\DB\\AdapterMySQL' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterMySQL.php', 'OC\\DB\\AdapterOCI8' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterOCI8.php', diff --git a/lib/private/Authentication/Token/DefaultToken.php b/lib/private/Authentication/Token/DefaultToken.php index 67aa89ea66b..85ea0dc4cdd 100644 --- a/lib/private/Authentication/Token/DefaultToken.php +++ b/lib/private/Authentication/Token/DefaultToken.php @@ -37,9 +37,12 @@ use OCP\AppFramework\Db\Entity; * @method void setRemember(int $remember) * @method void setLastActivity(int $lastactivity) * @method int getLastActivity() + * @method void setVersion(int $version) */ class DefaultToken extends Entity implements IToken { + const VERSION = 1; + /** @var string user UID */ protected $uid; @@ -73,6 +76,9 @@ class DefaultToken extends Entity implements IToken { /** @var int */ protected $expires; + /** @var int */ + protected $version; + public function __construct() { $this->addType('uid', 'string'); $this->addType('loginName', 'string'); @@ -85,6 +91,7 @@ class DefaultToken extends Entity implements IToken { $this->addType('lastCheck', 'int'); $this->addType('scope', 'string'); $this->addType('expires', 'int'); + $this->addType('version', 'int'); } public function getId(): int { diff --git a/lib/private/Authentication/Token/DefaultTokenMapper.php b/lib/private/Authentication/Token/DefaultTokenMapper.php index a67d7d151e9..b8df00ff094 100644 --- a/lib/private/Authentication/Token/DefaultTokenMapper.php +++ b/lib/private/Authentication/Token/DefaultTokenMapper.php @@ -33,7 +33,6 @@ use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; -use OCP\IUser; class DefaultTokenMapper extends QBMapper { @@ -50,8 +49,8 @@ class DefaultTokenMapper extends QBMapper { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') - ->where($qb->expr()->eq('token', $qb->createParameter('token'))) - ->setParameter('token', $token) + ->where($qb->expr()->eq('token', $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); } @@ -66,6 +65,7 @@ class DefaultTokenMapper extends QBMapper { ->where($qb->expr()->lt('last_activity', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); } @@ -79,9 +79,10 @@ class DefaultTokenMapper extends QBMapper { public function getToken(string $token): DefaultToken { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $result = $qb->select('*') + $result = $qb->select('id', 'uid', 'login_name', 'password', 'name', 'token', 'type', 'remember', 'last_activity', 'last_check', 'scope', 'expires', 'version') ->from('authtoken') ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); $data = $result->fetch(); @@ -102,9 +103,10 @@ class DefaultTokenMapper extends QBMapper { public function getTokenById(int $id): DefaultToken { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $result = $qb->select('*') + $result = $qb->select('id', 'uid', 'login_name', 'password', 'name', 'token', 'type', 'remember', 'last_activity', 'last_check', 'scope', 'expires', 'version') ->from('authtoken') ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))) ->execute(); $data = $result->fetch(); @@ -121,15 +123,16 @@ class DefaultTokenMapper extends QBMapper { * The provider may limit the number of result rows in case of an abuse * where a high number of (session) tokens is generated * - * @param IUser $user + * @param string $uid * @return DefaultToken[] */ - public function getTokenByUser(IUser $user): array { + public function getTokenByUser(string $uid): array { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->select('*') + $qb->select('id', 'uid', 'login_name', 'password', 'name', 'token', 'type', 'remember', 'last_activity', 'last_check', 'scope', 'expires', 'version') ->from('authtoken') - ->where($qb->expr()->eq('uid', $qb->createNamedParameter($user->getUID()))) + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))) ->setMaxResults(1000); $result = $qb->execute(); $data = $result->fetchAll(); @@ -142,16 +145,13 @@ class DefaultTokenMapper extends QBMapper { return $entities; } - /** - * @param IUser $user - * @param int $id - */ - public function deleteById(IUser $user, int $id) { + public function deleteById(string $uid, int $id) { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) - ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($user->getUID()))); + ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))); $qb->execute(); } @@ -163,7 +163,8 @@ class DefaultTokenMapper extends QBMapper { public function deleteByName(string $name) { $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') - ->where($qb->expr()->eq('name', $qb->createNamedParameter($name), IQueryBuilder::PARAM_STR)); + ->where($qb->expr()->eq('name', $qb->createNamedParameter($name), IQueryBuilder::PARAM_STR)) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(DefaultToken::VERSION, IQueryBuilder::PARAM_INT))); $qb->execute(); } diff --git a/lib/private/Authentication/Token/DefaultTokenProvider.php b/lib/private/Authentication/Token/DefaultTokenProvider.php index 7a43dbb23e1..8c2d8c33a97 100644 --- a/lib/private/Authentication/Token/DefaultTokenProvider.php +++ b/lib/private/Authentication/Token/DefaultTokenProvider.php @@ -35,7 +35,6 @@ use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; use OCP\ILogger; -use OCP\IUser; use OCP\Security\ICrypto; class DefaultTokenProvider implements IProvider { @@ -105,6 +104,7 @@ class DefaultTokenProvider implements IProvider { $dbToken->setRemember($remember); $dbToken->setLastActivity($this->time->getTime()); $dbToken->setLastCheck($this->time->getTime()); + $dbToken->setVersion(DefaultToken::VERSION); $this->mapper->insert($dbToken); @@ -143,17 +143,8 @@ class DefaultTokenProvider implements IProvider { } } - /** - * Get all tokens of a user - * - * The provider may limit the number of result rows in case of an abuse - * where a high number of (session) tokens is generated - * - * @param IUser $user - * @return IToken[] - */ - public function getTokenByUser(IUser $user): array { - return $this->mapper->getTokenByUser($user); + public function getTokenByUser(string $uid): array { + return $this->mapper->getTokenByUser($uid); } /** @@ -265,14 +256,8 @@ class DefaultTokenProvider implements IProvider { $this->mapper->invalidate($this->hashToken($token)); } - /** - * Invalidate (delete) the given token - * - * @param IUser $user - * @param int $id - */ - public function invalidateTokenById(IUser $user, int $id) { - $this->mapper->deleteById($user, $id); + public function invalidateTokenById(string $uid, int $id) { + $this->mapper->deleteById($uid, $id); } /** @@ -313,7 +298,7 @@ class DefaultTokenProvider implements IProvider { * @param string $token * @return string */ - private function hashToken(string $token) { + private function hashToken(string $token): string { $secret = $this->config->getSystemValue('secret'); return hash('sha512', $token . $secret); } diff --git a/lib/private/Authentication/Token/IProvider.php b/lib/private/Authentication/Token/IProvider.php index 0efffefac68..ab46bd12126 100644 --- a/lib/private/Authentication/Token/IProvider.php +++ b/lib/private/Authentication/Token/IProvider.php @@ -28,7 +28,6 @@ namespace OC\Authentication\Token; use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Exceptions\PasswordlessTokenException; -use OCP\IUser; interface IProvider { @@ -92,10 +91,10 @@ interface IProvider { /** * Invalidate (delete) the given token * - * @param IUser $user + * @param string $uid * @param int $id */ - public function invalidateTokenById(IUser $user, int $id); + public function invalidateTokenById(string $uid, int $id); /** * Invalidate (delete) old session tokens @@ -122,10 +121,10 @@ interface IProvider { * The provider may limit the number of result rows in case of an abuse * where a high number of (session) tokens is generated * - * @param IUser $user + * @param string $uid * @return IToken[] */ - public function getTokenByUser(IUser $user): array; + public function getTokenByUser(string $uid): array; /** * Get the (unencrypted) password of the given token diff --git a/lib/private/Authentication/Token/Manager.php b/lib/private/Authentication/Token/Manager.php new file mode 100644 index 00000000000..254a1598943 --- /dev/null +++ b/lib/private/Authentication/Token/Manager.php @@ -0,0 +1,230 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Authentication\Token; + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Exceptions\PasswordlessTokenException; + +class Manager implements IProvider { + + /** @var DefaultTokenProvider */ + private $defaultTokenProvider; + + /** @var PublicKeyTokenProvider */ + private $publicKeyTokenProvider; + + public function __construct(DefaultTokenProvider $defaultTokenProvider, PublicKeyTokenProvider $publicKeyTokenProvider) { + $this->defaultTokenProvider = $defaultTokenProvider; + $this->publicKeyTokenProvider = $publicKeyTokenProvider; + } + + /** + * Create and persist a new token + * + * @param string $token + * @param string $uid + * @param string $loginName + * @param string|null $password + * @param string $name + * @param int $type token type + * @param int $remember whether the session token should be used for remember-me + * @return IToken + */ + public function generateToken(string $token, + string $uid, + string $loginName, + $password, + string $name, + int $type = IToken::TEMPORARY_TOKEN, + int $remember = IToken::DO_NOT_REMEMBER): IToken { + return $this->publicKeyTokenProvider->generateToken( + $token, + $uid, + $loginName, + $password, + $name, + $type, + $remember + ); + } + + /** + * Save the updated token + * + * @param IToken $token + * @throws InvalidTokenException + */ + public function updateToken(IToken $token) { + $provider = $this->getProvider($token); + $provider->updateToken($token); + } + + /** + * Update token activity timestamp + * + * @throws InvalidTokenException + * @param IToken $token + */ + public function updateTokenActivity(IToken $token) { + $provider = $this->getProvider($token); + $provider->updateTokenActivity($token); + } + + /** + * @param string $uid + * @return IToken[] + */ + public function getTokenByUser(string $uid): array { + $old = $this->defaultTokenProvider->getTokenByUser($uid); + $new = $this->publicKeyTokenProvider->getTokenByUser($uid); + + return array_merge($old, $new); + } + + /** + * Get a token by token + * + * @param string $tokenId + * @throws InvalidTokenException + * @return IToken + */ + public function getToken(string $tokenId): IToken { + try { + return $this->publicKeyTokenProvider->getToken($tokenId); + } catch (InvalidTokenException $e) { + // No worries we try to convert it to a PublicKey Token + } + + //Convert! + $token = $this->defaultTokenProvider->getToken($tokenId); + + try { + $password = $this->defaultTokenProvider->getPassword($token, $tokenId); + } catch (PasswordlessTokenException $e) { + $password = null; + } + + return $this->publicKeyTokenProvider->convertToken($token, $tokenId, $password); + } + + /** + * Get a token by token id + * + * @param int $tokenId + * @throws InvalidTokenException + * @return IToken + */ + public function getTokenById(int $tokenId): IToken { + try { + return $this->publicKeyTokenProvider->getTokenById($tokenId); + } catch (InvalidTokenException $e) { + return $this->defaultTokenProvider->getTokenById($tokenId); + } + } + + /** + * @param string $oldSessionId + * @param string $sessionId + * @throws InvalidTokenException + */ + public function renewSessionToken(string $oldSessionId, string $sessionId) { + try { + $this->publicKeyTokenProvider->renewSessionToken($oldSessionId, $sessionId); + } catch (InvalidTokenException $e) { + $this->defaultTokenProvider->renewSessionToken($oldSessionId, $sessionId); + } + } + + /** + * @param IToken $savedToken + * @param string $tokenId session token + * @throws InvalidTokenException + * @throws PasswordlessTokenException + * @return string + */ + public function getPassword(IToken $savedToken, string $tokenId): string { + $provider = $this->getProvider($savedToken); + return $provider->getPassword($savedToken, $tokenId); + } + + public function setPassword(IToken $token, string $tokenId, string $password) { + $provider = $this->getProvider($token); + $provider->setPassword($token, $tokenId, $password); + } + + public function invalidateToken(string $token) { + $this->defaultTokenProvider->invalidateToken($token); + $this->publicKeyTokenProvider->invalidateToken($token); + } + + public function invalidateTokenById(string $uid, int $id) { + $this->defaultTokenProvider->invalidateTokenById($uid, $id); + $this->publicKeyTokenProvider->invalidateTokenById($uid, $id); + } + + public function invalidateOldTokens() { + $this->defaultTokenProvider->invalidateOldTokens(); + $this->publicKeyTokenProvider->invalidateOldTokens(); + } + + /** + * @param IToken $token + * @param string $oldTokenId + * @param string $newTokenId + * @return IToken + * @throws InvalidTokenException + */ + public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken { + if ($token instanceof DefaultToken) { + try { + $password = $this->defaultTokenProvider->getPassword($token, $oldTokenId); + } catch (PasswordlessTokenException $e) { + $password = null; + } + + return $this->publicKeyTokenProvider->convertToken($token, $newTokenId, $password); + } + + if ($token instanceof PublicKeyToken) { + return $this->publicKeyTokenProvider->rotate($token, $oldTokenId, $newTokenId); + } + + throw new InvalidTokenException(); + } + + /** + * @param IToken $token + * @return IProvider + * @throws InvalidTokenException + */ + private function getProvider(IToken $token): IProvider { + if ($token instanceof DefaultToken) { + return $this->defaultTokenProvider; + } + if ($token instanceof PublicKeyToken) { + return $this->publicKeyTokenProvider; + } + throw new InvalidTokenException(); + } +} diff --git a/lib/private/Authentication/Token/PublicKeyToken.php b/lib/private/Authentication/Token/PublicKeyToken.php new file mode 100644 index 00000000000..0e793ce8c7c --- /dev/null +++ b/lib/private/Authentication/Token/PublicKeyToken.php @@ -0,0 +1,217 @@ +<?php +/** @noinspection ALL */ +declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Authentication\Token; + +use OCP\AppFramework\Db\Entity; + +/** + * @method void setId(int $id) + * @method void setUid(string $uid); + * @method void setLoginName(string $loginname) + * @method void setName(string $name) + * @method string getToken() + * @method void setType(int $type) + * @method int getType() + * @method void setRemember(int $remember) + * @method void setLastActivity(int $lastactivity) + * @method int getLastActivity() + * @method string getPrivateKey() + * @method void setPrivateKey(string $key) + * @method string getPublicKey() + * @method void setPublicKey(string $key) + * @method void setVersion(int $version) + */ +class PublicKeyToken extends Entity implements IToken { + + const VERSION = 2; + + /** @var string user UID */ + protected $uid; + + /** @var string login name used for generating the token */ + protected $loginName; + + /** @var string encrypted user password */ + protected $password; + + /** @var string token name (e.g. browser/OS) */ + protected $name; + + /** @var string */ + protected $token; + + /** @var int */ + protected $type; + + /** @var int */ + protected $remember; + + /** @var int */ + protected $lastActivity; + + /** @var int */ + protected $lastCheck; + + /** @var string */ + protected $scope; + + /** @var int */ + protected $expires; + + /** @var string */ + protected $privateKey; + + /** @var string */ + protected $publicKey; + + /** @var int */ + protected $version; + + public function __construct() { + $this->addType('uid', 'string'); + $this->addType('loginName', 'string'); + $this->addType('password', 'string'); + $this->addType('name', 'string'); + $this->addType('token', 'string'); + $this->addType('type', 'int'); + $this->addType('remember', 'int'); + $this->addType('lastActivity', 'int'); + $this->addType('lastCheck', 'int'); + $this->addType('scope', 'string'); + $this->addType('expires', 'int'); + $this->addType('publicKey', 'string'); + $this->addType('privateKey', 'string'); + $this->addType('version', 'int'); + } + + public function getId(): int { + return $this->id; + } + + public function getUID(): string { + return $this->uid; + } + + /** + * Get the login name used when generating the token + * + * @return string + */ + public function getLoginName(): string { + return parent::getLoginName(); + } + + /** + * Get the (encrypted) login password + * + * @return string|null + */ + public function getPassword() { + return parent::getPassword(); + } + + public function jsonSerialize() { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'lastActivity' => $this->lastActivity, + 'type' => $this->type, + 'scope' => $this->getScopeAsArray() + ]; + } + + /** + * Get the timestamp of the last password check + * + * @return int + */ + public function getLastCheck(): int { + return parent::getLastCheck(); + } + + /** + * Get the timestamp of the last password check + * + * @param int $time + */ + public function setLastCheck(int $time) { + parent::setLastCheck($time); + } + + public function getScope(): string { + $scope = parent::getScope(); + if ($scope === null) { + return ''; + } + + return $scope; + } + + public function getScopeAsArray(): array { + $scope = json_decode($this->getScope(), true); + if (!$scope) { + return [ + 'filesystem'=> true + ]; + } + return $scope; + } + + public function setScope($scope) { + if (is_array($scope)) { + parent::setScope(json_encode($scope)); + } else { + parent::setScope((string)$scope); + } + } + + public function getName(): string { + return parent::getName(); + } + + public function getRemember(): int { + return parent::getRemember(); + } + + public function setToken(string $token) { + parent::setToken($token); + } + + public function setPassword(string $password = null) { + parent::setPassword($password); + } + + public function setExpires($expires) { + parent::setExpires($expires); + } + + /** + * @return int|null + */ + public function getExpires() { + return parent::getExpires(); + } +} diff --git a/lib/private/Authentication/Token/PublicKeyTokenMapper.php b/lib/private/Authentication/Token/PublicKeyTokenMapper.php new file mode 100644 index 00000000000..5e5c69dbc46 --- /dev/null +++ b/lib/private/Authentication/Token/PublicKeyTokenMapper.php @@ -0,0 +1,172 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Authentication\Token; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class PublicKeyTokenMapper extends QBMapper { + + public function __construct(IDBConnection $db) { + parent::__construct($db, 'authtoken'); + } + + /** + * Invalidate (delete) a given token + * + * @param string $token + */ + public function invalidate(string $token) { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->delete('authtoken') + ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) + ->execute(); + } + + /** + * @param int $olderThan + * @param int $remember + */ + public function invalidateOld(int $olderThan, int $remember = IToken::DO_NOT_REMEMBER) { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->delete('authtoken') + ->where($qb->expr()->lt('last_activity', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) + ->execute(); + } + + /** + * Get the user UID for the given token + * + * @throws DoesNotExistException + */ + public function getToken(string $token): PublicKeyToken { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $result = $qb->select('*') + ->from('authtoken') + ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) + ->execute(); + + $data = $result->fetch(); + $result->closeCursor(); + if ($data === false) { + throw new DoesNotExistException('token does not exist'); + } + return PublicKeyToken::fromRow($data); + } + + /** + * Get the token for $id + * + * @throws DoesNotExistException + */ + public function getTokenById(int $id): PublicKeyToken { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $result = $qb->select('*') + ->from('authtoken') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) + ->execute(); + + $data = $result->fetch(); + $result->closeCursor(); + if ($data === false) { + throw new DoesNotExistException('token does not exist'); + } + return PublicKeyToken::fromRow($data); + } + + /** + * Get all tokens of a user + * + * The provider may limit the number of result rows in case of an abuse + * where a high number of (session) tokens is generated + * + * @param string $uid + * @return PublicKeyToken[] + */ + public function getTokenByUser(string $uid): array { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('authtoken') + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) + ->setMaxResults(1000); + $result = $qb->execute(); + $data = $result->fetchAll(); + $result->closeCursor(); + + $entities = array_map(function ($row) { + return PublicKeyToken::fromRow($row); + }, $data); + + return $entities; + } + + public function deleteById(string $uid, int $id) { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->delete('authtoken') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); + $qb->execute(); + } + + /** + * delete all auth token which belong to a specific client if the client was deleted + * + * @param string $name + */ + public function deleteByName(string $name) { + $qb = $this->db->getQueryBuilder(); + $qb->delete('authtoken') + ->where($qb->expr()->eq('name', $qb->createNamedParameter($name), IQueryBuilder::PARAM_STR)) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); + $qb->execute(); + } + + public function deleteTempToken(PublicKeyToken $except) { + $qb = $this->db->getQueryBuilder(); + + $qb->delete('authtoken') + ->where($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN))) + ->andWhere($qb->expr()->neq('id', $qb->createNamedParameter($except->getId()))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); + + $qb->execute(); + } +} diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php new file mode 100644 index 00000000000..f6a6fc3455f --- /dev/null +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -0,0 +1,320 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Authentication\Token; + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Exceptions\PasswordlessTokenException; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\ILogger; +use OCP\Security\ICrypto; + +class PublicKeyTokenProvider implements IProvider { + /** @var PublicKeyTokenMapper */ + private $mapper; + + /** @var ICrypto */ + private $crypto; + + /** @var IConfig */ + private $config; + + /** @var ILogger $logger */ + private $logger; + + /** @var ITimeFactory $time */ + private $time; + + public function __construct(PublicKeyTokenMapper $mapper, + ICrypto $crypto, + IConfig $config, + ILogger $logger, + ITimeFactory $time) { + $this->mapper = $mapper; + $this->crypto = $crypto; + $this->config = $config; + $this->logger = $logger; + $this->time = $time; + } + + public function generateToken(string $token, + string $uid, + string $loginName, + $password, + string $name, + int $type = IToken::TEMPORARY_TOKEN, + int $remember = IToken::DO_NOT_REMEMBER): IToken { + $dbToken = $this->newToken($token, $uid, $loginName, $password, $name, $type, $remember); + + $this->mapper->insert($dbToken); + + return $dbToken; + } + + public function getToken(string $tokenId): IToken { + try { + $token = $this->mapper->getToken($this->hashToken($tokenId)); + } catch (DoesNotExistException $ex) { + throw new InvalidTokenException(); + } + + if ($token->getExpires() !== null && $token->getExpires() < $this->time->getTime()) { + throw new ExpiredTokenException($token); + } + + return $token; + } + + public function getTokenById(int $tokenId): IToken { + try { + $token = $this->mapper->getTokenById($tokenId); + } catch (DoesNotExistException $ex) { + throw new InvalidTokenException(); + } + + if ($token->getExpires() !== null && $token->getExpires() < $this->time->getTime()) { + throw new ExpiredTokenException($token); + } + + return $token; + } + + public function renewSessionToken(string $oldSessionId, string $sessionId) { + $token = $this->getToken($oldSessionId); + + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + + $password = null; + if (!is_null($token->getPassword())) { + $privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId); + $password = $this->decryptPassword($token->getPassword(), $privateKey); + } + + $this->generateToken( + $sessionId, + $token->getUID(), + $token->getLoginName(), + $password, + $token->getName(), + IToken::TEMPORARY_TOKEN, + $token->getRemember() + ); + + $this->mapper->delete($token); + } + + public function invalidateToken(string $token) { + $this->mapper->invalidate($this->hashToken($token)); + } + + public function invalidateTokenById(string $uid, int $id) { + $this->mapper->deleteById($uid, $id); + } + + public function invalidateOldTokens() { + $olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24); + $this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']); + $this->mapper->invalidateOld($olderThan, IToken::DO_NOT_REMEMBER); + $rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); + $this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']); + $this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER); + } + + public function updateToken(IToken $token) { + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + $this->mapper->update($token); + } + + public function updateTokenActivity(IToken $token) { + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + /** @var DefaultToken $token */ + $now = $this->time->getTime(); + if ($token->getLastActivity() < ($now - 60)) { + // Update token only once per minute + $token->setLastActivity($now); + $this->mapper->update($token); + } + } + + public function getTokenByUser(string $uid): array { + return $this->mapper->getTokenByUser($uid); + } + + public function getPassword(IToken $token, string $tokenId): string { + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + + if ($token->getPassword() === null) { + throw new PasswordlessTokenException(); + } + + // Decrypt private key with tokenId + $privateKey = $this->decrypt($token->getPrivateKey(), $tokenId); + + // Decrypt password with private key + return $this->decryptPassword($token->getPassword(), $privateKey); + } + + public function setPassword(IToken $token, string $tokenId, string $password) { + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + + // When changing passwords all temp tokens are deleted + $this->mapper->deleteTempToken($token); + + // Update the password for all tokens + $tokens = $this->mapper->getTokenByUser($token->getUID()); + foreach ($tokens as $t) { + $publicKey = $t->getPublicKey(); + $t->setPassword($this->encryptPassword($password, $publicKey)); + $this->updateToken($t); + } + } + + public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken { + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + + // Decrypt private key with oldTokenId + $privateKey = $this->decrypt($token->getPrivateKey(), $oldTokenId); + // Encrypt with the new token + $token->setPrivateKey($this->encrypt($privateKey, $newTokenId)); + + $token->setToken($this->hashToken($newTokenId)); + $this->updateToken($token); + + return $token; + } + + private function encrypt(string $plaintext, string $token): string { + $secret = $this->config->getSystemValue('secret'); + return $this->crypto->encrypt($plaintext, $token . $secret); + } + + /** + * @throws InvalidTokenException + */ + private function decrypt(string $cipherText, string $token): string { + $secret = $this->config->getSystemValue('secret'); + try { + return $this->crypto->decrypt($cipherText, $token . $secret); + } catch (\Exception $ex) { + // Delete the invalid token + $this->invalidateToken($token); + throw new InvalidTokenException(); + } + } + + private function encryptPassword(string $password, string $publicKey): string { + openssl_public_encrypt($password, $encryptedPassword, $publicKey, OPENSSL_PKCS1_OAEP_PADDING); + $encryptedPassword = base64_encode($encryptedPassword); + + return $encryptedPassword; + } + + private function decryptPassword(string $encryptedPassword, string $privateKey): string { + $encryptedPassword = base64_decode($encryptedPassword); + openssl_private_decrypt($encryptedPassword, $password, $privateKey, OPENSSL_PKCS1_OAEP_PADDING); + + return $password; + } + + private function hashToken(string $token): string { + $secret = $this->config->getSystemValue('secret'); + return hash('sha512', $token . $secret); + } + + /** + * Convert a DefaultToken to a publicKeyToken + * This will also be updated directly in the Database + */ + public function convertToken(DefaultToken $defaultToken, string $token, $password): PublicKeyToken { + $pkToken = $this->newToken( + $token, + $defaultToken->getUID(), + $defaultToken->getLoginName(), + $password, + $defaultToken->getName(), + $defaultToken->getType(), + $defaultToken->getRemember() + ); + + $pkToken->setExpires($defaultToken->getExpires()); + $pkToken->setId($defaultToken->getId()); + + return $this->mapper->update($pkToken); + } + + private function newToken(string $token, + string $uid, + string $loginName, + $password, + string $name, + int $type, + int $remember): PublicKeyToken { + $dbToken = new PublicKeyToken(); + $dbToken->setUid($uid); + $dbToken->setLoginName($loginName); + + $config = [ + 'digest_alg' => 'sha512', + 'private_key_bits' => 2048, + ]; + + // Generate new key + $res = openssl_pkey_new($config); + openssl_pkey_export($res, $privateKey); + + // Extract the public key from $res to $pubKey + $publicKey = openssl_pkey_get_details($res); + $publicKey = $publicKey['key']; + + $dbToken->setPublicKey($publicKey); + $dbToken->setPrivateKey($this->encrypt($privateKey, $token)); + + if (!is_null($password)) { + $dbToken->setPassword($this->encryptPassword($password, $publicKey)); + } + + $dbToken->setName($name); + $dbToken->setToken($this->hashToken($token)); + $dbToken->setType($type); + $dbToken->setRemember($remember); + $dbToken->setLastActivity($this->time->getTime()); + $dbToken->setLastCheck($this->time->getTime()); + $dbToken->setVersion(PublicKeyToken::VERSION); + + return $dbToken; + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index d1818c287e1..31f088ea718 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -339,15 +339,7 @@ class Server extends ServerContainer implements IServerContainer { $dbConnection = $c->getDatabaseConnection(); return new Authentication\Token\DefaultTokenMapper($dbConnection); }); - $this->registerService(Authentication\Token\DefaultTokenProvider::class, function (Server $c) { - $mapper = $c->query(Authentication\Token\DefaultTokenMapper::class); - $crypto = $c->getCrypto(); - $config = $c->getConfig(); - $logger = $c->getLogger(); - $timeFactory = new TimeFactory(); - return new \OC\Authentication\Token\DefaultTokenProvider($mapper, $crypto, $config, $logger, $timeFactory); - }); - $this->registerAlias(IProvider::class, Authentication\Token\DefaultTokenProvider::class); + $this->registerAlias(IProvider::class, Authentication\Token\Manager::class); $this->registerService(\OCP\IUserSession::class, function (Server $c) { $manager = $c->getUserManager(); 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/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/DefaultTokenMapperTest.php b/tests/lib/Authentication/Token/DefaultTokenMapperTest.php index b5d24a7ab5e..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 @@ -190,23 +192,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 +208,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()); } diff --git a/tests/lib/Authentication/Token/DefaultTokenProviderTest.php b/tests/lib/Authentication/Token/DefaultTokenProviderTest.php index 95b5b928559..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') @@ -132,13 +133,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 +243,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/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 @@ +<?php +/** + * @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace 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 new file mode 100644 index 00000000000..5a98747ab0d --- /dev/null +++ b/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php @@ -0,0 +1,250 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace 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'); + $token->setVersion(PublicKeyToken::VERSION); + + $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'); + $token->setVersion(PublicKeyToken::VERSION); + + $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() { + $this->assertCount(2, $this->mapper->getTokenByUser('user1')); + } + + public function testGetTokenByUserNotFound() { + $this->assertCount(0, $this->mapper->getTokenByUser('user1000')); + } + + 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']; + + $this->mapper->deleteById('user1', (int)$id); + $this->assertEquals(2, $this->getNumberOfTokens()); + } + + public function testDeleteByIdWrongUser() { + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->createMock(IUser::class); + $id = 33; + + $this->mapper->deleteById('user1000', $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..cd3bcb81ba6 --- /dev/null +++ b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php @@ -0,0 +1,506 @@ +<?php +/** + * @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace 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; +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() { + $this->mapper->expects($this->once()) + ->method('getTokenByUser') + ->with('uid') + ->will($this->returnValue(['token'])); + + $this->assertEquals(['token'], $this->tokenProvider->getTokenByUser('uid')); + } + + 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); + + $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')); + } + + /** + * @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; + + $this->mapper->expects($this->once()) + ->method('deleteById') + ->with('uid', $id); + + $this->tokenProvider->invalidateTokenById('uid', $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 DoesNotExistException('nope')); + + $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()); + } + + 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()); + } +} 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 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace 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()); + } +} 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'; |