OauthApiControllerTest.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Lukas Reschke <lukas@statuscode.ch>
  7. * @author Morris Jobke <hey@morrisjobke.de>
  8. * @author Roeland Jago Douma <roeland@famdouma.nl>
  9. *
  10. * @license GNU AGPL version 3 or any later version
  11. *
  12. * This program is free software: you can redistribute it and/or modify
  13. * it under the terms of the GNU Affero General Public License as
  14. * published by the Free Software Foundation, either version 3 of the
  15. * License, or (at your option) any later version.
  16. *
  17. * This program is distributed in the hope that it will be useful,
  18. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. * GNU Affero General Public License for more details.
  21. *
  22. * You should have received a copy of the GNU Affero General Public License
  23. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  24. *
  25. */
  26. namespace OCA\OAuth2\Tests\Controller;
  27. use OC\Authentication\Exceptions\ExpiredTokenException;
  28. use OC\Authentication\Exceptions\InvalidTokenException;
  29. use OC\Authentication\Token\IProvider as TokenProvider;
  30. use OC\Authentication\Token\PublicKeyToken;
  31. use OC\Security\Bruteforce\Throttler;
  32. use OCA\OAuth2\Controller\OauthApiController;
  33. use OCA\OAuth2\Db\AccessToken;
  34. use OCA\OAuth2\Db\AccessTokenMapper;
  35. use OCA\OAuth2\Db\Client;
  36. use OCA\OAuth2\Db\ClientMapper;
  37. use OCA\OAuth2\Exceptions\AccessTokenNotFoundException;
  38. use OCA\OAuth2\Exceptions\ClientNotFoundException;
  39. use OCP\AppFramework\Http;
  40. use OCP\AppFramework\Http\JSONResponse;
  41. use OCP\AppFramework\Utility\ITimeFactory;
  42. use OCP\IRequest;
  43. use OCP\Security\ICrypto;
  44. use OCP\Security\ISecureRandom;
  45. use Test\TestCase;
  46. class OauthApiControllerTest extends TestCase {
  47. /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
  48. private $request;
  49. /** @var ICrypto|\PHPUnit\Framework\MockObject\MockObject */
  50. private $crypto;
  51. /** @var AccessTokenMapper|\PHPUnit\Framework\MockObject\MockObject */
  52. private $accessTokenMapper;
  53. /** @var ClientMapper|\PHPUnit\Framework\MockObject\MockObject */
  54. private $clientMapper;
  55. /** @var TokenProvider|\PHPUnit\Framework\MockObject\MockObject */
  56. private $tokenProvider;
  57. /** @var ISecureRandom|\PHPUnit\Framework\MockObject\MockObject */
  58. private $secureRandom;
  59. /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
  60. private $time;
  61. /** @var Throttler|\PHPUnit\Framework\MockObject\MockObject */
  62. private $throttler;
  63. /** @var OauthApiController */
  64. private $oauthApiController;
  65. protected function setUp(): void {
  66. parent::setUp();
  67. $this->request = $this->createMock(IRequest::class);
  68. $this->crypto = $this->createMock(ICrypto::class);
  69. $this->accessTokenMapper = $this->createMock(AccessTokenMapper::class);
  70. $this->clientMapper = $this->createMock(ClientMapper::class);
  71. $this->tokenProvider = $this->createMock(TokenProvider::class);
  72. $this->secureRandom = $this->createMock(ISecureRandom::class);
  73. $this->time = $this->createMock(ITimeFactory::class);
  74. $this->throttler = $this->createMock(Throttler::class);
  75. $this->oauthApiController = new OauthApiController(
  76. 'oauth2',
  77. $this->request,
  78. $this->crypto,
  79. $this->accessTokenMapper,
  80. $this->clientMapper,
  81. $this->tokenProvider,
  82. $this->secureRandom,
  83. $this->time,
  84. $this->throttler
  85. );
  86. }
  87. public function testGetTokenInvalidGrantType() {
  88. $expected = new JSONResponse([
  89. 'error' => 'invalid_grant',
  90. ], Http::STATUS_BAD_REQUEST);
  91. $expected->throttle(['invalid_grant' => 'foo']);
  92. $this->assertEquals($expected, $this->oauthApiController->getToken('foo', null, null, null, null));
  93. }
  94. public function testGetTokenInvalidCode() {
  95. $expected = new JSONResponse([
  96. 'error' => 'invalid_request',
  97. ], Http::STATUS_BAD_REQUEST);
  98. $expected->throttle(['invalid_request' => 'token not found', 'code' => 'invalidcode']);
  99. $this->accessTokenMapper->method('getByCode')
  100. ->with('invalidcode')
  101. ->willThrowException(new AccessTokenNotFoundException());
  102. $this->assertEquals($expected, $this->oauthApiController->getToken('authorization_code', 'invalidcode', null, null, null));
  103. }
  104. public function testGetTokenInvalidRefreshToken() {
  105. $expected = new JSONResponse([
  106. 'error' => 'invalid_request',
  107. ], Http::STATUS_BAD_REQUEST);
  108. $expected->throttle(['invalid_request' => 'token not found', 'code' => 'invalidrefresh']);
  109. $this->accessTokenMapper->method('getByCode')
  110. ->with('invalidrefresh')
  111. ->willThrowException(new AccessTokenNotFoundException());
  112. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'invalidrefresh', null, null));
  113. }
  114. public function testGetTokenClientDoesNotExist() {
  115. $expected = new JSONResponse([
  116. 'error' => 'invalid_request',
  117. ], Http::STATUS_BAD_REQUEST);
  118. $expected->throttle(['invalid_request' => 'client not found', 'client_id' => 42]);
  119. $accessToken = new AccessToken();
  120. $accessToken->setClientId(42);
  121. $this->accessTokenMapper->method('getByCode')
  122. ->with('validrefresh')
  123. ->willReturn($accessToken);
  124. $this->clientMapper->method('getByUid')
  125. ->with(42)
  126. ->willThrowException(new ClientNotFoundException());
  127. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', null, null));
  128. }
  129. public function invalidClientProvider() {
  130. return [
  131. ['invalidClientId', 'invalidClientSecret'],
  132. ['clientId', 'invalidClientSecret'],
  133. ['invalidClientId', 'clientSecret'],
  134. ];
  135. }
  136. /**
  137. * @dataProvider invalidClientProvider
  138. *
  139. * @param string $clientId
  140. * @param string $clientSecret
  141. */
  142. public function testGetTokenInvalidClient($clientId, $clientSecret) {
  143. $expected = new JSONResponse([
  144. 'error' => 'invalid_client',
  145. ], Http::STATUS_BAD_REQUEST);
  146. $expected->throttle(['invalid_client' => 'client ID or secret does not match']);
  147. $accessToken = new AccessToken();
  148. $accessToken->setClientId(42);
  149. $this->accessTokenMapper->method('getByCode')
  150. ->with('validrefresh')
  151. ->willReturn($accessToken);
  152. $client = new Client();
  153. $client->setClientIdentifier('clientId');
  154. $client->setSecret('clientSecret');
  155. $this->clientMapper->method('getByUid')
  156. ->with(42)
  157. ->willReturn($client);
  158. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', $clientId, $clientSecret));
  159. }
  160. public function testGetTokenInvalidAppToken() {
  161. $expected = new JSONResponse([
  162. 'error' => 'invalid_request',
  163. ], Http::STATUS_BAD_REQUEST);
  164. $expected->throttle(['invalid_request' => 'token is invalid']);
  165. $accessToken = new AccessToken();
  166. $accessToken->setClientId(42);
  167. $accessToken->setTokenId(1337);
  168. $accessToken->setEncryptedToken('encryptedToken');
  169. $this->accessTokenMapper->method('getByCode')
  170. ->with('validrefresh')
  171. ->willReturn($accessToken);
  172. $client = new Client();
  173. $client->setClientIdentifier('clientId');
  174. $client->setSecret('clientSecret');
  175. $this->clientMapper->method('getByUid')
  176. ->with(42)
  177. ->willReturn($client);
  178. $this->crypto->method('decrypt')
  179. ->with(
  180. 'encryptedToken',
  181. 'validrefresh'
  182. )->willReturn('decryptedToken');
  183. $this->tokenProvider->method('getTokenById')
  184. ->with(1337)
  185. ->willThrowException(new InvalidTokenException());
  186. $this->accessTokenMapper->expects($this->once())
  187. ->method('delete')
  188. ->with($accessToken);
  189. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', 'clientId', 'clientSecret'));
  190. }
  191. public function testGetTokenValidAppToken() {
  192. $accessToken = new AccessToken();
  193. $accessToken->setClientId(42);
  194. $accessToken->setTokenId(1337);
  195. $accessToken->setEncryptedToken('encryptedToken');
  196. $this->accessTokenMapper->method('getByCode')
  197. ->with('validrefresh')
  198. ->willReturn($accessToken);
  199. $client = new Client();
  200. $client->setClientIdentifier('clientId');
  201. $client->setSecret('clientSecret');
  202. $this->clientMapper->method('getByUid')
  203. ->with(42)
  204. ->willReturn($client);
  205. $this->crypto->method('decrypt')
  206. ->with(
  207. 'encryptedToken',
  208. 'validrefresh'
  209. )->willReturn('decryptedToken');
  210. $appToken = new PublicKeyToken();
  211. $appToken->setUid('userId');
  212. $this->tokenProvider->method('getTokenById')
  213. ->with(1337)
  214. ->willThrowException(new ExpiredTokenException($appToken));
  215. $this->accessTokenMapper->expects($this->never())
  216. ->method('delete')
  217. ->with($accessToken);
  218. $this->secureRandom->method('generate')
  219. ->willReturnCallback(function ($len) {
  220. return 'random'.$len;
  221. });
  222. $this->tokenProvider->expects($this->once())
  223. ->method('rotate')
  224. ->with(
  225. $appToken,
  226. 'decryptedToken',
  227. 'random72'
  228. )->willReturn($appToken);
  229. $this->time->method('getTime')
  230. ->willReturn(1000);
  231. $this->tokenProvider->expects($this->once())
  232. ->method('updateToken')
  233. ->with(
  234. $this->callback(function (PublicKeyToken $token) {
  235. return $token->getExpires() === 4600;
  236. })
  237. );
  238. $this->crypto->method('encrypt')
  239. ->with('random72', 'random128')
  240. ->willReturn('newEncryptedToken');
  241. $this->accessTokenMapper->expects($this->once())
  242. ->method('update')
  243. ->with(
  244. $this->callback(function (AccessToken $token) {
  245. return $token->getHashedCode() === hash('sha512', 'random128') &&
  246. $token->getEncryptedToken() === 'newEncryptedToken';
  247. })
  248. );
  249. $expected = new JSONResponse([
  250. 'access_token' => 'random72',
  251. 'token_type' => 'Bearer',
  252. 'expires_in' => 3600,
  253. 'refresh_token' => 'random128',
  254. 'user_id' => 'userId',
  255. ]);
  256. $this->request->method('getRemoteAddress')
  257. ->willReturn('1.2.3.4');
  258. $this->throttler->expects($this->once())
  259. ->method('resetDelay')
  260. ->with(
  261. '1.2.3.4',
  262. 'login',
  263. ['user' => 'userId']
  264. );
  265. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', 'clientId', 'clientSecret'));
  266. }
  267. public function testGetTokenValidAppTokenBasicAuth() {
  268. $accessToken = new AccessToken();
  269. $accessToken->setClientId(42);
  270. $accessToken->setTokenId(1337);
  271. $accessToken->setEncryptedToken('encryptedToken');
  272. $this->accessTokenMapper->method('getByCode')
  273. ->with('validrefresh')
  274. ->willReturn($accessToken);
  275. $client = new Client();
  276. $client->setClientIdentifier('clientId');
  277. $client->setSecret('clientSecret');
  278. $this->clientMapper->method('getByUid')
  279. ->with(42)
  280. ->willReturn($client);
  281. $this->crypto->method('decrypt')
  282. ->with(
  283. 'encryptedToken',
  284. 'validrefresh'
  285. )->willReturn('decryptedToken');
  286. $appToken = new PublicKeyToken();
  287. $appToken->setUid('userId');
  288. $this->tokenProvider->method('getTokenById')
  289. ->with(1337)
  290. ->willThrowException(new ExpiredTokenException($appToken));
  291. $this->accessTokenMapper->expects($this->never())
  292. ->method('delete')
  293. ->with($accessToken);
  294. $this->secureRandom->method('generate')
  295. ->willReturnCallback(function ($len) {
  296. return 'random'.$len;
  297. });
  298. $this->tokenProvider->expects($this->once())
  299. ->method('rotate')
  300. ->with(
  301. $appToken,
  302. 'decryptedToken',
  303. 'random72'
  304. )->willReturn($appToken);
  305. $this->time->method('getTime')
  306. ->willReturn(1000);
  307. $this->tokenProvider->expects($this->once())
  308. ->method('updateToken')
  309. ->with(
  310. $this->callback(function (PublicKeyToken $token) {
  311. return $token->getExpires() === 4600;
  312. })
  313. );
  314. $this->crypto->method('encrypt')
  315. ->with('random72', 'random128')
  316. ->willReturn('newEncryptedToken');
  317. $this->accessTokenMapper->expects($this->once())
  318. ->method('update')
  319. ->with(
  320. $this->callback(function (AccessToken $token) {
  321. return $token->getHashedCode() === hash('sha512', 'random128') &&
  322. $token->getEncryptedToken() === 'newEncryptedToken';
  323. })
  324. );
  325. $expected = new JSONResponse([
  326. 'access_token' => 'random72',
  327. 'token_type' => 'Bearer',
  328. 'expires_in' => 3600,
  329. 'refresh_token' => 'random128',
  330. 'user_id' => 'userId',
  331. ]);
  332. $this->request->server['PHP_AUTH_USER'] = 'clientId';
  333. $this->request->server['PHP_AUTH_PW'] = 'clientSecret';
  334. $this->request->method('getRemoteAddress')
  335. ->willReturn('1.2.3.4');
  336. $this->throttler->expects($this->once())
  337. ->method('resetDelay')
  338. ->with(
  339. '1.2.3.4',
  340. 'login',
  341. ['user' => 'userId']
  342. );
  343. $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', null, null));
  344. }
  345. public function testGetTokenExpiredAppToken() {
  346. $accessToken = new AccessToken();
  347. $accessToken->setClientId(42);
  348. $accessToken->setTokenId(1337);
  349. $accessToken->setEncryptedToken('encryptedToken');
  350. $this->accessTokenMapper->method('getByCode')
  351. ->with('validrefresh')
  352. ->willReturn($accessToken);
  353. $client = new Client();
  354. $client->setClientIdentifier('clientId');
  355. $client->setSecret('clientSecret');
  356. $this->clientMapper->method('getByUid')
  357. ->with(42)
  358. ->willReturn($client);
  359. $this->crypto->method('decrypt')
  360. ->with(
  361. 'encryptedToken',
  362. 'validrefresh'
  363. )->willReturn('decryptedToken');
  364. $appToken = new PublicKeyToken();
  365. $appToken->setUid('userId');
  366. $this->tokenProvider->method('getTokenById')
  367. ->with(1337)
  368. ->willReturn($appToken);
  369. $this->accessTokenMapper->expects($this->never())
  370. ->method('delete')
  371. ->with($accessToken);
  372. $this->secureRandom->method('generate')
  373. ->willReturnCallback(function ($len) {
  374. return 'random'.$len;
  375. });
  376. $this->tokenProvider->expects($this->once())
  377. ->method('rotate')
  378. ->with(
  379. $appToken,
  380. 'decryptedToken',
  381. 'random72'
  382. )->willReturn($appToken);
  383. $this->time->method('getTime')
  384. ->willReturn(1000);
  385. $this->tokenProvider->expects($this->once())
  386. ->method('updateToken')
  387. ->with(
  388. $this->callback(function (PublicKeyToken $token) {
  389. return $token->getExpires() === 4600;
  390. })
  391. );
  392. $this->crypto->method('encrypt')
  393. ->with('random72', 'random128')
  394. ->willReturn('newEncryptedToken');
  395. $this->accessTokenMapper->expects($this->once())
  396. ->method('update')
  397. ->with(
  398. $this->callback(function (AccessToken $token) {
  399. return $token->getHashedCode() === hash('sha512', 'random128') &&
  400. $token->getEncryptedToken() === 'newEncryptedToken';
  401. })
  402. );
  403. $expected = new JSONResponse([
  404. 'access_token' => 'random72',
  405. 'token_type' => 'Bearer',
  406. 'expires_in' => 3600,
  407. 'refresh_token' => 'random128',
  408. 'user_id' => 'userId',
  409. ]);
  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', 'clientId', 'clientSecret'));
  420. }
  421. }