You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PublicKeyTokenProviderTest.php 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl>
  4. *
  5. * @author Roeland Jago Douma <roeland@famdouma.nl>
  6. *
  7. * @license GNU AGPL version 3 or any later version
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as
  11. * published by the Free Software Foundation, either version 3 of the
  12. * License, or (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. */
  23. namespace Test\Authentication\Token;
  24. use OC\Authentication\Exceptions\ExpiredTokenException;
  25. use OC\Authentication\Exceptions\InvalidTokenException;
  26. use OC\Authentication\Exceptions\PasswordlessTokenException;
  27. use OC\Authentication\Token\IToken;
  28. use OC\Authentication\Token\PublicKeyToken;
  29. use OC\Authentication\Token\PublicKeyTokenMapper;
  30. use OC\Authentication\Token\PublicKeyTokenProvider;
  31. use OCP\AppFramework\Db\DoesNotExistException;
  32. use OCP\AppFramework\Utility\ITimeFactory;
  33. use OCP\IConfig;
  34. use OCP\Security\ICrypto;
  35. use Psr\Log\LoggerInterface;
  36. use Test\TestCase;
  37. class PublicKeyTokenProviderTest extends TestCase {
  38. /** @var PublicKeyTokenProvider|\PHPUnit\Framework\MockObject\MockObject */
  39. private $tokenProvider;
  40. /** @var PublicKeyTokenMapper|\PHPUnit\Framework\MockObject\MockObject */
  41. private $mapper;
  42. /** @var ICrypto */
  43. private $crypto;
  44. /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
  45. private $config;
  46. /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
  47. private $logger;
  48. /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
  49. private $timeFactory;
  50. /** @var int */
  51. private $time;
  52. protected function setUp(): void {
  53. parent::setUp();
  54. $this->mapper = $this->createMock(PublicKeyTokenMapper::class);
  55. $this->crypto = \OC::$server->getCrypto();
  56. $this->config = $this->createMock(IConfig::class);
  57. $this->config->method('getSystemValue')
  58. ->willReturnMap([
  59. ['session_lifetime', 60 * 60 * 24, 150],
  60. ['remember_login_cookie_lifetime', 60 * 60 * 24 * 15, 300],
  61. ['secret', '', '1f4h9s'],
  62. ['openssl', [], []],
  63. ]);
  64. $this->logger = $this->createMock(LoggerInterface::class);
  65. $this->timeFactory = $this->createMock(ITimeFactory::class);
  66. $this->time = 1313131;
  67. $this->timeFactory->method('getTime')
  68. ->willReturn($this->time);
  69. $this->tokenProvider = new PublicKeyTokenProvider($this->mapper, $this->crypto, $this->config, $this->logger,
  70. $this->timeFactory);
  71. }
  72. public function testGenerateToken() {
  73. $token = 'token';
  74. $uid = 'user';
  75. $user = 'User';
  76. $password = 'passme';
  77. $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';
  78. $type = IToken::PERMANENT_TOKEN;
  79. $this->config->method('getSystemValueBool')
  80. ->willReturnMap([
  81. ['auth.storeCryptedPassword', true, true],
  82. ]);
  83. $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
  84. $this->assertInstanceOf(PublicKeyToken::class, $actual);
  85. $this->assertSame($uid, $actual->getUID());
  86. $this->assertSame($user, $actual->getLoginName());
  87. $this->assertSame($name, $actual->getName());
  88. $this->assertSame(IToken::DO_NOT_REMEMBER, $actual->getRemember());
  89. $this->assertSame($password, $this->tokenProvider->getPassword($actual, $token));
  90. }
  91. public function testGenerateTokenNoPassword() {
  92. $token = 'token';
  93. $uid = 'user';
  94. $user = 'User';
  95. $password = 'passme';
  96. $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';
  97. $type = IToken::PERMANENT_TOKEN;
  98. $this->config->method('getSystemValueBool')
  99. ->willReturnMap([
  100. ['auth.storeCryptedPassword', true, false],
  101. ]);
  102. $this->expectException(PasswordlessTokenException::class);
  103. $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
  104. $this->assertInstanceOf(PublicKeyToken::class, $actual);
  105. $this->assertSame($uid, $actual->getUID());
  106. $this->assertSame($user, $actual->getLoginName());
  107. $this->assertSame($name, $actual->getName());
  108. $this->assertSame(IToken::DO_NOT_REMEMBER, $actual->getRemember());
  109. $this->tokenProvider->getPassword($actual, $token);
  110. }
  111. public function testGenerateTokenInvalidName() {
  112. $token = 'token';
  113. $uid = 'user';
  114. $user = 'User';
  115. $password = 'passme';
  116. $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'
  117. . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
  118. . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'
  119. . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12';
  120. $type = IToken::PERMANENT_TOKEN;
  121. $this->config->method('getSystemValueBool')
  122. ->willReturnMap([
  123. ['auth.storeCryptedPassword', true, true],
  124. ]);
  125. $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
  126. $this->assertInstanceOf(PublicKeyToken::class, $actual);
  127. $this->assertSame($uid, $actual->getUID());
  128. $this->assertSame($user, $actual->getLoginName());
  129. $this->assertSame('User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12User-Agent: Mozill…', $actual->getName());
  130. $this->assertSame(IToken::DO_NOT_REMEMBER, $actual->getRemember());
  131. $this->assertSame($password, $this->tokenProvider->getPassword($actual, $token));
  132. }
  133. public function testUpdateToken() {
  134. $tk = new PublicKeyToken();
  135. $this->mapper->expects($this->once())
  136. ->method('updateActivity')
  137. ->with($tk, $this->time);
  138. $tk->setLastActivity($this->time - 200);
  139. $this->tokenProvider->updateTokenActivity($tk);
  140. $this->assertEquals($this->time, $tk->getLastActivity());
  141. }
  142. public function testUpdateTokenDebounce() {
  143. $tk = new PublicKeyToken();
  144. $this->config->method('getSystemValueInt')
  145. ->willReturnCallback(function ($value, $default) {
  146. return $default;
  147. });
  148. $tk->setLastActivity($this->time - 30);
  149. $this->mapper->expects($this->never())
  150. ->method('updateActivity')
  151. ->with($tk, $this->time);
  152. $this->tokenProvider->updateTokenActivity($tk);
  153. }
  154. public function testGetTokenByUser() {
  155. $this->mapper->expects($this->once())
  156. ->method('getTokenByUser')
  157. ->with('uid')
  158. ->willReturn(['token']);
  159. $this->assertEquals(['token'], $this->tokenProvider->getTokenByUser('uid'));
  160. }
  161. public function testGetPassword() {
  162. $token = 'token';
  163. $uid = 'user';
  164. $user = 'User';
  165. $password = 'passme';
  166. $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';
  167. $type = IToken::PERMANENT_TOKEN;
  168. $this->config->method('getSystemValueBool')
  169. ->willReturnMap([
  170. ['auth.storeCryptedPassword', true, true],
  171. ]);
  172. $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
  173. $this->assertSame($password, $this->tokenProvider->getPassword($actual, $token));
  174. }
  175. public function testGetPasswordPasswordLessToken() {
  176. $this->expectException(\OC\Authentication\Exceptions\PasswordlessTokenException::class);
  177. $token = 'token1234';
  178. $tk = new PublicKeyToken();
  179. $tk->setPassword(null);
  180. $this->tokenProvider->getPassword($tk, $token);
  181. }
  182. public function testGetPasswordInvalidToken() {
  183. $this->expectException(\OC\Authentication\Exceptions\InvalidTokenException::class);
  184. $token = 'token';
  185. $uid = 'user';
  186. $user = 'User';
  187. $password = 'passme';
  188. $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';
  189. $type = IToken::PERMANENT_TOKEN;
  190. $this->config->method('getSystemValueBool')
  191. ->willReturnMap([
  192. ['auth.storeCryptedPassword', true, true],
  193. ]);
  194. $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
  195. $this->tokenProvider->getPassword($actual, 'wrongtoken');
  196. }
  197. public function testSetPassword() {
  198. $token = 'token';
  199. $uid = 'user';
  200. $user = 'User';
  201. $password = 'passme';
  202. $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';
  203. $type = IToken::PERMANENT_TOKEN;
  204. $this->config->method('getSystemValueBool')
  205. ->willReturnMap([
  206. ['auth.storeCryptedPassword', true, true],
  207. ]);
  208. $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
  209. $this->mapper->method('getTokenByUser')
  210. ->with('user')
  211. ->willReturn([$actual]);
  212. $newpass = 'newpass';
  213. $this->mapper->expects($this->once())
  214. ->method('update')
  215. ->with($this->callback(function ($token) use ($newpass) {
  216. return $newpass === $this->tokenProvider->getPassword($token, 'token');
  217. }));
  218. $this->tokenProvider->setPassword($actual, $token, $newpass);
  219. $this->assertSame($newpass, $this->tokenProvider->getPassword($actual, 'token'));
  220. }
  221. public function testSetPasswordInvalidToken() {
  222. $this->expectException(\OC\Authentication\Exceptions\InvalidTokenException::class);
  223. $token = $this->createMock(IToken::class);
  224. $tokenId = 'token123';
  225. $password = '123456';
  226. $this->tokenProvider->setPassword($token, $tokenId, $password);
  227. }
  228. public function testInvalidateToken() {
  229. $this->mapper->expects($this->once())
  230. ->method('invalidate')
  231. ->with(hash('sha512', 'token7'.'1f4h9s'));
  232. $this->tokenProvider->invalidateToken('token7');
  233. }
  234. public function testInvaildateTokenById() {
  235. $id = 123;
  236. $this->mapper->expects($this->once())
  237. ->method('deleteById')
  238. ->with('uid', $id);
  239. $this->tokenProvider->invalidateTokenById('uid', $id);
  240. }
  241. public function testInvalidateOldTokens() {
  242. $defaultSessionLifetime = 60 * 60 * 24;
  243. $defaultRememberMeLifetime = 60 * 60 * 24 * 15;
  244. $this->config->expects($this->exactly(2))
  245. ->method('getSystemValue')
  246. ->willReturnMap([
  247. ['session_lifetime', $defaultSessionLifetime, 150],
  248. ['remember_login_cookie_lifetime', $defaultRememberMeLifetime, 300],
  249. ]);
  250. $this->mapper->expects($this->exactly(2))
  251. ->method('invalidateOld')
  252. ->withConsecutive(
  253. [$this->time - 150],
  254. [$this->time - 300]
  255. );
  256. $this->tokenProvider->invalidateOldTokens();
  257. }
  258. public function testRenewSessionTokenWithoutPassword() {
  259. $token = 'oldId';
  260. $uid = 'user';
  261. $user = 'User';
  262. $password = null;
  263. $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';
  264. $type = IToken::PERMANENT_TOKEN;
  265. $oldToken = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
  266. $this->mapper
  267. ->expects($this->once())
  268. ->method('getToken')
  269. ->with(hash('sha512', 'oldId' . '1f4h9s'))
  270. ->willReturn($oldToken);
  271. $this->mapper
  272. ->expects($this->once())
  273. ->method('insert')
  274. ->with($this->callback(function (PublicKeyToken $token) use ($user, $uid, $name) {
  275. return $token->getUID() === $uid &&
  276. $token->getLoginName() === $user &&
  277. $token->getName() === $name &&
  278. $token->getType() === IToken::DO_NOT_REMEMBER &&
  279. $token->getLastActivity() === $this->time &&
  280. $token->getPassword() === null;
  281. }));
  282. $this->mapper
  283. ->expects($this->once())
  284. ->method('delete')
  285. ->with($this->callback(function ($token) use ($oldToken) {
  286. return $token === $oldToken;
  287. }));
  288. $this->tokenProvider->renewSessionToken('oldId', 'newId');
  289. }
  290. public function testRenewSessionTokenWithPassword(): void {
  291. $token = 'oldId';
  292. $uid = 'user';
  293. $user = 'User';
  294. $password = 'password';
  295. $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';
  296. $type = IToken::PERMANENT_TOKEN;
  297. $this->config->method('getSystemValueBool')
  298. ->willReturnMap([
  299. ['auth.storeCryptedPassword', true, true],
  300. ]);
  301. $oldToken = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
  302. $this->mapper
  303. ->expects($this->once())
  304. ->method('getToken')
  305. ->with(hash('sha512', 'oldId' . '1f4h9s'))
  306. ->willReturn($oldToken);
  307. $this->mapper
  308. ->expects($this->once())
  309. ->method('insert')
  310. ->with($this->callback(function (PublicKeyToken $token) use ($user, $uid, $name): bool {
  311. return $token->getUID() === $uid &&
  312. $token->getLoginName() === $user &&
  313. $token->getName() === $name &&
  314. $token->getType() === IToken::DO_NOT_REMEMBER &&
  315. $token->getLastActivity() === $this->time &&
  316. $token->getPassword() !== null &&
  317. $this->tokenProvider->getPassword($token, 'newId') === 'password';
  318. }));
  319. $this->mapper
  320. ->expects($this->once())
  321. ->method('delete')
  322. ->with($this->callback(function ($token) use ($oldToken): bool {
  323. return $token === $oldToken;
  324. }));
  325. $this->tokenProvider->renewSessionToken('oldId', 'newId');
  326. }
  327. public function testGetToken(): void {
  328. $token = new PublicKeyToken();
  329. $this->config->method('getSystemValue')
  330. ->with('secret')
  331. ->willReturn('mysecret');
  332. $this->mapper->method('getToken')
  333. ->with(
  334. $this->callback(function (string $token) {
  335. return hash('sha512', 'unhashedToken'.'1f4h9s') === $token;
  336. })
  337. )->willReturn($token);
  338. $this->assertSame($token, $this->tokenProvider->getToken('unhashedToken'));
  339. }
  340. public function testGetInvalidToken() {
  341. $this->expectException(InvalidTokenException::class);
  342. $this->mapper->method('getToken')
  343. ->with(
  344. $this->callback(function (string $token) {
  345. return hash('sha512', 'unhashedToken'.'1f4h9s') === $token;
  346. })
  347. )->willThrowException(new DoesNotExistException('nope'));
  348. $this->tokenProvider->getToken('unhashedToken');
  349. }
  350. public function testGetExpiredToken() {
  351. $token = 'token';
  352. $uid = 'user';
  353. $user = 'User';
  354. $password = 'passme';
  355. $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';
  356. $type = IToken::PERMANENT_TOKEN;
  357. $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
  358. $actual->setExpires(42);
  359. $this->mapper->method('getToken')
  360. ->with(
  361. $this->callback(function (string $token) {
  362. return hash('sha512', 'token'.'1f4h9s') === $token;
  363. })
  364. )->willReturn($actual);
  365. try {
  366. $this->tokenProvider->getToken('token');
  367. $this->fail();
  368. } catch (ExpiredTokenException $e) {
  369. $this->assertSame($actual, $e->getToken());
  370. }
  371. }
  372. public function testGetTokenById() {
  373. $token = $this->createMock(PublicKeyToken::class);
  374. $this->mapper->expects($this->once())
  375. ->method('getTokenById')
  376. ->with($this->equalTo(42))
  377. ->willReturn($token);
  378. $this->assertSame($token, $this->tokenProvider->getTokenById(42));
  379. }
  380. public function testGetInvalidTokenById() {
  381. $this->expectException(InvalidTokenException::class);
  382. $this->mapper->expects($this->once())
  383. ->method('getTokenById')
  384. ->with($this->equalTo(42))
  385. ->willThrowException(new DoesNotExistException('nope'));
  386. $this->tokenProvider->getTokenById(42);
  387. }
  388. public function testGetExpiredTokenById() {
  389. $token = new PublicKeyToken();
  390. $token->setExpires(42);
  391. $this->mapper->expects($this->once())
  392. ->method('getTokenById')
  393. ->with($this->equalTo(42))
  394. ->willReturn($token);
  395. try {
  396. $this->tokenProvider->getTokenById(42);
  397. $this->fail();
  398. } catch (ExpiredTokenException $e) {
  399. $this->assertSame($token, $e->getToken());
  400. }
  401. }
  402. public function testRotate() {
  403. $token = 'oldtoken';
  404. $uid = 'user';
  405. $user = 'User';
  406. $password = 'password';
  407. $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';
  408. $type = IToken::PERMANENT_TOKEN;
  409. $this->config->method('getSystemValueBool')
  410. ->willReturnMap([
  411. ['auth.storeCryptedPassword', true, true],
  412. ]);
  413. $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
  414. $new = $this->tokenProvider->rotate($actual, 'oldtoken', 'newtoken');
  415. $this->assertSame('password', $this->tokenProvider->getPassword($new, 'newtoken'));
  416. }
  417. public function testRotateNoPassword() {
  418. $token = 'oldtoken';
  419. $uid = 'user';
  420. $user = 'User';
  421. $password = null;
  422. $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';
  423. $type = IToken::PERMANENT_TOKEN;
  424. $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER);
  425. $oldPrivate = $actual->getPrivateKey();
  426. $new = $this->tokenProvider->rotate($actual, 'oldtoken', 'newtoken');
  427. $newPrivate = $new->getPrivateKey();
  428. $this->assertNotSame($newPrivate, $oldPrivate);
  429. $this->assertNull($new->getPassword());
  430. }
  431. public function testMarkPasswordInvalidInvalidToken() {
  432. $token = $this->createMock(IToken::class);
  433. $this->expectException(InvalidTokenException::class);
  434. $this->tokenProvider->markPasswordInvalid($token, 'tokenId');
  435. }
  436. public function testMarkPasswordInvalid() {
  437. $token = $this->createMock(PublicKeyToken::class);
  438. $token->expects($this->once())
  439. ->method('setPasswordInvalid')
  440. ->with(true);
  441. $this->mapper->expects($this->once())
  442. ->method('update')
  443. ->with($token);
  444. $this->tokenProvider->markPasswordInvalid($token, 'tokenId');
  445. }
  446. public function testUpdatePasswords() {
  447. $uid = 'myUID';
  448. $token1 = $this->tokenProvider->generateToken(
  449. 'foo',
  450. $uid,
  451. $uid,
  452. 'bar',
  453. 'random1',
  454. IToken::PERMANENT_TOKEN,
  455. IToken::REMEMBER);
  456. $token2 = $this->tokenProvider->generateToken(
  457. 'foobar',
  458. $uid,
  459. $uid,
  460. 'bar',
  461. 'random2',
  462. IToken::PERMANENT_TOKEN,
  463. IToken::REMEMBER);
  464. $this->mapper->method('hasExpiredTokens')
  465. ->with($uid)
  466. ->willReturn(true);
  467. $this->mapper->expects($this->once())
  468. ->method('getTokenByUser')
  469. ->with($uid)
  470. ->willReturn([$token1, $token2]);
  471. $this->mapper->expects($this->exactly(2))
  472. ->method('update')
  473. ->with($this->callback(function (PublicKeyToken $t) use ($token1, $token2) {
  474. return $t === $token1 || $t === $token2;
  475. }));
  476. $this->tokenProvider->updatePasswords($uid, 'bar2');
  477. }
  478. }