OauthApiControllerTest.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. namespace OCA\OAuth2\Tests\Controller;
  7. use OC\Authentication\Exceptions\ExpiredTokenException;
  8. use OC\Authentication\Exceptions\InvalidTokenException;
  9. use OC\Authentication\Token\IProvider as TokenProvider;
  10. use OC\Authentication\Token\PublicKeyToken;
  11. use OCA\OAuth2\Controller\OauthApiController;
  12. use OCA\OAuth2\Db\AccessToken;
  13. use OCA\OAuth2\Db\AccessTokenMapper;
  14. use OCA\OAuth2\Db\Client;
  15. use OCA\OAuth2\Db\ClientMapper;
  16. use OCA\OAuth2\Exceptions\AccessTokenNotFoundException;
  17. use OCA\OAuth2\Exceptions\ClientNotFoundException;
  18. use OCP\AppFramework\Http;
  19. use OCP\AppFramework\Http\JSONResponse;
  20. use OCP\AppFramework\Utility\ITimeFactory;
  21. use OCP\IRequest;
  22. use OCP\Security\Bruteforce\IThrottler;
  23. use OCP\Security\ICrypto;
  24. use OCP\Security\ISecureRandom;
  25. use Psr\Log\LoggerInterface;
  26. use Test\TestCase;
  27. /* We have to use this to add a property to the mocked request and avoid warnings about dynamic properties on PHP>=8.2 */
  28. abstract class RequestMock implements IRequest {
  29. public array $server = [];
  30. }
  31. class OauthApiControllerTest extends TestCase {
  32. /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
  33. private $request;
  34. /** @var ICrypto|\PHPUnit\Framework\MockObject\MockObject */
  35. private $crypto;
  36. /** @var AccessTokenMapper|\PHPUnit\Framework\MockObject\MockObject */
  37. private $accessTokenMapper;
  38. /** @var ClientMapper|\PHPUnit\Framework\MockObject\MockObject */
  39. private $clientMapper;
  40. /** @var TokenProvider|\PHPUnit\Framework\MockObject\MockObject */
  41. private $tokenProvider;
  42. /** @var ISecureRandom|\PHPUnit\Framework\MockObject\MockObject */
  43. private $secureRandom;
  44. /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
  45. private $time;
  46. /** @var IThrottler|\PHPUnit\Framework\MockObject\MockObject */
  47. private $throttler;
  48. /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
  49. private $logger;
  50. /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
  51. private $timeFactory;
  52. /** @var OauthApiController */
  53. private $oauthApiController;
  54. protected function setUp(): void {
  55. parent::setUp();
  56. $this->request = $this->createMock(RequestMock::class);
  57. $this->crypto = $this->createMock(ICrypto::class);
  58. $this->accessTokenMapper = $this->createMock(AccessTokenMapper::class);
  59. $this->clientMapper = $this->createMock(ClientMapper::class);
  60. $this->tokenProvider = $this->createMock(TokenProvider::class);
  61. $this->secureRandom = $this->createMock(ISecureRandom::class);
  62. $this->time = $this->createMock(ITimeFactory::class);
  63. $this->throttler = $this->createMock(IThrottler::class);
  64. $this->logger = $this->createMock(LoggerInterface::class);
  65. $this->timeFactory = $this->createMock(ITimeFactory::class);
  66. $this->oauthApiController = new OauthApiController(
  67. 'oauth2',
  68. $this->request,
  69. $this->crypto,
  70. $this->accessTokenMapper,
  71. $this->clientMapper,
  72. $this->tokenProvider,
  73. $this->secureRandom,
  74. $this->time,
  75. $this->logger,
  76. $this->throttler,
  77. $this->timeFactory
  78. );
  79. }
  80. public function testGetTokenInvalidGrantType() {
  81. $expected = new JSONResponse([
  82. 'error' => 'invalid_grant',
  83. ], Http::STATUS_BAD_REQUEST);
  84. $expected->throttle(['invalid_grant' => 'foo']);
  85. $this->assertEquals($expected, $this->oauthApiController->getToken('foo', null, null, null, null));
  86. }
  87. public function testGetTokenInvalidCode() {
  88. $expected = new JSONResponse([
  89. 'error' => 'invalid_request',
  90. ], Http::STATUS_BAD_REQUEST);
  91. $expected->throttle(['invalid_request' => 'token not found', 'code' => 'invalidcode']);
  92. $this->accessTokenMapper->method('getByCode')
  93. ->with('invalidcode')
  94. ->willThrowException(new AccessTokenNotFoundException());
  95. $this->assertEquals($expected, $this->oauthApiController->getToken('authorization_code', 'invalidcode', null, null, null));
  96. }
  97. public function testGetTokenExpiredCode() {
  98. $codeCreatedAt = 100;
  99. $expiredSince = 123;
  100. $expected = new JSONResponse([
  101. 'error' => 'invalid_request',
  102. ], Http::STATUS_BAD_REQUEST);
  103. $expected->throttle(['invalid_request' => 'authorization_code_expired', 'expired_since' => $expiredSince]);
  104. $accessToken = new AccessToken();
  105. $accessToken->setClientId(42);
  106. $accessToken->setCodeCreatedAt($codeCreatedAt);
  107. $this->accessTokenMapper->method('getByCode')
  108. ->with('validcode')
  109. ->willReturn($accessToken);
  110. $tsNow = $codeCreatedAt + OauthApiController::AUTHORIZATION_CODE_EXPIRES_AFTER + $expiredSince;
  111. $dateNow = (new \DateTimeImmutable())->setTimestamp($tsNow);
  112. $this->timeFactory->method('now')
  113. ->willReturn($dateNow);
  114. $this->assertEquals($expected, $this->oauthApiController->getToken('authorization_code', 'validcode', null, null, null));
  115. }
  116. public function testGetTokenWithCodeForActiveToken() {
  117. // if a token has already delivered oauth tokens,
  118. // it should not be possible to get a new oauth token from a valid authorization code
  119. $codeCreatedAt = 100;
  120. $expected = new JSONResponse([
  121. 'error' => 'invalid_request',
  122. ], Http::STATUS_BAD_REQUEST);
  123. $expected->throttle(['invalid_request' => 'authorization_code_received_for_active_token']);
  124. $accessToken = new AccessToken();
  125. $accessToken->setClientId(42);
  126. $accessToken->setCodeCreatedAt($codeCreatedAt);
  127. $accessToken->setTokenCount(1);
  128. $this->accessTokenMapper->method('getByCode')
  129. ->with('validcode')
  130. ->willReturn($accessToken);
  131. $tsNow = $codeCreatedAt + 1;
  132. $dateNow = (new \DateTimeImmutable())->setTimestamp($tsNow);
  133. $this->timeFactory->method('now')
  134. ->willReturn($dateNow);
  135. $this->assertEquals($expected, $this->oauthApiController->getToken('authorization_code', 'validcode', null, null, null));
  136. }
  137. public function testGetTokenClientDoesNotExist() {
  138. // In this test, the token's authorization code is valid and has not expired
  139. // and we check what happens when the associated Oauth client does not exist
  140. $codeCreatedAt = 100;
  141. $expected = new JSONResponse([
  142. 'error' => 'invalid_request',
  143. ], Http::STATUS_BAD_REQUEST);
  144. $expected->throttle(['invalid_request' => 'client not found', 'client_id' => 42]);
  145. $accessToken = new AccessToken();
  146. $accessToken->setClientId(42);
  147. $accessToken->setCodeCreatedAt($codeCreatedAt);
  148. $this->accessTokenMapper->method('getByCode')
  149. ->with('validcode')
  150. ->willReturn($accessToken);
  151. // 'now' is before the token's authorization code expiration
  152. $tsNow = $codeCreatedAt + OauthApiController::AUTHORIZATION_CODE_EXPIRES_AFTER - 1;
  153. $dateNow = (new \DateTimeImmutable())->setTimestamp($tsNow);
  154. $this->timeFactory->method('now')
  155. ->willReturn($dateNow);
  156. $this->clientMapper->method('getByUid')
  157. ->with(42)
  158. ->willThrowException(new ClientNotFoundException());
  159. $this->assertEquals($expected, $this->oauthApiController->getToken('authorization_code', 'validcode', null, null, null));
  160. }
  161. public function testRefreshTokenInvalidRefreshToken() {
  162. $expected = new JSONResponse([
  163. 'error' => 'invalid_request',
  164. ], Http::STATUS_BAD_REQUEST);
  165. $expected->throttle(['invalid_request' => 'token not found', 'code' => 'invalidrefresh']);
  166. $this->accessTokenMapper->method('getByCode')
  167. ->with('invalidrefresh')
  168. ->willThrowException(new AccessTokenNotFoundException());
  169. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'invalidrefresh', null, null));
  170. }
  171. public function testRefreshTokenClientDoesNotExist() {
  172. $expected = new JSONResponse([
  173. 'error' => 'invalid_request',
  174. ], Http::STATUS_BAD_REQUEST);
  175. $expected->throttle(['invalid_request' => 'client not found', 'client_id' => 42]);
  176. $accessToken = new AccessToken();
  177. $accessToken->setClientId(42);
  178. $this->accessTokenMapper->method('getByCode')
  179. ->with('validrefresh')
  180. ->willReturn($accessToken);
  181. $this->clientMapper->method('getByUid')
  182. ->with(42)
  183. ->willThrowException(new ClientNotFoundException());
  184. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', null, null));
  185. }
  186. public function invalidClientProvider() {
  187. return [
  188. ['invalidClientId', 'invalidClientSecret'],
  189. ['clientId', 'invalidClientSecret'],
  190. ['invalidClientId', 'clientSecret'],
  191. ];
  192. }
  193. /**
  194. * @dataProvider invalidClientProvider
  195. *
  196. * @param string $clientId
  197. * @param string $clientSecret
  198. */
  199. public function testRefreshTokenInvalidClient($clientId, $clientSecret) {
  200. $expected = new JSONResponse([
  201. 'error' => 'invalid_client',
  202. ], Http::STATUS_BAD_REQUEST);
  203. $expected->throttle(['invalid_client' => 'client ID or secret does not match']);
  204. $accessToken = new AccessToken();
  205. $accessToken->setClientId(42);
  206. $this->accessTokenMapper->method('getByCode')
  207. ->with('validrefresh')
  208. ->willReturn($accessToken);
  209. $client = new Client();
  210. $client->setClientIdentifier('clientId');
  211. $client->setSecret('clientSecret');
  212. $this->clientMapper->method('getByUid')
  213. ->with(42)
  214. ->willReturn($client);
  215. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', $clientId, $clientSecret));
  216. }
  217. public function testRefreshTokenInvalidAppToken() {
  218. $expected = new JSONResponse([
  219. 'error' => 'invalid_request',
  220. ], Http::STATUS_BAD_REQUEST);
  221. $expected->throttle(['invalid_request' => 'token is invalid']);
  222. $accessToken = new AccessToken();
  223. $accessToken->setClientId(42);
  224. $accessToken->setTokenId(1337);
  225. $accessToken->setEncryptedToken('encryptedToken');
  226. $this->accessTokenMapper->method('getByCode')
  227. ->with('validrefresh')
  228. ->willReturn($accessToken);
  229. $client = new Client();
  230. $client->setClientIdentifier('clientId');
  231. $client->setSecret('encryptedClientSecret');
  232. $this->clientMapper->method('getByUid')
  233. ->with(42)
  234. ->willReturn($client);
  235. $this->crypto
  236. ->method('decrypt')
  237. ->with($this->callback(function (string $text) {
  238. return $text === 'encryptedClientSecret' || $text === 'encryptedToken';
  239. }))
  240. ->willReturnCallback(function (string $text) {
  241. return $text === 'encryptedClientSecret'
  242. ? 'clientSecret'
  243. : ($text === 'encryptedToken' ? 'decryptedToken' : '');
  244. });
  245. $this->tokenProvider->method('getTokenById')
  246. ->with(1337)
  247. ->willThrowException(new InvalidTokenException());
  248. $this->accessTokenMapper->expects($this->once())
  249. ->method('delete')
  250. ->with($accessToken);
  251. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', 'clientId', 'clientSecret'));
  252. }
  253. public function testRefreshTokenValidAppToken() {
  254. $accessToken = new AccessToken();
  255. $accessToken->setClientId(42);
  256. $accessToken->setTokenId(1337);
  257. $accessToken->setEncryptedToken('encryptedToken');
  258. $this->accessTokenMapper->method('getByCode')
  259. ->with('validrefresh')
  260. ->willReturn($accessToken);
  261. $client = new Client();
  262. $client->setClientIdentifier('clientId');
  263. $client->setSecret('encryptedClientSecret');
  264. $this->clientMapper->method('getByUid')
  265. ->with(42)
  266. ->willReturn($client);
  267. $this->crypto
  268. ->method('decrypt')
  269. ->with($this->callback(function (string $text) {
  270. return $text === 'encryptedClientSecret' || $text === 'encryptedToken';
  271. }))
  272. ->willReturnCallback(function (string $text) {
  273. return $text === 'encryptedClientSecret'
  274. ? 'clientSecret'
  275. : ($text === 'encryptedToken' ? 'decryptedToken' : '');
  276. });
  277. $appToken = new PublicKeyToken();
  278. $appToken->setUid('userId');
  279. $this->tokenProvider->method('getTokenById')
  280. ->with(1337)
  281. ->willThrowException(new ExpiredTokenException($appToken));
  282. $this->accessTokenMapper->expects($this->never())
  283. ->method('delete')
  284. ->with($accessToken);
  285. $this->secureRandom->method('generate')
  286. ->willReturnCallback(function ($len) {
  287. return 'random'.$len;
  288. });
  289. $this->tokenProvider->expects($this->once())
  290. ->method('rotate')
  291. ->with(
  292. $appToken,
  293. 'decryptedToken',
  294. 'random72'
  295. )->willReturn($appToken);
  296. $this->time->method('getTime')
  297. ->willReturn(1000);
  298. $this->tokenProvider->expects($this->once())
  299. ->method('updateToken')
  300. ->with(
  301. $this->callback(function (PublicKeyToken $token) {
  302. return $token->getExpires() === 4600;
  303. })
  304. );
  305. $this->crypto->method('encrypt')
  306. ->with('random72', 'random128')
  307. ->willReturn('newEncryptedToken');
  308. $this->accessTokenMapper->expects($this->once())
  309. ->method('update')
  310. ->with(
  311. $this->callback(function (AccessToken $token) {
  312. return $token->getHashedCode() === hash('sha512', 'random128') &&
  313. $token->getEncryptedToken() === 'newEncryptedToken';
  314. })
  315. );
  316. $expected = new JSONResponse([
  317. 'access_token' => 'random72',
  318. 'token_type' => 'Bearer',
  319. 'expires_in' => 3600,
  320. 'refresh_token' => 'random128',
  321. 'user_id' => 'userId',
  322. ]);
  323. $this->request->method('getRemoteAddress')
  324. ->willReturn('1.2.3.4');
  325. $this->throttler->expects($this->once())
  326. ->method('resetDelay')
  327. ->with(
  328. '1.2.3.4',
  329. 'login',
  330. ['user' => 'userId']
  331. );
  332. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', 'clientId', 'clientSecret'));
  333. }
  334. public function testRefreshTokenValidAppTokenBasicAuth() {
  335. $accessToken = new AccessToken();
  336. $accessToken->setClientId(42);
  337. $accessToken->setTokenId(1337);
  338. $accessToken->setEncryptedToken('encryptedToken');
  339. $this->accessTokenMapper->method('getByCode')
  340. ->with('validrefresh')
  341. ->willReturn($accessToken);
  342. $client = new Client();
  343. $client->setClientIdentifier('clientId');
  344. $client->setSecret('encryptedClientSecret');
  345. $this->clientMapper->method('getByUid')
  346. ->with(42)
  347. ->willReturn($client);
  348. $this->crypto
  349. ->method('decrypt')
  350. ->with($this->callback(function (string $text) {
  351. return $text === 'encryptedClientSecret' || $text === 'encryptedToken';
  352. }))
  353. ->willReturnCallback(function (string $text) {
  354. return $text === 'encryptedClientSecret'
  355. ? 'clientSecret'
  356. : ($text === 'encryptedToken' ? 'decryptedToken' : '');
  357. });
  358. $appToken = new PublicKeyToken();
  359. $appToken->setUid('userId');
  360. $this->tokenProvider->method('getTokenById')
  361. ->with(1337)
  362. ->willThrowException(new ExpiredTokenException($appToken));
  363. $this->accessTokenMapper->expects($this->never())
  364. ->method('delete')
  365. ->with($accessToken);
  366. $this->secureRandom->method('generate')
  367. ->willReturnCallback(function ($len) {
  368. return 'random'.$len;
  369. });
  370. $this->tokenProvider->expects($this->once())
  371. ->method('rotate')
  372. ->with(
  373. $appToken,
  374. 'decryptedToken',
  375. 'random72'
  376. )->willReturn($appToken);
  377. $this->time->method('getTime')
  378. ->willReturn(1000);
  379. $this->tokenProvider->expects($this->once())
  380. ->method('updateToken')
  381. ->with(
  382. $this->callback(function (PublicKeyToken $token) {
  383. return $token->getExpires() === 4600;
  384. })
  385. );
  386. $this->crypto->method('encrypt')
  387. ->with('random72', 'random128')
  388. ->willReturn('newEncryptedToken');
  389. $this->accessTokenMapper->expects($this->once())
  390. ->method('update')
  391. ->with(
  392. $this->callback(function (AccessToken $token) {
  393. return $token->getHashedCode() === hash('sha512', 'random128') &&
  394. $token->getEncryptedToken() === 'newEncryptedToken';
  395. })
  396. );
  397. $expected = new JSONResponse([
  398. 'access_token' => 'random72',
  399. 'token_type' => 'Bearer',
  400. 'expires_in' => 3600,
  401. 'refresh_token' => 'random128',
  402. 'user_id' => 'userId',
  403. ]);
  404. $this->request->server['PHP_AUTH_USER'] = 'clientId';
  405. $this->request->server['PHP_AUTH_PW'] = 'clientSecret';
  406. $this->request->method('getRemoteAddress')
  407. ->willReturn('1.2.3.4');
  408. $this->throttler->expects($this->once())
  409. ->method('resetDelay')
  410. ->with(
  411. '1.2.3.4',
  412. 'login',
  413. ['user' => 'userId']
  414. );
  415. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', null, null));
  416. }
  417. public function testRefreshTokenExpiredAppToken() {
  418. $accessToken = new AccessToken();
  419. $accessToken->setClientId(42);
  420. $accessToken->setTokenId(1337);
  421. $accessToken->setEncryptedToken('encryptedToken');
  422. $this->accessTokenMapper->method('getByCode')
  423. ->with('validrefresh')
  424. ->willReturn($accessToken);
  425. $client = new Client();
  426. $client->setClientIdentifier('clientId');
  427. $client->setSecret('encryptedClientSecret');
  428. $this->clientMapper->method('getByUid')
  429. ->with(42)
  430. ->willReturn($client);
  431. $this->crypto
  432. ->method('decrypt')
  433. ->with($this->callback(function (string $text) {
  434. return $text === 'encryptedClientSecret' || $text === 'encryptedToken';
  435. }))
  436. ->willReturnCallback(function (string $text) {
  437. return $text === 'encryptedClientSecret'
  438. ? 'clientSecret'
  439. : ($text === 'encryptedToken' ? 'decryptedToken' : '');
  440. });
  441. $appToken = new PublicKeyToken();
  442. $appToken->setUid('userId');
  443. $this->tokenProvider->method('getTokenById')
  444. ->with(1337)
  445. ->willReturn($appToken);
  446. $this->accessTokenMapper->expects($this->never())
  447. ->method('delete')
  448. ->with($accessToken);
  449. $this->secureRandom->method('generate')
  450. ->willReturnCallback(function ($len) {
  451. return 'random'.$len;
  452. });
  453. $this->tokenProvider->expects($this->once())
  454. ->method('rotate')
  455. ->with(
  456. $appToken,
  457. 'decryptedToken',
  458. 'random72'
  459. )->willReturn($appToken);
  460. $this->time->method('getTime')
  461. ->willReturn(1000);
  462. $this->tokenProvider->expects($this->once())
  463. ->method('updateToken')
  464. ->with(
  465. $this->callback(function (PublicKeyToken $token) {
  466. return $token->getExpires() === 4600;
  467. })
  468. );
  469. $this->crypto->method('encrypt')
  470. ->with('random72', 'random128')
  471. ->willReturn('newEncryptedToken');
  472. $this->accessTokenMapper->expects($this->once())
  473. ->method('update')
  474. ->with(
  475. $this->callback(function (AccessToken $token) {
  476. return $token->getHashedCode() === hash('sha512', 'random128') &&
  477. $token->getEncryptedToken() === 'newEncryptedToken';
  478. })
  479. );
  480. $expected = new JSONResponse([
  481. 'access_token' => 'random72',
  482. 'token_type' => 'Bearer',
  483. 'expires_in' => 3600,
  484. 'refresh_token' => 'random128',
  485. 'user_id' => 'userId',
  486. ]);
  487. $this->request->method('getRemoteAddress')
  488. ->willReturn('1.2.3.4');
  489. $this->throttler->expects($this->once())
  490. ->method('resetDelay')
  491. ->with(
  492. '1.2.3.4',
  493. 'login',
  494. ['user' => 'userId']
  495. );
  496. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', 'clientId', 'clientSecret'));
  497. }
  498. }