LoginControllerTest.php 16 KB

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