From 1f17010e0b4099b41cc72f53e18f4d162ce2e3da Mon Sep 17 00:00:00 2001
From: Roeland Jago Douma <roeland@famdouma.nl>
Date: Tue, 29 May 2018 09:24:20 +0200
Subject: [PATCH] Add first tests

Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
---
 .../Token/PublicKeyTokenProvider.php          |  11 +-
 .../Token/PublicKeyTokenMapperTest.php        | 266 ++++++++++
 .../Token/PublicKeyTokenProviderTest.php      | 465 ++++++++++++++++++
 .../Token/PublicKeyTokenTest.php              |  44 ++
 4 files changed, 785 insertions(+), 1 deletion(-)
 create mode 100644 tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php
 create mode 100644 tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php
 create mode 100644 tests/lib/Authentication/Token/PublicKeyTokenTest.php

diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php
index d7e9038a076..1c5f3da147f 100644
--- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php
+++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php
@@ -134,9 +134,14 @@ class PublicKeyTokenProvider implements IProvider {
 	public function renewSessionToken(string $oldSessionId, string $sessionId) {
 		$token = $this->getToken($oldSessionId);
 
+		if (!($token instanceof PublicKeyToken)) {
+			throw new InvalidTokenException();
+		}
+
 		$password = null;
 		if (!is_null($token->getPassword())) {
-			$password = $this->decryptPassword($token->getPassword(), $oldSessionId);
+			$privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId);
+			$password = $this->decryptPassword($token->getPassword(), $privateKey);
 		}
 
 		$this->generateToken(
@@ -198,6 +203,10 @@ class PublicKeyTokenProvider implements IProvider {
 			throw new InvalidTokenException();
 		}
 
+		if ($token->getPassword() === null) {
+			throw new PasswordlessTokenException();
+		}
+
 		// Decrypt private key with tokenId
 		$privateKey = $this->decrypt($token->getPrivateKey(), $tokenId);
 
diff --git a/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php b/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php
new file mode 100644
index 00000000000..7c4dbabad6c
--- /dev/null
+++ b/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php
@@ -0,0 +1,266 @@
+<?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');
+
+		$dbToken = $this->mapper->getToken($token->getToken());
+
+		$token->setId($dbToken->getId()); // We don't know the ID
+		$token->resetUpdatedFields();
+
+		$this->assertEquals($token, $dbToken);
+	}
+
+	/**
+	 * @expectedException \OCP\AppFramework\Db\DoesNotExistException
+	 */
+	public function testGetInvalidToken() {
+		$token = 'thisisaninvalidtokenthatisnotinthedatabase';
+
+		$this->mapper->getToken($token);
+	}
+
+	public function testGetTokenById() {
+		$token = new PublicKeyToken();
+		$token->setUid('user2');
+		$token->setLoginName('User2');
+		$token->setPassword('971a337057853344700bbeccf836519f|UwOQwyb34sJHtqPV|036d4890f8c21d17bbc7b88072d8ef049a5c832a38e97f3e3d5f9186e896c2593aee16883f617322fa242728d0236ff32d163caeb4bd45e14ca002c57a88665f');
+		$token->setName('Firefox on Android');
+		$token->setToken('1504445f1524fc801035448a95681a9378ba2e83930c814546c56e5d6ebde221198792fd900c88ed5ead0555780dad1ebce3370d7e154941cd5de87eb419899b');
+		$token->setType(IToken::TEMPORARY_TOKEN);
+		$token->setRemember(IToken::DO_NOT_REMEMBER);
+		$token->setLastActivity($this->time - 60 * 60 * 24 * 3);
+		$token->setLastCheck($this->time - 10);
+		$token->setPublicKey('public key');
+		$token->setPrivateKey('private key');
+
+		$dbToken = $this->mapper->getToken($token->getToken());
+		$token->setId($dbToken->getId()); // We don't know the ID
+		$token->resetUpdatedFields();
+
+		$dbToken = $this->mapper->getTokenById($token->getId());
+		$this->assertEquals($token, $dbToken);
+	}
+
+	/**
+	 * @expectedException \OCP\AppFramework\Db\DoesNotExistException
+	 */
+	public function testGetTokenByIdNotFound() {
+		$this->mapper->getTokenById(-1);
+	}
+
+	/**
+	 * @expectedException \OCP\AppFramework\Db\DoesNotExistException
+	 */
+	public function testGetInvalidTokenById() {
+		$id = '42';
+
+		$this->mapper->getToken($id);
+	}
+
+	public function testGetTokenByUser() {
+		/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
+		$user = $this->createMock(IUser::class);
+		$user->expects($this->once())
+			->method('getUID')
+			->will($this->returnValue('user1'));
+
+		$this->assertCount(2, $this->mapper->getTokenByUser($user));
+	}
+
+	public function testGetTokenByUserNotFound() {
+		/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
+		$user = $this->createMock(IUser::class);
+		$user->expects($this->once())
+			->method('getUID')
+			->will($this->returnValue('user1000'));
+
+		$this->assertCount(0, $this->mapper->getTokenByUser($user));
+	}
+
+	public function testDeleteById() {
+		/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
+		$user = $this->createMock(IUser::class);
+		$qb = $this->dbConnection->getQueryBuilder();
+		$qb->select('id')
+			->from('authtoken')
+			->where($qb->expr()->eq('token', $qb->createNamedParameter('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206')));
+		$result = $qb->execute();
+		$id = $result->fetch()['id'];
+		$user->expects($this->once())
+			->method('getUID')
+			->will($this->returnValue('user1'));
+
+		$this->mapper->deleteById($user, (int)$id);
+		$this->assertEquals(2, $this->getNumberOfTokens());
+	}
+
+	public function testDeleteByIdWrongUser() {
+		/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
+		$user = $this->createMock(IUser::class);
+		$id = 33;
+		$user->expects($this->once())
+			->method('getUID')
+			->will($this->returnValue('user10000'));
+
+		$this->mapper->deleteById($user, $id);
+		$this->assertEquals(3, $this->getNumberOfTokens());
+	}
+
+	public function testDeleteByName() {
+		$qb = $this->dbConnection->getQueryBuilder();
+		$qb->select('name')
+			->from('authtoken')
+			->where($qb->expr()->eq('token', $qb->createNamedParameter('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206')));
+		$result = $qb->execute();
+		$name = $result->fetch()['name'];
+		$this->mapper->deleteByName($name);
+		$this->assertEquals(2, $this->getNumberOfTokens());
+	}
+
+}
diff --git a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php
new file mode 100644
index 00000000000..4901001db99
--- /dev/null
+++ b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php
@@ -0,0 +1,465 @@
+<?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\PublicKeyToken;
+use OC\Authentication\Token\PublicKeyTokenMapper;
+use OC\Authentication\Token\PublicKeyTokenProvider;
+use OC\Authentication\Token\ExpiredTokenException;
+use OC\Authentication\Token\IToken;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
+use OCP\ILogger;
+use OCP\IUser;
+use OCP\Security\ICrypto;
+use Test\TestCase;
+
+class PublicKeyTokenProviderTest extends TestCase {
+
+	/** @var PublicKeyTokenProvider|\PHPUnit_Framework_MockObject_MockObject */
+	private $tokenProvider;
+	/** @var PublicKeyTokenMapper|\PHPUnit_Framework_MockObject_MockObject */
+	private $mapper;
+	/** @var ICrypto */
+	private $crypto;
+	/** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */
+	private $config;
+	/** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */
+	private $logger;
+	/** @var ITimeFactory|\PHPUnit_Framework_MockObject_MockObject */
+	private $timeFactory;
+	/** @var int */
+	private $time;
+
+	protected function setUp() {
+		parent::setUp();
+
+		$this->mapper = $this->createMock(PublicKeyTokenMapper::class);
+		$this->crypto = \OC::$server->getCrypto();
+		$this->config = $this->createMock(IConfig::class);
+		$this->config->method('getSystemValue')
+			->will($this->returnValueMap([
+				['session_lifetime', 60 * 60 * 24, 150],
+				['remember_login_cookie_lifetime', 60 * 60 * 24 * 15, 300],
+				['secret', '', '1f4h9s'],
+			]));
+		$this->logger = $this->createMock(ILogger::class);
+		$this->timeFactory = $this->createMock(ITimeFactory::class);
+		$this->time = 1313131;
+		$this->timeFactory->method('getTime')
+			->willReturn($this->time);
+
+		$this->tokenProvider = new PublicKeyTokenProvider($this->mapper, $this->crypto, $this->config, $this->logger,
+			$this->timeFactory);
+	}
+
+	public function testGenerateToken() {
+		$token = 'token';
+		$uid = 'user';
+		$user = 'User';
+		$password = 'passme';
+		$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
+		$type = IToken::PERMANENT_TOKEN;
+
+		$actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
+
+		$this->assertInstanceOf(PublicKeyToken::class, $actual);
+		$this->assertSame($uid, $actual->getUID());
+		$this->assertSame($user, $actual->getLoginName());
+		$this->assertSame($name, $actual->getName());
+		$this->assertSame(IToken::DO_NOT_REMEMBER, $actual->getRemember());
+		$this->assertSame($password, $this->tokenProvider->getPassword($actual, $token));
+	}
+
+	public function testUpdateToken() {
+		$tk = new PublicKeyToken();
+		$tk->setLastActivity($this->time - 200);
+		$this->mapper->expects($this->once())
+			->method('update')
+			->with($tk);
+
+		$this->tokenProvider->updateTokenActivity($tk);
+
+		$this->assertEquals($this->time, $tk->getLastActivity());
+	}
+
+	public function testUpdateTokenDebounce() {
+		$tk = new PublicKeyToken();
+		$tk->setLastActivity($this->time - 30);
+		$this->mapper->expects($this->never())
+			->method('update')
+			->with($tk);
+
+		$this->tokenProvider->updateTokenActivity($tk);
+	}
+
+	public function testGetTokenByUser() {
+		$user = $this->createMock(IUser::class);
+		$this->mapper->expects($this->once())
+			->method('getTokenByUser')
+			->with($user)
+			->will($this->returnValue(['token']));
+
+		$this->assertEquals(['token'], $this->tokenProvider->getTokenByUser($user));
+	}
+
+	public function testGetPassword() {
+		$token = 'token';
+		$uid = 'user';
+		$user = 'User';
+		$password = 'passme';
+		$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
+		$type = IToken::PERMANENT_TOKEN;
+
+		$actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
+
+		$this->assertSame($password, $this->tokenProvider->getPassword($actual, $token));
+	}
+
+	/**
+	 * @expectedException \OC\Authentication\Exceptions\PasswordlessTokenException
+	 */
+	public function testGetPasswordPasswordLessToken() {
+		$token = 'token1234';
+		$tk = new PublicKeyToken();
+		$tk->setPassword(null);
+
+		$this->tokenProvider->getPassword($tk, $token);
+	}
+
+	/**
+	 * @expectedException \OC\Authentication\Exceptions\InvalidTokenException
+	 */
+	public function testGetPasswordInvalidToken() {
+		$token = 'token';
+		$uid = 'user';
+		$user = 'User';
+		$password = 'passme';
+		$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
+		$type = IToken::PERMANENT_TOKEN;
+
+		$actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
+
+		$this->tokenProvider->getPassword($actual, 'wrongtoken');
+	}
+
+	public function testSetPassword() {
+		$token = 'token';
+		$uid = 'user';
+		$user = 'User';
+		$password = 'passme';
+		$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
+		$type = IToken::PERMANENT_TOKEN;
+
+		$actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
+
+		$newpass = 'newpass';
+		$this->tokenProvider->setPassword($actual, $token, $newpass);
+
+		$this->assertSame($newpass, $this->tokenProvider->getPassword($actual, 'token'));
+	}
+
+	/**
+	 * @expectedException \OC\Authentication\Exceptions\InvalidTokenException
+	 */
+	public function testSetPasswordInvalidToken() {
+		$token = $this->createMock(IToken::class);
+		$tokenId = 'token123';
+		$password = '123456';
+
+		$this->tokenProvider->setPassword($token, $tokenId, $password);
+	}
+
+	public function testInvalidateToken() {
+		$this->mapper->expects($this->once())
+			->method('invalidate')
+			->with(hash('sha512', 'token7'.'1f4h9s'));
+
+		$this->tokenProvider->invalidateToken('token7');
+	}
+
+	public function testInvaildateTokenById() {
+		$id = 123;
+		$user = $this->createMock(IUser::class);
+
+		$this->mapper->expects($this->once())
+			->method('deleteById')
+			->with($user, $id);
+
+		$this->tokenProvider->invalidateTokenById($user, $id);
+	}
+
+	public function testInvalidateOldTokens() {
+		$defaultSessionLifetime = 60 * 60 * 24;
+		$defaultRememberMeLifetime = 60 * 60 * 24 * 15;
+		$this->config->expects($this->exactly(2))
+			->method('getSystemValue')
+			->will($this->returnValueMap([
+				['session_lifetime', $defaultSessionLifetime, 150],
+				['remember_login_cookie_lifetime', $defaultRememberMeLifetime, 300],
+			]));
+		$this->mapper->expects($this->at(0))
+			->method('invalidateOld')
+			->with($this->time - 150);
+		$this->mapper->expects($this->at(1))
+			->method('invalidateOld')
+			->with($this->time - 300);
+
+		$this->tokenProvider->invalidateOldTokens();
+	}
+
+	public function testRenewSessionTokenWithoutPassword() {
+		$token = 'oldId';
+		$uid = 'user';
+		$user = 'User';
+		$password = null;
+		$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
+		$type = IToken::PERMANENT_TOKEN;
+
+		$oldToken = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
+
+		$this->mapper
+			->expects($this->at(0))
+			->method('getToken')
+			->with(hash('sha512', 'oldId' . '1f4h9s'))
+			->willReturn($oldToken);
+		$this->mapper
+			->expects($this->at(1))
+			->method('insert')
+			->with($this->callback(function (PublicKeyToken $token) use ($user, $uid, $name) {
+				return $token->getUID() === $uid &&
+					$token->getLoginName() === $user &&
+					$token->getName() === $name &&
+					$token->getType() === IToken::DO_NOT_REMEMBER &&
+					$token->getLastActivity() === $this->time &&
+					$token->getPassword() === null;
+			}));
+		$this->mapper
+			->expects($this->at(2))
+			->method('delete')
+			->with($this->callback(function($token) use ($oldToken) {
+				return $token === $oldToken;
+			}));
+
+		$this->tokenProvider->renewSessionToken('oldId', 'newId');
+	}
+
+	public function testRenewSessionTokenWithPassword() {
+		$token = 'oldId';
+		$uid = 'user';
+		$user = 'User';
+		$password = 'password';
+		$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
+		$type = IToken::PERMANENT_TOKEN;
+
+		$oldToken = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
+
+		$this->mapper
+			->expects($this->at(0))
+			->method('getToken')
+			->with(hash('sha512', 'oldId' . '1f4h9s'))
+			->willReturn($oldToken);
+		$this->mapper
+			->expects($this->at(1))
+			->method('insert')
+			->with($this->callback(function (PublicKeyToken $token) use ($user, $uid, $name) {
+				return $token->getUID() === $uid &&
+					$token->getLoginName() === $user &&
+					$token->getName() === $name &&
+					$token->getType() === IToken::DO_NOT_REMEMBER &&
+					$token->getLastActivity() === $this->time &&
+					$token->getPassword() !== null &&
+					$this->tokenProvider->getPassword($token, 'newId') === 'password';
+			}));
+		$this->mapper
+			->expects($this->at(2))
+			->method('delete')
+			->with($this->callback(function($token) use ($oldToken) {
+				return $token === $oldToken;
+			}));
+
+		$this->tokenProvider->renewSessionToken('oldId', 'newId');
+	}
+
+	public function testGetToken() {
+		$token = new PublicKeyToken();
+
+		$this->config->method('getSystemValue')
+			->with('secret')
+			->willReturn('mysecret');
+
+		$this->mapper->method('getToken')
+			->with(
+				$this->callback(function (string $token) {
+					return hash('sha512', 'unhashedToken'.'1f4h9s') === $token;
+				})
+			)->willReturn($token);
+
+		$this->assertSame($token, $this->tokenProvider->getToken('unhashedToken'));
+	}
+
+	public function testGetInvalidToken() {
+		$this->expectException(InvalidTokenException::class);
+
+		$this->mapper->method('getToken')
+			->with(
+				$this->callback(function (string $token) {
+					return hash('sha512', 'unhashedToken'.'1f4h9s') === $token;
+				})
+			)->willThrowException(new InvalidTokenException());
+
+		$this->tokenProvider->getToken('unhashedToken');
+	}
+
+	public function testGetExpiredToken() {
+		$token = 'token';
+		$uid = 'user';
+		$user = 'User';
+		$password = 'passme';
+		$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
+		$type = IToken::PERMANENT_TOKEN;
+
+		$actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
+		$actual->setExpires(42);
+
+		$this->mapper->method('getToken')
+			->with(
+				$this->callback(function (string $token) {
+					return hash('sha512', 'token'.'1f4h9s') === $token;
+				})
+			)->willReturn($actual);
+
+		try {
+			$this->tokenProvider->getToken('token');
+			$this->fail();
+		} catch (ExpiredTokenException $e) {
+			$this->assertSame($actual, $e->getToken());
+		}
+
+	}
+
+	public function testGetTokenById() {
+		$token = $this->createMock(PublicKeyToken::class);
+
+		$this->mapper->expects($this->once())
+			->method('getTokenById')
+			->with($this->equalTo(42))
+			->willReturn($token);
+
+		$this->assertSame($token, $this->tokenProvider->getTokenById(42));
+	}
+
+	public function testGetInvalidTokenById() {
+		$this->expectException(InvalidTokenException::class);
+
+		$this->mapper->expects($this->once())
+			->method('getTokenById')
+			->with($this->equalTo(42))
+			->willThrowException(new DoesNotExistException('nope'));
+
+		$this->tokenProvider->getTokenById(42);
+	}
+
+	public function testGetExpiredTokenById() {
+		$token = new PublicKeyToken();
+		$token->setExpires(42);
+
+		$this->mapper->expects($this->once())
+			->method('getTokenById')
+			->with($this->equalTo(42))
+			->willReturn($token);
+
+		try {
+			$this->tokenProvider->getTokenById(42);
+			$this->fail();
+		} catch (ExpiredTokenException $e) {
+			$this->assertSame($token, $e->getToken());
+		}
+	}
+
+	public function testRotate() {
+		$token = 'oldtoken';
+		$uid = 'user';
+		$user = 'User';
+		$password = 'password';
+		$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
+		$type = IToken::PERMANENT_TOKEN;
+
+		$actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
+
+		$new = $this->tokenProvider->rotate($actual, 'oldtoken', 'newtoken');
+
+		$this->assertSame('password', $this->tokenProvider->getPassword($new, 'newtoken'));
+	}
+
+	public function testRotateNoPassword() {
+		$token = 'oldtoken';
+		$uid = 'user';
+		$user = 'User';
+		$password = null;
+		$name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
+			. 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
+		$type = IToken::PERMANENT_TOKEN;
+
+		$actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
+
+		$oldPrivate = $actual->getPrivateKey();
+
+		$new = $this->tokenProvider->rotate($actual, 'oldtoken', 'newtoken');
+
+		$newPrivate = $new->getPrivateKey();
+
+		$this->assertNotSame($newPrivate, $oldPrivate);
+		$this->assertNull($new->getPassword());
+	}
+}
diff --git a/tests/lib/Authentication/Token/PublicKeyTokenTest.php b/tests/lib/Authentication/Token/PublicKeyTokenTest.php
new file mode 100644
index 00000000000..d0226eb9902
--- /dev/null
+++ b/tests/lib/Authentication/Token/PublicKeyTokenTest.php
@@ -0,0 +1,44 @@
+<?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());
+	}
+}
-- 
GitLab