OauthApiControllerTest.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  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(): void {
  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(): void {
  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(): void {
  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(): void {
  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(): void {
  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(): void {
  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(): void {
  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): void {
  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. $this->crypto
  210. ->method('calculateHMAC')
  211. ->with($this->callback(function (string $text) {
  212. return $text === 'clientSecret' || $text === 'invalidClientSecret';
  213. }))
  214. ->willReturnCallback(function (string $text) {
  215. return $text === 'clientSecret'
  216. ? 'hashedClientSecret'
  217. : 'hashedInvalidClientSecret';
  218. });
  219. $client = new Client();
  220. $client->setClientIdentifier('clientId');
  221. $client->setSecret(bin2hex('hashedClientSecret'));
  222. $this->clientMapper->method('getByUid')
  223. ->with(42)
  224. ->willReturn($client);
  225. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', $clientId, $clientSecret));
  226. }
  227. public function testRefreshTokenInvalidAppToken(): void {
  228. $expected = new JSONResponse([
  229. 'error' => 'invalid_request',
  230. ], Http::STATUS_BAD_REQUEST);
  231. $expected->throttle(['invalid_request' => 'token is invalid']);
  232. $accessToken = new AccessToken();
  233. $accessToken->setClientId(42);
  234. $accessToken->setTokenId(1337);
  235. $accessToken->setEncryptedToken('encryptedToken');
  236. $this->accessTokenMapper->method('getByCode')
  237. ->with('validrefresh')
  238. ->willReturn($accessToken);
  239. $client = new Client();
  240. $client->setClientIdentifier('clientId');
  241. $client->setSecret(bin2hex('hashedClientSecret'));
  242. $this->clientMapper->method('getByUid')
  243. ->with(42)
  244. ->willReturn($client);
  245. $this->crypto
  246. ->method('decrypt')
  247. ->with('encryptedToken')
  248. ->willReturn('decryptedToken');
  249. $this->crypto
  250. ->method('calculateHMAC')
  251. ->with('clientSecret')
  252. ->willReturn('hashedClientSecret');
  253. $this->tokenProvider->method('getTokenById')
  254. ->with(1337)
  255. ->willThrowException(new InvalidTokenException());
  256. $this->accessTokenMapper->expects($this->once())
  257. ->method('delete')
  258. ->with($accessToken);
  259. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', 'clientId', 'clientSecret'));
  260. }
  261. public function testRefreshTokenValidAppToken(): void {
  262. $accessToken = new AccessToken();
  263. $accessToken->setClientId(42);
  264. $accessToken->setTokenId(1337);
  265. $accessToken->setEncryptedToken('encryptedToken');
  266. $this->accessTokenMapper->method('getByCode')
  267. ->with('validrefresh')
  268. ->willReturn($accessToken);
  269. $client = new Client();
  270. $client->setClientIdentifier('clientId');
  271. $client->setSecret(bin2hex('hashedClientSecret'));
  272. $this->clientMapper->method('getByUid')
  273. ->with(42)
  274. ->willReturn($client);
  275. $this->crypto
  276. ->method('decrypt')
  277. ->with('encryptedToken')
  278. ->willReturn('decryptedToken');
  279. $this->crypto
  280. ->method('calculateHMAC')
  281. ->with('clientSecret')
  282. ->willReturn('hashedClientSecret');
  283. $appToken = new PublicKeyToken();
  284. $appToken->setUid('userId');
  285. $this->tokenProvider->method('getTokenById')
  286. ->with(1337)
  287. ->willThrowException(new ExpiredTokenException($appToken));
  288. $this->accessTokenMapper->expects($this->never())
  289. ->method('delete')
  290. ->with($accessToken);
  291. $this->secureRandom->method('generate')
  292. ->willReturnCallback(function ($len) {
  293. return 'random'.$len;
  294. });
  295. $this->tokenProvider->expects($this->once())
  296. ->method('rotate')
  297. ->with(
  298. $appToken,
  299. 'decryptedToken',
  300. 'random72'
  301. )->willReturn($appToken);
  302. $this->time->method('getTime')
  303. ->willReturn(1000);
  304. $this->tokenProvider->expects($this->once())
  305. ->method('updateToken')
  306. ->with(
  307. $this->callback(function (PublicKeyToken $token) {
  308. return $token->getExpires() === 4600;
  309. })
  310. );
  311. $this->crypto->method('encrypt')
  312. ->with('random72', 'random128')
  313. ->willReturn('newEncryptedToken');
  314. $this->accessTokenMapper->expects($this->once())
  315. ->method('update')
  316. ->with(
  317. $this->callback(function (AccessToken $token) {
  318. return $token->getHashedCode() === hash('sha512', 'random128') &&
  319. $token->getEncryptedToken() === 'newEncryptedToken';
  320. })
  321. );
  322. $expected = new JSONResponse([
  323. 'access_token' => 'random72',
  324. 'token_type' => 'Bearer',
  325. 'expires_in' => 3600,
  326. 'refresh_token' => 'random128',
  327. 'user_id' => 'userId',
  328. ]);
  329. $this->request->method('getRemoteAddress')
  330. ->willReturn('1.2.3.4');
  331. $this->throttler->expects($this->once())
  332. ->method('resetDelay')
  333. ->with(
  334. '1.2.3.4',
  335. 'login',
  336. ['user' => 'userId']
  337. );
  338. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', 'clientId', 'clientSecret'));
  339. }
  340. public function testRefreshTokenValidAppTokenBasicAuth(): void {
  341. $accessToken = new AccessToken();
  342. $accessToken->setClientId(42);
  343. $accessToken->setTokenId(1337);
  344. $accessToken->setEncryptedToken('encryptedToken');
  345. $this->accessTokenMapper->method('getByCode')
  346. ->with('validrefresh')
  347. ->willReturn($accessToken);
  348. $client = new Client();
  349. $client->setClientIdentifier('clientId');
  350. $client->setSecret(bin2hex('hashedClientSecret'));
  351. $this->clientMapper->method('getByUid')
  352. ->with(42)
  353. ->willReturn($client);
  354. $this->crypto
  355. ->method('decrypt')
  356. ->with('encryptedToken')
  357. ->willReturn('decryptedToken');
  358. $this->crypto
  359. ->method('calculateHMAC')
  360. ->with('clientSecret')
  361. ->willReturn('hashedClientSecret');
  362. $appToken = new PublicKeyToken();
  363. $appToken->setUid('userId');
  364. $this->tokenProvider->method('getTokenById')
  365. ->with(1337)
  366. ->willThrowException(new ExpiredTokenException($appToken));
  367. $this->accessTokenMapper->expects($this->never())
  368. ->method('delete')
  369. ->with($accessToken);
  370. $this->secureRandom->method('generate')
  371. ->willReturnCallback(function ($len) {
  372. return 'random'.$len;
  373. });
  374. $this->tokenProvider->expects($this->once())
  375. ->method('rotate')
  376. ->with(
  377. $appToken,
  378. 'decryptedToken',
  379. 'random72'
  380. )->willReturn($appToken);
  381. $this->time->method('getTime')
  382. ->willReturn(1000);
  383. $this->tokenProvider->expects($this->once())
  384. ->method('updateToken')
  385. ->with(
  386. $this->callback(function (PublicKeyToken $token) {
  387. return $token->getExpires() === 4600;
  388. })
  389. );
  390. $this->crypto->method('encrypt')
  391. ->with('random72', 'random128')
  392. ->willReturn('newEncryptedToken');
  393. $this->accessTokenMapper->expects($this->once())
  394. ->method('update')
  395. ->with(
  396. $this->callback(function (AccessToken $token) {
  397. return $token->getHashedCode() === hash('sha512', 'random128') &&
  398. $token->getEncryptedToken() === 'newEncryptedToken';
  399. })
  400. );
  401. $expected = new JSONResponse([
  402. 'access_token' => 'random72',
  403. 'token_type' => 'Bearer',
  404. 'expires_in' => 3600,
  405. 'refresh_token' => 'random128',
  406. 'user_id' => 'userId',
  407. ]);
  408. $this->request->server['PHP_AUTH_USER'] = 'clientId';
  409. $this->request->server['PHP_AUTH_PW'] = 'clientSecret';
  410. $this->request->method('getRemoteAddress')
  411. ->willReturn('1.2.3.4');
  412. $this->throttler->expects($this->once())
  413. ->method('resetDelay')
  414. ->with(
  415. '1.2.3.4',
  416. 'login',
  417. ['user' => 'userId']
  418. );
  419. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', null, null));
  420. }
  421. public function testRefreshTokenExpiredAppToken(): void {
  422. $accessToken = new AccessToken();
  423. $accessToken->setClientId(42);
  424. $accessToken->setTokenId(1337);
  425. $accessToken->setEncryptedToken('encryptedToken');
  426. $this->accessTokenMapper->method('getByCode')
  427. ->with('validrefresh')
  428. ->willReturn($accessToken);
  429. $client = new Client();
  430. $client->setClientIdentifier('clientId');
  431. $client->setSecret(bin2hex('hashedClientSecret'));
  432. $this->clientMapper->method('getByUid')
  433. ->with(42)
  434. ->willReturn($client);
  435. $this->crypto
  436. ->method('decrypt')
  437. ->with('encryptedToken')
  438. ->willReturn('decryptedToken');
  439. $this->crypto
  440. ->method('calculateHMAC')
  441. ->with('clientSecret')
  442. ->willReturn('hashedClientSecret');
  443. $appToken = new PublicKeyToken();
  444. $appToken->setUid('userId');
  445. $this->tokenProvider->method('getTokenById')
  446. ->with(1337)
  447. ->willReturn($appToken);
  448. $this->accessTokenMapper->expects($this->never())
  449. ->method('delete')
  450. ->with($accessToken);
  451. $this->secureRandom->method('generate')
  452. ->willReturnCallback(function ($len) {
  453. return 'random'.$len;
  454. });
  455. $this->tokenProvider->expects($this->once())
  456. ->method('rotate')
  457. ->with(
  458. $appToken,
  459. 'decryptedToken',
  460. 'random72'
  461. )->willReturn($appToken);
  462. $this->time->method('getTime')
  463. ->willReturn(1000);
  464. $this->tokenProvider->expects($this->once())
  465. ->method('updateToken')
  466. ->with(
  467. $this->callback(function (PublicKeyToken $token) {
  468. return $token->getExpires() === 4600;
  469. })
  470. );
  471. $this->crypto->method('encrypt')
  472. ->with('random72', 'random128')
  473. ->willReturn('newEncryptedToken');
  474. $this->accessTokenMapper->expects($this->once())
  475. ->method('update')
  476. ->with(
  477. $this->callback(function (AccessToken $token) {
  478. return $token->getHashedCode() === hash('sha512', 'random128') &&
  479. $token->getEncryptedToken() === 'newEncryptedToken';
  480. })
  481. );
  482. $expected = new JSONResponse([
  483. 'access_token' => 'random72',
  484. 'token_type' => 'Bearer',
  485. 'expires_in' => 3600,
  486. 'refresh_token' => 'random128',
  487. 'user_id' => 'userId',
  488. ]);
  489. $this->request->method('getRemoteAddress')
  490. ->willReturn('1.2.3.4');
  491. $this->throttler->expects($this->once())
  492. ->method('resetDelay')
  493. ->with(
  494. '1.2.3.4',
  495. 'login',
  496. ['user' => 'userId']
  497. );
  498. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', 'clientId', 'clientSecret'));
  499. }
  500. }