LoginControllerTest.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @author Lukas Reschke <lukas@owncloud.com>
  5. *
  6. * @copyright Copyright (c) 2016, ownCloud, Inc.
  7. * @license AGPL-3.0
  8. *
  9. * This code is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License, version 3,
  11. * as published by the Free Software Foundation.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License, version 3,
  19. * along with this program. If not, see <http://www.gnu.org/licenses/>
  20. *
  21. */
  22. namespace Tests\Core\Controller;
  23. use OC\Authentication\Login\Chain as LoginChain;
  24. use OC\Authentication\Login\LoginData;
  25. use OC\Authentication\Login\LoginResult;
  26. use OC\Authentication\TwoFactorAuth\Manager;
  27. use OC\Core\Controller\LoginController;
  28. use OC\Security\Bruteforce\Throttler;
  29. use OC\User\Session;
  30. use OCP\AppFramework\Http\RedirectResponse;
  31. use OCP\AppFramework\Http\TemplateResponse;
  32. use OCP\Defaults;
  33. use OCP\IConfig;
  34. use OCP\IInitialStateService;
  35. use OCP\IL10N;
  36. use OCP\IRequest;
  37. use OCP\ISession;
  38. use OCP\IURLGenerator;
  39. use OCP\IUser;
  40. use OCP\IUserManager;
  41. use OCP\Notification\IManager;
  42. use PHPUnit\Framework\MockObject\MockObject;
  43. use Test\TestCase;
  44. class LoginControllerTest extends TestCase {
  45. /** @var LoginController */
  46. private $loginController;
  47. /** @var IRequest|MockObject */
  48. private $request;
  49. /** @var IUserManager|MockObject */
  50. private $userManager;
  51. /** @var IConfig|MockObject */
  52. private $config;
  53. /** @var ISession|MockObject */
  54. private $session;
  55. /** @var Session|MockObject */
  56. private $userSession;
  57. /** @var IURLGenerator|MockObject */
  58. private $urlGenerator;
  59. /** @var Manager|MockObject */
  60. private $twoFactorManager;
  61. /** @var Defaults|MockObject */
  62. private $defaults;
  63. /** @var Throttler|MockObject */
  64. private $throttler;
  65. /** @var IInitialStateService|MockObject */
  66. private $initialStateService;
  67. /** @var \OC\Authentication\WebAuthn\Manager|MockObject */
  68. private $webAuthnManager;
  69. /** @var IManager|MockObject */
  70. private $notificationManager;
  71. /** @var IL10N|MockObject */
  72. private $l;
  73. protected function setUp(): void {
  74. parent::setUp();
  75. $this->request = $this->createMock(IRequest::class);
  76. $this->userManager = $this->createMock(\OC\User\Manager::class);
  77. $this->config = $this->createMock(IConfig::class);
  78. $this->session = $this->createMock(ISession::class);
  79. $this->userSession = $this->createMock(Session::class);
  80. $this->urlGenerator = $this->createMock(IURLGenerator::class);
  81. $this->twoFactorManager = $this->createMock(Manager::class);
  82. $this->defaults = $this->createMock(Defaults::class);
  83. $this->throttler = $this->createMock(Throttler::class);
  84. $this->initialStateService = $this->createMock(IInitialStateService::class);
  85. $this->webAuthnManager = $this->createMock(\OC\Authentication\WebAuthn\Manager::class);
  86. $this->notificationManager = $this->createMock(IManager::class);
  87. $this->l = $this->createMock(IL10N::class);
  88. $this->l->expects($this->any())
  89. ->method('t')
  90. ->willReturnCallback(function ($text, $parameters = []) {
  91. return vsprintf($text, $parameters);
  92. });
  93. $this->request->method('getRemoteAddress')
  94. ->willReturn('1.2.3.4');
  95. $this->throttler->method('getDelay')
  96. ->with(
  97. $this->equalTo('1.2.3.4'),
  98. $this->equalTo('')
  99. )->willReturn(1000);
  100. $this->loginController = new LoginController(
  101. 'core',
  102. $this->request,
  103. $this->userManager,
  104. $this->config,
  105. $this->session,
  106. $this->userSession,
  107. $this->urlGenerator,
  108. $this->defaults,
  109. $this->throttler,
  110. $this->initialStateService,
  111. $this->webAuthnManager,
  112. $this->notificationManager,
  113. $this->l
  114. );
  115. }
  116. public function testLogoutWithoutToken() {
  117. $this->request
  118. ->expects($this->once())
  119. ->method('getCookie')
  120. ->with('nc_token')
  121. ->willReturn(null);
  122. $this->request
  123. ->expects($this->once())
  124. ->method('isUserAgent')
  125. ->willReturn(false);
  126. $this->config
  127. ->expects($this->never())
  128. ->method('deleteUserValue');
  129. $this->urlGenerator
  130. ->expects($this->once())
  131. ->method('linkToRouteAbsolute')
  132. ->with('core.login.showLoginForm')
  133. ->willReturn('/login');
  134. $expected = new RedirectResponse('/login');
  135. $expected->addHeader('Clear-Site-Data', '"cache", "storage"');
  136. $this->assertEquals($expected, $this->loginController->logout());
  137. }
  138. public function testLogoutNoClearSiteData() {
  139. $this->request
  140. ->expects($this->once())
  141. ->method('getCookie')
  142. ->with('nc_token')
  143. ->willReturn(null);
  144. $this->request
  145. ->expects($this->once())
  146. ->method('isUserAgent')
  147. ->willReturn(true);
  148. $this->urlGenerator
  149. ->expects($this->once())
  150. ->method('linkToRouteAbsolute')
  151. ->with('core.login.showLoginForm')
  152. ->willReturn('/login');
  153. $expected = new RedirectResponse('/login');
  154. $this->assertEquals($expected, $this->loginController->logout());
  155. }
  156. public function testLogoutWithToken() {
  157. $this->request
  158. ->expects($this->once())
  159. ->method('getCookie')
  160. ->with('nc_token')
  161. ->willReturn('MyLoginToken');
  162. $this->request
  163. ->expects($this->once())
  164. ->method('isUserAgent')
  165. ->willReturn(false);
  166. $user = $this->createMock(IUser::class);
  167. $user
  168. ->expects($this->once())
  169. ->method('getUID')
  170. ->willReturn('JohnDoe');
  171. $this->userSession
  172. ->expects($this->once())
  173. ->method('getUser')
  174. ->willReturn($user);
  175. $this->config
  176. ->expects($this->once())
  177. ->method('deleteUserValue')
  178. ->with('JohnDoe', 'login_token', 'MyLoginToken');
  179. $this->urlGenerator
  180. ->expects($this->once())
  181. ->method('linkToRouteAbsolute')
  182. ->with('core.login.showLoginForm')
  183. ->willReturn('/login');
  184. $expected = new RedirectResponse('/login');
  185. $expected->addHeader('Clear-Site-Data', '"cache", "storage"');
  186. $this->assertEquals($expected, $this->loginController->logout());
  187. }
  188. public function testShowLoginFormForLoggedInUsers() {
  189. $this->userSession
  190. ->expects($this->once())
  191. ->method('isLoggedIn')
  192. ->willReturn(true);
  193. $this->urlGenerator
  194. ->expects($this->once())
  195. ->method('linkToDefaultPageUrl')
  196. ->willReturn('/default/foo');
  197. $expectedResponse = new RedirectResponse('/default/foo');
  198. $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', ''));
  199. }
  200. public function testShowLoginFormWithErrorsInSession() {
  201. $this->userSession
  202. ->expects($this->once())
  203. ->method('isLoggedIn')
  204. ->willReturn(false);
  205. $this->session
  206. ->expects($this->once())
  207. ->method('get')
  208. ->with('loginMessages')
  209. ->willReturn(
  210. [
  211. [
  212. 'ErrorArray1',
  213. 'ErrorArray2',
  214. ],
  215. [
  216. 'MessageArray1',
  217. 'MessageArray2',
  218. ],
  219. ]
  220. );
  221. $this->initialStateService->expects($this->exactly(11))
  222. ->method('provideInitialState')
  223. ->withConsecutive([
  224. 'core',
  225. 'loginMessages',
  226. [
  227. 'MessageArray1',
  228. 'MessageArray2',
  229. 'This community release of Nextcloud is unsupported and push notifications are limited.',
  230. ],
  231. ],
  232. [
  233. 'core',
  234. 'loginErrors',
  235. [
  236. 'ErrorArray1',
  237. 'ErrorArray2',
  238. ],
  239. ],
  240. [
  241. 'core',
  242. 'loginUsername',
  243. '',
  244. ]);
  245. $expectedResponse = new TemplateResponse(
  246. 'core',
  247. 'login',
  248. [
  249. 'alt_login' => [],
  250. 'pageTitle' => 'Login'
  251. ],
  252. 'guest'
  253. );
  254. $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', ''));
  255. }
  256. public function testShowLoginFormForFlowAuth() {
  257. $this->userSession
  258. ->expects($this->once())
  259. ->method('isLoggedIn')
  260. ->willReturn(false);
  261. $this->initialStateService->expects($this->exactly(12))
  262. ->method('provideInitialState')
  263. ->withConsecutive([], [], [], [
  264. 'core',
  265. 'loginAutocomplete',
  266. false
  267. ], [
  268. 'core',
  269. 'loginRedirectUrl',
  270. 'login/flow'
  271. ]);
  272. $expectedResponse = new TemplateResponse(
  273. 'core',
  274. 'login',
  275. [
  276. 'alt_login' => [],
  277. 'pageTitle' => 'Login'
  278. ],
  279. 'guest'
  280. );
  281. $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', 'login/flow'));
  282. }
  283. /**
  284. * @return array
  285. */
  286. public function passwordResetDataProvider(): array {
  287. return [
  288. [
  289. true,
  290. true,
  291. ],
  292. [
  293. false,
  294. false,
  295. ],
  296. ];
  297. }
  298. /**
  299. * @dataProvider passwordResetDataProvider
  300. */
  301. public function testShowLoginFormWithPasswordResetOption($canChangePassword,
  302. $expectedResult) {
  303. $this->userSession
  304. ->expects($this->once())
  305. ->method('isLoggedIn')
  306. ->willReturn(false);
  307. $this->config
  308. ->expects(self::once())
  309. ->method('getSystemValue')
  310. ->willReturnMap([
  311. ['login_form_autocomplete', true, true],
  312. ]);
  313. $this->config
  314. ->expects(self::once())
  315. ->method('getSystemValueString')
  316. ->willReturnMap([
  317. ['lost_password_link', '', ''],
  318. ]);
  319. $user = $this->createMock(IUser::class);
  320. $user
  321. ->expects($this->once())
  322. ->method('canChangePassword')
  323. ->willReturn($canChangePassword);
  324. $this->userManager
  325. ->expects($this->once())
  326. ->method('get')
  327. ->with('LdapUser')
  328. ->willReturn($user);
  329. $this->initialStateService->expects($this->exactly(11))
  330. ->method('provideInitialState')
  331. ->withConsecutive([], [], [
  332. 'core',
  333. 'loginUsername',
  334. 'LdapUser'
  335. ], [], [], [], [
  336. 'core',
  337. 'loginCanResetPassword',
  338. $expectedResult
  339. ]);
  340. $expectedResponse = new TemplateResponse(
  341. 'core',
  342. 'login',
  343. [
  344. 'alt_login' => [],
  345. 'pageTitle' => 'Login'
  346. ],
  347. 'guest'
  348. );
  349. $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('LdapUser', ''));
  350. }
  351. public function testShowLoginFormForUserNamed0() {
  352. $this->userSession
  353. ->expects($this->once())
  354. ->method('isLoggedIn')
  355. ->willReturn(false);
  356. $this->config
  357. ->expects(self::once())
  358. ->method('getSystemValue')
  359. ->willReturnMap([
  360. ['login_form_autocomplete', true, true],
  361. ]);
  362. $this->config
  363. ->expects(self::once())
  364. ->method('getSystemValueString')
  365. ->willReturnMap([
  366. ['lost_password_link', '', ''],
  367. ]);
  368. $user = $this->createMock(IUser::class);
  369. $user->expects($this->once())
  370. ->method('canChangePassword')
  371. ->willReturn(false);
  372. $this->userManager
  373. ->expects($this->once())
  374. ->method('get')
  375. ->with('0')
  376. ->willReturn($user);
  377. $this->initialStateService->expects($this->exactly(11))
  378. ->method('provideInitialState')
  379. ->withConsecutive([], [], [], [
  380. 'core',
  381. 'loginAutocomplete',
  382. true
  383. ], [], [
  384. 'core',
  385. 'loginResetPasswordLink',
  386. false
  387. ], [
  388. 'core',
  389. 'loginCanResetPassword',
  390. false
  391. ]);
  392. $expectedResponse = new TemplateResponse(
  393. 'core',
  394. 'login',
  395. [
  396. 'alt_login' => [],
  397. 'pageTitle' => 'Login'
  398. ],
  399. 'guest'
  400. );
  401. $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('0', ''));
  402. }
  403. public function testLoginWithInvalidCredentials(): void {
  404. $user = 'MyUserName';
  405. $password = 'secret';
  406. $loginPageUrl = '/login?redirect_url=/apps/files';
  407. $loginChain = $this->createMock(LoginChain::class);
  408. $this->request
  409. ->expects($this->once())
  410. ->method('passesCSRFCheck')
  411. ->willReturn(true);
  412. $loginData = new LoginData(
  413. $this->request,
  414. $user,
  415. $password,
  416. '/apps/files'
  417. );
  418. $loginResult = LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD);
  419. $loginChain->expects($this->once())
  420. ->method('process')
  421. ->with($this->equalTo($loginData))
  422. ->willReturn($loginResult);
  423. $this->urlGenerator->expects($this->once())
  424. ->method('linkToRoute')
  425. ->with('core.login.showLoginForm', [
  426. 'user' => $user,
  427. 'redirect_url' => '/apps/files',
  428. 'direct' => 1,
  429. ])
  430. ->willReturn($loginPageUrl);
  431. $expected = new RedirectResponse($loginPageUrl);
  432. $expected->throttle(['user' => 'MyUserName']);
  433. $response = $this->loginController->tryLogin($loginChain, $user, $password, '/apps/files');
  434. $this->assertEquals($expected, $response);
  435. }
  436. public function testLoginWithValidCredentials() {
  437. $user = 'MyUserName';
  438. $password = 'secret';
  439. $loginChain = $this->createMock(LoginChain::class);
  440. $this->request
  441. ->expects($this->once())
  442. ->method('passesCSRFCheck')
  443. ->willReturn(true);
  444. $loginData = new LoginData(
  445. $this->request,
  446. $user,
  447. $password
  448. );
  449. $loginResult = LoginResult::success($loginData);
  450. $loginChain->expects($this->once())
  451. ->method('process')
  452. ->with($this->equalTo($loginData))
  453. ->willReturn($loginResult);
  454. $this->urlGenerator
  455. ->expects($this->once())
  456. ->method('linkToDefaultPageUrl')
  457. ->willReturn('/default/foo');
  458. $expected = new RedirectResponse('/default/foo');
  459. $this->assertEquals($expected, $this->loginController->tryLogin($loginChain, $user, $password));
  460. }
  461. public function testLoginWithoutPassedCsrfCheckAndNotLoggedIn(): void {
  462. /** @var IUser|MockObject $user */
  463. $user = $this->createMock(IUser::class);
  464. $user->expects($this->any())
  465. ->method('getUID')
  466. ->willReturn('jane');
  467. $password = 'secret';
  468. $originalUrl = 'another%20url';
  469. $loginChain = $this->createMock(LoginChain::class);
  470. $this->request
  471. ->expects($this->once())
  472. ->method('passesCSRFCheck')
  473. ->willReturn(false);
  474. $this->userSession
  475. ->method('isLoggedIn')
  476. ->with()
  477. ->willReturn(false);
  478. $this->config->expects($this->never())
  479. ->method('deleteUserValue');
  480. $this->userSession->expects($this->never())
  481. ->method('createRememberMeToken');
  482. $response = $this->loginController->tryLogin($loginChain, 'Jane', $password, $originalUrl);
  483. $expected = new RedirectResponse('');
  484. $expected->throttle(['user' => 'Jane']);
  485. $this->assertEquals($expected, $response);
  486. }
  487. public function testLoginWithoutPassedCsrfCheckAndLoggedIn() {
  488. /** @var IUser|MockObject $user */
  489. $user = $this->createMock(IUser::class);
  490. $user->expects($this->any())
  491. ->method('getUID')
  492. ->willReturn('jane');
  493. $password = 'secret';
  494. $originalUrl = 'another url';
  495. $redirectUrl = 'http://localhost/another url';
  496. $loginChain = $this->createMock(LoginChain::class);
  497. $this->request
  498. ->expects($this->once())
  499. ->method('passesCSRFCheck')
  500. ->willReturn(false);
  501. $this->userSession
  502. ->method('isLoggedIn')
  503. ->with()
  504. ->willReturn(true);
  505. $this->urlGenerator->expects($this->once())
  506. ->method('getAbsoluteURL')
  507. ->with(urldecode($originalUrl))
  508. ->willReturn($redirectUrl);
  509. $this->config->expects($this->never())
  510. ->method('deleteUserValue');
  511. $this->userSession->expects($this->never())
  512. ->method('createRememberMeToken');
  513. $this->config
  514. ->method('getSystemValue')
  515. ->with('remember_login_cookie_lifetime')
  516. ->willReturn(1234);
  517. $response = $this->loginController->tryLogin($loginChain, 'Jane', $password, $originalUrl);
  518. $expected = new RedirectResponse($redirectUrl);
  519. $this->assertEquals($expected, $response);
  520. }
  521. public function testLoginWithValidCredentialsAndRedirectUrl() {
  522. $user = 'MyUserName';
  523. $password = 'secret';
  524. $redirectUrl = 'https://next.cloud/apps/mail';
  525. $loginChain = $this->createMock(LoginChain::class);
  526. $this->request
  527. ->expects($this->once())
  528. ->method('passesCSRFCheck')
  529. ->willReturn(true);
  530. $loginData = new LoginData(
  531. $this->request,
  532. $user,
  533. $password,
  534. '/apps/mail'
  535. );
  536. $loginResult = LoginResult::success($loginData);
  537. $loginChain->expects($this->once())
  538. ->method('process')
  539. ->with($this->equalTo($loginData))
  540. ->willReturn($loginResult);
  541. $this->userSession->expects($this->once())
  542. ->method('isLoggedIn')
  543. ->willReturn(true);
  544. $this->urlGenerator->expects($this->once())
  545. ->method('getAbsoluteURL')
  546. ->with('/apps/mail')
  547. ->willReturn($redirectUrl);
  548. $expected = new RedirectResponse($redirectUrl);
  549. $response = $this->loginController->tryLogin($loginChain, $user, $password, '/apps/mail');
  550. $this->assertEquals($expected, $response);
  551. }
  552. public function testToNotLeakLoginName() {
  553. $loginChain = $this->createMock(LoginChain::class);
  554. $this->request
  555. ->expects($this->once())
  556. ->method('passesCSRFCheck')
  557. ->willReturn(true);
  558. $loginPageUrl = '/login?redirect_url=/apps/files';
  559. $loginData = new LoginData(
  560. $this->request,
  561. 'john@doe.com',
  562. 'just wrong',
  563. '/apps/files'
  564. );
  565. $loginResult = LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD);
  566. $loginChain->expects($this->once())
  567. ->method('process')
  568. ->with($this->equalTo($loginData))
  569. ->willReturnCallback(function (LoginData $data) use ($loginResult) {
  570. $data->setUsername('john');
  571. return $loginResult;
  572. });
  573. $this->urlGenerator->expects($this->once())
  574. ->method('linkToRoute')
  575. ->with('core.login.showLoginForm', [
  576. 'user' => 'john@doe.com',
  577. 'redirect_url' => '/apps/files',
  578. 'direct' => 1,
  579. ])
  580. ->willReturn($loginPageUrl);
  581. $expected = new RedirectResponse($loginPageUrl);
  582. $expected->throttle(['user' => 'john']);
  583. $response = $this->loginController->tryLogin(
  584. $loginChain,
  585. 'john@doe.com',
  586. 'just wrong',
  587. '/apps/files'
  588. );
  589. $this->assertEquals($expected, $response);
  590. }
  591. }