Completed
Push — master ( 2d0c5c...1498b5 )
by
unknown
28:52
created
lib/private/Authentication/Login/CreateSessionTokenCommand.php 1 patch
Indentation   +45 added lines, -45 removed lines patch added patch discarded remove patch
@@ -13,54 +13,54 @@
 block discarded – undo
13 13
 use OCP\IConfig;
14 14
 
15 15
 class CreateSessionTokenCommand extends ALoginCommand {
16
-	/** @var IConfig */
17
-	private $config;
16
+    /** @var IConfig */
17
+    private $config;
18 18
 
19
-	/** @var Session */
20
-	private $userSession;
19
+    /** @var Session */
20
+    private $userSession;
21 21
 
22
-	public function __construct(IConfig $config,
23
-		Session $userSession) {
24
-		$this->config = $config;
25
-		$this->userSession = $userSession;
26
-	}
22
+    public function __construct(IConfig $config,
23
+        Session $userSession) {
24
+        $this->config = $config;
25
+        $this->userSession = $userSession;
26
+    }
27 27
 
28
-	public function process(LoginData $loginData): LoginResult {
29
-		if ($this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15) === 0) {
30
-			$loginData->setRememberLogin(false);
31
-		}
32
-		if ($loginData->isRememberLogin()) {
33
-			$tokenType = IToken::REMEMBER;
34
-		} else {
35
-			$tokenType = IToken::DO_NOT_REMEMBER;
36
-		}
28
+    public function process(LoginData $loginData): LoginResult {
29
+        if ($this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15) === 0) {
30
+            $loginData->setRememberLogin(false);
31
+        }
32
+        if ($loginData->isRememberLogin()) {
33
+            $tokenType = IToken::REMEMBER;
34
+        } else {
35
+            $tokenType = IToken::DO_NOT_REMEMBER;
36
+        }
37 37
 
38
-		if ($loginData->getPassword() === '') {
39
-			$this->userSession->createSessionToken(
40
-				$loginData->getRequest(),
41
-				$loginData->getUser()->getUID(),
42
-				$loginData->getUsername(),
43
-				null,
44
-				$tokenType
45
-			);
46
-			$this->userSession->updateTokens(
47
-				$loginData->getUser()->getUID(),
48
-				''
49
-			);
50
-		} else {
51
-			$this->userSession->createSessionToken(
52
-				$loginData->getRequest(),
53
-				$loginData->getUser()->getUID(),
54
-				$loginData->getUsername(),
55
-				$loginData->getPassword(),
56
-				$tokenType
57
-			);
58
-			$this->userSession->updateTokens(
59
-				$loginData->getUser()->getUID(),
60
-				$loginData->getPassword()
61
-			);
62
-		}
38
+        if ($loginData->getPassword() === '') {
39
+            $this->userSession->createSessionToken(
40
+                $loginData->getRequest(),
41
+                $loginData->getUser()->getUID(),
42
+                $loginData->getUsername(),
43
+                null,
44
+                $tokenType
45
+            );
46
+            $this->userSession->updateTokens(
47
+                $loginData->getUser()->getUID(),
48
+                ''
49
+            );
50
+        } else {
51
+            $this->userSession->createSessionToken(
52
+                $loginData->getRequest(),
53
+                $loginData->getUser()->getUID(),
54
+                $loginData->getUsername(),
55
+                $loginData->getPassword(),
56
+                $tokenType
57
+            );
58
+            $this->userSession->updateTokens(
59
+                $loginData->getUser()->getUID(),
60
+                $loginData->getPassword()
61
+            );
62
+        }
63 63
 
64
-		return $this->processNextOrFinishSuccessfully($loginData);
65
-	}
64
+        return $this->processNextOrFinishSuccessfully($loginData);
65
+    }
66 66
 }
Please login to merge, or discard this patch.
lib/private/Authentication/Login/LoginData.php 1 patch
Indentation   +63 added lines, -63 removed lines patch added patch discarded remove patch
@@ -13,67 +13,67 @@
 block discarded – undo
13 13
 use OCP\IUser;
14 14
 
15 15
 class LoginData {
16
-	/** @var IUser|false|null */
17
-	private $user = null;
18
-
19
-	public function __construct(
20
-		private IRequest $request,
21
-		private string $username,
22
-		private ?string $password,
23
-		private bool $rememberLogin = true,
24
-		private ?string $redirectUrl = null,
25
-		private string $timeZone = '',
26
-		private string $timeZoneOffset = '',
27
-	) {
28
-	}
29
-
30
-	public function getRequest(): IRequest {
31
-		return $this->request;
32
-	}
33
-
34
-	public function setUsername(string $username): void {
35
-		$this->username = $username;
36
-	}
37
-
38
-	public function getUsername(): string {
39
-		return $this->username;
40
-	}
41
-
42
-	public function getPassword(): ?string {
43
-		return $this->password;
44
-	}
45
-
46
-	public function getRedirectUrl(): ?string {
47
-		return $this->redirectUrl;
48
-	}
49
-
50
-	public function getTimeZone(): string {
51
-		return $this->timeZone;
52
-	}
53
-
54
-	public function getTimeZoneOffset(): string {
55
-		return $this->timeZoneOffset;
56
-	}
57
-
58
-	/**
59
-	 * @param IUser|false|null $user
60
-	 */
61
-	public function setUser($user): void {
62
-		$this->user = $user;
63
-	}
64
-
65
-	/**
66
-	 * @return false|IUser|null
67
-	 */
68
-	public function getUser() {
69
-		return $this->user;
70
-	}
71
-
72
-	public function setRememberLogin(bool $rememberLogin): void {
73
-		$this->rememberLogin = $rememberLogin;
74
-	}
75
-
76
-	public function isRememberLogin(): bool {
77
-		return $this->rememberLogin;
78
-	}
16
+    /** @var IUser|false|null */
17
+    private $user = null;
18
+
19
+    public function __construct(
20
+        private IRequest $request,
21
+        private string $username,
22
+        private ?string $password,
23
+        private bool $rememberLogin = true,
24
+        private ?string $redirectUrl = null,
25
+        private string $timeZone = '',
26
+        private string $timeZoneOffset = '',
27
+    ) {
28
+    }
29
+
30
+    public function getRequest(): IRequest {
31
+        return $this->request;
32
+    }
33
+
34
+    public function setUsername(string $username): void {
35
+        $this->username = $username;
36
+    }
37
+
38
+    public function getUsername(): string {
39
+        return $this->username;
40
+    }
41
+
42
+    public function getPassword(): ?string {
43
+        return $this->password;
44
+    }
45
+
46
+    public function getRedirectUrl(): ?string {
47
+        return $this->redirectUrl;
48
+    }
49
+
50
+    public function getTimeZone(): string {
51
+        return $this->timeZone;
52
+    }
53
+
54
+    public function getTimeZoneOffset(): string {
55
+        return $this->timeZoneOffset;
56
+    }
57
+
58
+    /**
59
+     * @param IUser|false|null $user
60
+     */
61
+    public function setUser($user): void {
62
+        $this->user = $user;
63
+    }
64
+
65
+    /**
66
+     * @return false|IUser|null
67
+     */
68
+    public function getUser() {
69
+        return $this->user;
70
+    }
71
+
72
+    public function setRememberLogin(bool $rememberLogin): void {
73
+        $this->rememberLogin = $rememberLogin;
74
+    }
75
+
76
+    public function isRememberLogin(): bool {
77
+        return $this->rememberLogin;
78
+    }
79 79
 }
Please login to merge, or discard this patch.
core/Controller/LoginController.php 1 patch
Indentation   +387 added lines, -387 removed lines patch added patch discarded remove patch
@@ -47,391 +47,391 @@
 block discarded – undo
47 47
 use OCP\Util;
48 48
 
49 49
 class LoginController extends Controller {
50
-	public const LOGIN_MSG_INVALIDPASSWORD = 'invalidpassword';
51
-	public const LOGIN_MSG_USERDISABLED = 'userdisabled';
52
-	public const LOGIN_MSG_CSRFCHECKFAILED = 'csrfCheckFailed';
53
-	public const LOGIN_MSG_INVALID_ORIGIN = 'invalidOrigin';
54
-
55
-	public function __construct(
56
-		?string $appName,
57
-		IRequest $request,
58
-		private IUserManager $userManager,
59
-		private IConfig $config,
60
-		private ISession $session,
61
-		private Session $userSession,
62
-		private IURLGenerator $urlGenerator,
63
-		private Defaults $defaults,
64
-		private IThrottler $throttler,
65
-		private IInitialState $initialState,
66
-		private WebAuthnManager $webAuthnManager,
67
-		private IManager $manager,
68
-		private IL10N $l10n,
69
-		private IAppManager $appManager,
70
-	) {
71
-		parent::__construct($appName, $request);
72
-	}
73
-
74
-	/**
75
-	 * @return RedirectResponse
76
-	 */
77
-	#[NoAdminRequired]
78
-	#[UseSession]
79
-	#[FrontpageRoute(verb: 'GET', url: '/logout')]
80
-	public function logout() {
81
-		$loginToken = $this->request->getCookie('nc_token');
82
-		if (!is_null($loginToken)) {
83
-			$this->config->deleteUserValue($this->userSession->getUser()->getUID(), 'login_token', $loginToken);
84
-		}
85
-		$this->userSession->logout();
86
-
87
-		$response = new RedirectResponse($this->urlGenerator->linkToRouteAbsolute(
88
-			'core.login.showLoginForm',
89
-			['clear' => true] // this param the code in login.js may be removed when the "Clear-Site-Data" is working in the browsers
90
-		));
91
-
92
-		$this->session->set('clearingExecutionContexts', '1');
93
-		$this->session->close();
94
-
95
-		if (
96
-			$this->request->getServerProtocol() === 'https'
97
-			&& !$this->request->isUserAgent([Request::USER_AGENT_CHROME, Request::USER_AGENT_ANDROID_MOBILE_CHROME])
98
-		) {
99
-			$response->addHeader('Clear-Site-Data', '"cache", "storage"');
100
-		}
101
-
102
-		return $response;
103
-	}
104
-
105
-	/**
106
-	 * @param string $user
107
-	 * @param string $redirect_url
108
-	 *
109
-	 * @return TemplateResponse|RedirectResponse
110
-	 */
111
-	#[NoCSRFRequired]
112
-	#[PublicPage]
113
-	#[UseSession]
114
-	#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
115
-	#[FrontpageRoute(verb: 'GET', url: '/login')]
116
-	public function showLoginForm(?string $user = null, ?string $redirect_url = null): Response {
117
-		if ($this->userSession->isLoggedIn()) {
118
-			return new RedirectResponse($this->urlGenerator->linkToDefaultPageUrl());
119
-		}
120
-
121
-		$loginMessages = $this->session->get('loginMessages');
122
-		if (!$this->manager->isFairUseOfFreePushService()) {
123
-			if (!is_array($loginMessages)) {
124
-				$loginMessages = [[], []];
125
-			}
126
-			$loginMessages[1][] = $this->l10n->t('This community release of Nextcloud is unsupported and push notifications are limited.');
127
-		}
128
-		if (is_array($loginMessages)) {
129
-			[$errors, $messages] = $loginMessages;
130
-			$this->initialState->provideInitialState('loginMessages', $messages);
131
-			$this->initialState->provideInitialState('loginErrors', $errors);
132
-		}
133
-		$this->session->remove('loginMessages');
134
-
135
-		if ($user !== null && $user !== '') {
136
-			$this->initialState->provideInitialState('loginUsername', $user);
137
-		} else {
138
-			$this->initialState->provideInitialState('loginUsername', '');
139
-		}
140
-
141
-		$this->initialState->provideInitialState(
142
-			'loginAutocomplete',
143
-			$this->config->getSystemValue('login_form_autocomplete', true) === true
144
-		);
145
-
146
-		$this->initialState->provideInitialState(
147
-			'loginCanRememberme',
148
-			$this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15) > 0
149
-		);
150
-
151
-		if (!empty($redirect_url)) {
152
-			[$url, ] = explode('?', $redirect_url);
153
-			if ($url !== $this->urlGenerator->linkToRoute('core.login.logout')) {
154
-				$this->initialState->provideInitialState('loginRedirectUrl', $redirect_url);
155
-			}
156
-		}
157
-
158
-		$this->initialState->provideInitialState(
159
-			'loginThrottleDelay',
160
-			$this->throttler->getDelay($this->request->getRemoteAddress())
161
-		);
162
-
163
-		$this->setPasswordResetInitialState($user);
164
-
165
-		$this->setEmailStates();
166
-
167
-		$this->initialState->provideInitialState('webauthn-available', $this->webAuthnManager->isWebAuthnAvailable());
168
-
169
-		$this->initialState->provideInitialState('hideLoginForm', $this->config->getSystemValueBool('hide_login_form', false));
170
-
171
-		// OpenGraph Support: http://ogp.me/
172
-		Util::addHeader('meta', ['property' => 'og:title', 'content' => Util::sanitizeHTML($this->defaults->getName())]);
173
-		Util::addHeader('meta', ['property' => 'og:description', 'content' => Util::sanitizeHTML($this->defaults->getSlogan())]);
174
-		Util::addHeader('meta', ['property' => 'og:site_name', 'content' => Util::sanitizeHTML($this->defaults->getName())]);
175
-		Util::addHeader('meta', ['property' => 'og:url', 'content' => $this->urlGenerator->getAbsoluteURL('/')]);
176
-		Util::addHeader('meta', ['property' => 'og:type', 'content' => 'website']);
177
-		Util::addHeader('meta', ['property' => 'og:image', 'content' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'favicon-touch.png'))]);
178
-
179
-		// Add same-origin referrer policy so we can check for valid requests
180
-		Util::addHeader('meta', ['name' => 'referrer', 'content' => 'same-origin']);
181
-
182
-		$parameters = [
183
-			'alt_login' => OC_App::getAlternativeLogIns(),
184
-			'pageTitle' => $this->l10n->t('Login'),
185
-		];
186
-
187
-		$this->initialState->provideInitialState('countAlternativeLogins', count($parameters['alt_login']));
188
-		$this->initialState->provideInitialState('alternativeLogins', $parameters['alt_login']);
189
-		$this->initialState->provideInitialState('loginTimeout', $this->config->getSystemValueInt('login_form_timeout', 5 * 60));
190
-
191
-		return new TemplateResponse(
192
-			$this->appName,
193
-			'login',
194
-			$parameters,
195
-			TemplateResponse::RENDER_AS_GUEST,
196
-		);
197
-	}
198
-
199
-	/**
200
-	 * Sets the password reset state
201
-	 *
202
-	 * @param string $username
203
-	 */
204
-	private function setPasswordResetInitialState(?string $username): void {
205
-		if ($username !== null && $username !== '') {
206
-			$user = $this->userManager->get($username);
207
-		} else {
208
-			$user = null;
209
-		}
210
-
211
-		$passwordLink = $this->config->getSystemValueString('lost_password_link', '');
212
-
213
-		$this->initialState->provideInitialState(
214
-			'loginResetPasswordLink',
215
-			$passwordLink
216
-		);
217
-
218
-		$this->initialState->provideInitialState(
219
-			'loginCanResetPassword',
220
-			$this->canResetPassword($passwordLink, $user)
221
-		);
222
-	}
223
-
224
-	/**
225
-	 * Sets the initial state of whether or not a user is allowed to login with their email
226
-	 * initial state is passed in the array of 1 for email allowed and 0 for not allowed
227
-	 */
228
-	private function setEmailStates(): void {
229
-		$emailStates = []; // true: can login with email, false otherwise - default to true
230
-
231
-		// check if user_ldap is enabled, and the required classes exist
232
-		if ($this->appManager->isAppLoaded('user_ldap')
233
-			&& class_exists(Helper::class)) {
234
-			$helper = Server::get(Helper::class);
235
-			$allPrefixes = $helper->getServerConfigurationPrefixes();
236
-			// check each LDAP server the user is connected too
237
-			foreach ($allPrefixes as $prefix) {
238
-				$emailConfig = new Configuration($prefix);
239
-				array_push($emailStates, $emailConfig->__get('ldapLoginFilterEmail'));
240
-			}
241
-		}
242
-		$this->initialState->provideInitialState('emailStates', $emailStates);
243
-	}
244
-
245
-	/**
246
-	 * @param string|null $passwordLink
247
-	 * @param IUser|null $user
248
-	 *
249
-	 * Users may not change their passwords if:
250
-	 * - The account is disabled
251
-	 * - The backend doesn't support password resets
252
-	 * - The password reset function is disabled
253
-	 *
254
-	 * @return bool
255
-	 */
256
-	private function canResetPassword(?string $passwordLink, ?IUser $user): bool {
257
-		if ($passwordLink === 'disabled') {
258
-			return false;
259
-		}
260
-
261
-		if (!$passwordLink && $user !== null) {
262
-			return $user->canChangePassword();
263
-		}
264
-
265
-		if ($user !== null && $user->isEnabled() === false) {
266
-			return false;
267
-		}
268
-
269
-		return true;
270
-	}
271
-
272
-	private function generateRedirect(?string $redirectUrl): RedirectResponse {
273
-		if ($redirectUrl !== null && $this->userSession->isLoggedIn()) {
274
-			$location = $this->urlGenerator->getAbsoluteURL($redirectUrl);
275
-			// Deny the redirect if the URL contains a @
276
-			// This prevents unvalidated redirects like ?redirect_url=:[email protected]
277
-			if (!str_contains($location, '@')) {
278
-				return new RedirectResponse($location);
279
-			}
280
-		}
281
-		return new RedirectResponse($this->urlGenerator->linkToDefaultPageUrl());
282
-	}
283
-
284
-	#[NoCSRFRequired]
285
-	#[PublicPage]
286
-	#[BruteForceProtection(action: 'login')]
287
-	#[UseSession]
288
-	#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
289
-	#[FrontpageRoute(verb: 'POST', url: '/login')]
290
-	public function tryLogin(
291
-		Chain $loginChain,
292
-		ITrustedDomainHelper $trustedDomainHelper,
293
-		string $user = '',
294
-		string $password = '',
295
-		bool $rememberme = false,
296
-		?string $redirect_url = null,
297
-		string $timezone = '',
298
-		string $timezone_offset = '',
299
-	): RedirectResponse {
300
-		$error = '';
301
-
302
-		$origin = $this->request->getHeader('Origin');
303
-		$throttle = true;
304
-		if ($origin === '' || !$trustedDomainHelper->isTrustedUrl($origin)) {
305
-			// Login attempt not from the same origin,
306
-			// We only allow this on the login flow but not on the UI login page.
307
-			// This could have come from someone malicious who tries to block a user by triggering the bruteforce protection.
308
-			$error = self::LOGIN_MSG_INVALID_ORIGIN;
309
-			$throttle = false;
310
-		} elseif (!$this->request->passesCSRFCheck()) {
311
-			if ($this->userSession->isLoggedIn()) {
312
-				// If the user is already logged in and the CSRF check does not pass then
313
-				// simply redirect the user to the correct page as required. This is the
314
-				// case when a user has already logged-in, in another tab.
315
-				return $this->generateRedirect($redirect_url);
316
-			}
317
-			$error = self::LOGIN_MSG_CSRFCHECKFAILED;
318
-		}
319
-
320
-		if ($error !== '') {
321
-			// Clear any auth remnants like cookies to ensure a clean login
322
-			// For the next attempt
323
-			$this->userSession->logout();
324
-			return $this->createLoginFailedResponse(
325
-				$user,
326
-				$user,
327
-				$redirect_url,
328
-				$error,
329
-				$throttle,
330
-			);
331
-		}
332
-
333
-		$user = trim($user);
334
-
335
-		if (strlen($user) > 255) {
336
-			return $this->createLoginFailedResponse(
337
-				$user,
338
-				$user,
339
-				$redirect_url,
340
-				$this->l10n->t('Unsupported email length (>255)')
341
-			);
342
-		}
343
-
344
-		$data = new LoginData(
345
-			$this->request,
346
-			$user,
347
-			$password,
348
-			$rememberme,
349
-			$redirect_url,
350
-			$timezone,
351
-			$timezone_offset,
352
-		);
353
-		$result = $loginChain->process($data);
354
-		if (!$result->isSuccess()) {
355
-			return $this->createLoginFailedResponse(
356
-				$data->getUsername(),
357
-				$user,
358
-				$redirect_url,
359
-				$result->getErrorMessage()
360
-			);
361
-		}
362
-
363
-		if ($result->getRedirectUrl() !== null) {
364
-			return new RedirectResponse($result->getRedirectUrl());
365
-		}
366
-		return $this->generateRedirect($redirect_url);
367
-	}
368
-
369
-	/**
370
-	 * Creates a login failed response.
371
-	 *
372
-	 * @param string $user
373
-	 * @param string $originalUser
374
-	 * @param string $redirect_url
375
-	 * @param string $loginMessage
376
-	 *
377
-	 * @return RedirectResponse
378
-	 */
379
-	private function createLoginFailedResponse(
380
-		$user,
381
-		$originalUser,
382
-		$redirect_url,
383
-		string $loginMessage,
384
-		bool $throttle = true,
385
-	) {
386
-		// Read current user and append if possible we need to
387
-		// return the unmodified user otherwise we will leak the login name
388
-		$args = $user !== null ? ['user' => $originalUser, 'direct' => 1] : [];
389
-		if ($redirect_url !== null) {
390
-			$args['redirect_url'] = $redirect_url;
391
-		}
392
-		$response = new RedirectResponse(
393
-			$this->urlGenerator->linkToRoute('core.login.showLoginForm', $args)
394
-		);
395
-		if ($throttle) {
396
-			$response->throttle(['user' => substr($user, 0, 64)]);
397
-		}
398
-		$this->session->set('loginMessages', [
399
-			[$loginMessage], []
400
-		]);
401
-
402
-		return $response;
403
-	}
404
-
405
-	/**
406
-	 * Confirm the user password
407
-	 *
408
-	 * @license GNU AGPL version 3 or any later version
409
-	 *
410
-	 * @param string $password The password of the user
411
-	 *
412
-	 * @return DataResponse<Http::STATUS_OK, array{lastLogin: int}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, list<empty>, array{}>
413
-	 *
414
-	 * 200: Password confirmation succeeded
415
-	 * 403: Password confirmation failed
416
-	 */
417
-	#[NoAdminRequired]
418
-	#[BruteForceProtection(action: 'sudo')]
419
-	#[UseSession]
420
-	#[NoCSRFRequired]
421
-	#[FrontpageRoute(verb: 'POST', url: '/login/confirm')]
422
-	#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
423
-	public function confirmPassword(string $password): DataResponse {
424
-		$loginName = $this->userSession->getLoginName();
425
-		$loginResult = $this->userManager->checkPassword($loginName, $password);
426
-		if ($loginResult === false) {
427
-			$response = new DataResponse([], Http::STATUS_FORBIDDEN);
428
-			$response->throttle(['loginName' => $loginName]);
429
-			return $response;
430
-		}
431
-
432
-		$confirmTimestamp = time();
433
-		$this->session->set('last-password-confirm', $confirmTimestamp);
434
-		$this->throttler->resetDelay($this->request->getRemoteAddress(), 'sudo', ['loginName' => $loginName]);
435
-		return new DataResponse(['lastLogin' => $confirmTimestamp], Http::STATUS_OK);
436
-	}
50
+    public const LOGIN_MSG_INVALIDPASSWORD = 'invalidpassword';
51
+    public const LOGIN_MSG_USERDISABLED = 'userdisabled';
52
+    public const LOGIN_MSG_CSRFCHECKFAILED = 'csrfCheckFailed';
53
+    public const LOGIN_MSG_INVALID_ORIGIN = 'invalidOrigin';
54
+
55
+    public function __construct(
56
+        ?string $appName,
57
+        IRequest $request,
58
+        private IUserManager $userManager,
59
+        private IConfig $config,
60
+        private ISession $session,
61
+        private Session $userSession,
62
+        private IURLGenerator $urlGenerator,
63
+        private Defaults $defaults,
64
+        private IThrottler $throttler,
65
+        private IInitialState $initialState,
66
+        private WebAuthnManager $webAuthnManager,
67
+        private IManager $manager,
68
+        private IL10N $l10n,
69
+        private IAppManager $appManager,
70
+    ) {
71
+        parent::__construct($appName, $request);
72
+    }
73
+
74
+    /**
75
+     * @return RedirectResponse
76
+     */
77
+    #[NoAdminRequired]
78
+    #[UseSession]
79
+    #[FrontpageRoute(verb: 'GET', url: '/logout')]
80
+    public function logout() {
81
+        $loginToken = $this->request->getCookie('nc_token');
82
+        if (!is_null($loginToken)) {
83
+            $this->config->deleteUserValue($this->userSession->getUser()->getUID(), 'login_token', $loginToken);
84
+        }
85
+        $this->userSession->logout();
86
+
87
+        $response = new RedirectResponse($this->urlGenerator->linkToRouteAbsolute(
88
+            'core.login.showLoginForm',
89
+            ['clear' => true] // this param the code in login.js may be removed when the "Clear-Site-Data" is working in the browsers
90
+        ));
91
+
92
+        $this->session->set('clearingExecutionContexts', '1');
93
+        $this->session->close();
94
+
95
+        if (
96
+            $this->request->getServerProtocol() === 'https'
97
+            && !$this->request->isUserAgent([Request::USER_AGENT_CHROME, Request::USER_AGENT_ANDROID_MOBILE_CHROME])
98
+        ) {
99
+            $response->addHeader('Clear-Site-Data', '"cache", "storage"');
100
+        }
101
+
102
+        return $response;
103
+    }
104
+
105
+    /**
106
+     * @param string $user
107
+     * @param string $redirect_url
108
+     *
109
+     * @return TemplateResponse|RedirectResponse
110
+     */
111
+    #[NoCSRFRequired]
112
+    #[PublicPage]
113
+    #[UseSession]
114
+    #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
115
+    #[FrontpageRoute(verb: 'GET', url: '/login')]
116
+    public function showLoginForm(?string $user = null, ?string $redirect_url = null): Response {
117
+        if ($this->userSession->isLoggedIn()) {
118
+            return new RedirectResponse($this->urlGenerator->linkToDefaultPageUrl());
119
+        }
120
+
121
+        $loginMessages = $this->session->get('loginMessages');
122
+        if (!$this->manager->isFairUseOfFreePushService()) {
123
+            if (!is_array($loginMessages)) {
124
+                $loginMessages = [[], []];
125
+            }
126
+            $loginMessages[1][] = $this->l10n->t('This community release of Nextcloud is unsupported and push notifications are limited.');
127
+        }
128
+        if (is_array($loginMessages)) {
129
+            [$errors, $messages] = $loginMessages;
130
+            $this->initialState->provideInitialState('loginMessages', $messages);
131
+            $this->initialState->provideInitialState('loginErrors', $errors);
132
+        }
133
+        $this->session->remove('loginMessages');
134
+
135
+        if ($user !== null && $user !== '') {
136
+            $this->initialState->provideInitialState('loginUsername', $user);
137
+        } else {
138
+            $this->initialState->provideInitialState('loginUsername', '');
139
+        }
140
+
141
+        $this->initialState->provideInitialState(
142
+            'loginAutocomplete',
143
+            $this->config->getSystemValue('login_form_autocomplete', true) === true
144
+        );
145
+
146
+        $this->initialState->provideInitialState(
147
+            'loginCanRememberme',
148
+            $this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15) > 0
149
+        );
150
+
151
+        if (!empty($redirect_url)) {
152
+            [$url, ] = explode('?', $redirect_url);
153
+            if ($url !== $this->urlGenerator->linkToRoute('core.login.logout')) {
154
+                $this->initialState->provideInitialState('loginRedirectUrl', $redirect_url);
155
+            }
156
+        }
157
+
158
+        $this->initialState->provideInitialState(
159
+            'loginThrottleDelay',
160
+            $this->throttler->getDelay($this->request->getRemoteAddress())
161
+        );
162
+
163
+        $this->setPasswordResetInitialState($user);
164
+
165
+        $this->setEmailStates();
166
+
167
+        $this->initialState->provideInitialState('webauthn-available', $this->webAuthnManager->isWebAuthnAvailable());
168
+
169
+        $this->initialState->provideInitialState('hideLoginForm', $this->config->getSystemValueBool('hide_login_form', false));
170
+
171
+        // OpenGraph Support: http://ogp.me/
172
+        Util::addHeader('meta', ['property' => 'og:title', 'content' => Util::sanitizeHTML($this->defaults->getName())]);
173
+        Util::addHeader('meta', ['property' => 'og:description', 'content' => Util::sanitizeHTML($this->defaults->getSlogan())]);
174
+        Util::addHeader('meta', ['property' => 'og:site_name', 'content' => Util::sanitizeHTML($this->defaults->getName())]);
175
+        Util::addHeader('meta', ['property' => 'og:url', 'content' => $this->urlGenerator->getAbsoluteURL('/')]);
176
+        Util::addHeader('meta', ['property' => 'og:type', 'content' => 'website']);
177
+        Util::addHeader('meta', ['property' => 'og:image', 'content' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'favicon-touch.png'))]);
178
+
179
+        // Add same-origin referrer policy so we can check for valid requests
180
+        Util::addHeader('meta', ['name' => 'referrer', 'content' => 'same-origin']);
181
+
182
+        $parameters = [
183
+            'alt_login' => OC_App::getAlternativeLogIns(),
184
+            'pageTitle' => $this->l10n->t('Login'),
185
+        ];
186
+
187
+        $this->initialState->provideInitialState('countAlternativeLogins', count($parameters['alt_login']));
188
+        $this->initialState->provideInitialState('alternativeLogins', $parameters['alt_login']);
189
+        $this->initialState->provideInitialState('loginTimeout', $this->config->getSystemValueInt('login_form_timeout', 5 * 60));
190
+
191
+        return new TemplateResponse(
192
+            $this->appName,
193
+            'login',
194
+            $parameters,
195
+            TemplateResponse::RENDER_AS_GUEST,
196
+        );
197
+    }
198
+
199
+    /**
200
+     * Sets the password reset state
201
+     *
202
+     * @param string $username
203
+     */
204
+    private function setPasswordResetInitialState(?string $username): void {
205
+        if ($username !== null && $username !== '') {
206
+            $user = $this->userManager->get($username);
207
+        } else {
208
+            $user = null;
209
+        }
210
+
211
+        $passwordLink = $this->config->getSystemValueString('lost_password_link', '');
212
+
213
+        $this->initialState->provideInitialState(
214
+            'loginResetPasswordLink',
215
+            $passwordLink
216
+        );
217
+
218
+        $this->initialState->provideInitialState(
219
+            'loginCanResetPassword',
220
+            $this->canResetPassword($passwordLink, $user)
221
+        );
222
+    }
223
+
224
+    /**
225
+     * Sets the initial state of whether or not a user is allowed to login with their email
226
+     * initial state is passed in the array of 1 for email allowed and 0 for not allowed
227
+     */
228
+    private function setEmailStates(): void {
229
+        $emailStates = []; // true: can login with email, false otherwise - default to true
230
+
231
+        // check if user_ldap is enabled, and the required classes exist
232
+        if ($this->appManager->isAppLoaded('user_ldap')
233
+            && class_exists(Helper::class)) {
234
+            $helper = Server::get(Helper::class);
235
+            $allPrefixes = $helper->getServerConfigurationPrefixes();
236
+            // check each LDAP server the user is connected too
237
+            foreach ($allPrefixes as $prefix) {
238
+                $emailConfig = new Configuration($prefix);
239
+                array_push($emailStates, $emailConfig->__get('ldapLoginFilterEmail'));
240
+            }
241
+        }
242
+        $this->initialState->provideInitialState('emailStates', $emailStates);
243
+    }
244
+
245
+    /**
246
+     * @param string|null $passwordLink
247
+     * @param IUser|null $user
248
+     *
249
+     * Users may not change their passwords if:
250
+     * - The account is disabled
251
+     * - The backend doesn't support password resets
252
+     * - The password reset function is disabled
253
+     *
254
+     * @return bool
255
+     */
256
+    private function canResetPassword(?string $passwordLink, ?IUser $user): bool {
257
+        if ($passwordLink === 'disabled') {
258
+            return false;
259
+        }
260
+
261
+        if (!$passwordLink && $user !== null) {
262
+            return $user->canChangePassword();
263
+        }
264
+
265
+        if ($user !== null && $user->isEnabled() === false) {
266
+            return false;
267
+        }
268
+
269
+        return true;
270
+    }
271
+
272
+    private function generateRedirect(?string $redirectUrl): RedirectResponse {
273
+        if ($redirectUrl !== null && $this->userSession->isLoggedIn()) {
274
+            $location = $this->urlGenerator->getAbsoluteURL($redirectUrl);
275
+            // Deny the redirect if the URL contains a @
276
+            // This prevents unvalidated redirects like ?redirect_url=:[email protected]
277
+            if (!str_contains($location, '@')) {
278
+                return new RedirectResponse($location);
279
+            }
280
+        }
281
+        return new RedirectResponse($this->urlGenerator->linkToDefaultPageUrl());
282
+    }
283
+
284
+    #[NoCSRFRequired]
285
+    #[PublicPage]
286
+    #[BruteForceProtection(action: 'login')]
287
+    #[UseSession]
288
+    #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
289
+    #[FrontpageRoute(verb: 'POST', url: '/login')]
290
+    public function tryLogin(
291
+        Chain $loginChain,
292
+        ITrustedDomainHelper $trustedDomainHelper,
293
+        string $user = '',
294
+        string $password = '',
295
+        bool $rememberme = false,
296
+        ?string $redirect_url = null,
297
+        string $timezone = '',
298
+        string $timezone_offset = '',
299
+    ): RedirectResponse {
300
+        $error = '';
301
+
302
+        $origin = $this->request->getHeader('Origin');
303
+        $throttle = true;
304
+        if ($origin === '' || !$trustedDomainHelper->isTrustedUrl($origin)) {
305
+            // Login attempt not from the same origin,
306
+            // We only allow this on the login flow but not on the UI login page.
307
+            // This could have come from someone malicious who tries to block a user by triggering the bruteforce protection.
308
+            $error = self::LOGIN_MSG_INVALID_ORIGIN;
309
+            $throttle = false;
310
+        } elseif (!$this->request->passesCSRFCheck()) {
311
+            if ($this->userSession->isLoggedIn()) {
312
+                // If the user is already logged in and the CSRF check does not pass then
313
+                // simply redirect the user to the correct page as required. This is the
314
+                // case when a user has already logged-in, in another tab.
315
+                return $this->generateRedirect($redirect_url);
316
+            }
317
+            $error = self::LOGIN_MSG_CSRFCHECKFAILED;
318
+        }
319
+
320
+        if ($error !== '') {
321
+            // Clear any auth remnants like cookies to ensure a clean login
322
+            // For the next attempt
323
+            $this->userSession->logout();
324
+            return $this->createLoginFailedResponse(
325
+                $user,
326
+                $user,
327
+                $redirect_url,
328
+                $error,
329
+                $throttle,
330
+            );
331
+        }
332
+
333
+        $user = trim($user);
334
+
335
+        if (strlen($user) > 255) {
336
+            return $this->createLoginFailedResponse(
337
+                $user,
338
+                $user,
339
+                $redirect_url,
340
+                $this->l10n->t('Unsupported email length (>255)')
341
+            );
342
+        }
343
+
344
+        $data = new LoginData(
345
+            $this->request,
346
+            $user,
347
+            $password,
348
+            $rememberme,
349
+            $redirect_url,
350
+            $timezone,
351
+            $timezone_offset,
352
+        );
353
+        $result = $loginChain->process($data);
354
+        if (!$result->isSuccess()) {
355
+            return $this->createLoginFailedResponse(
356
+                $data->getUsername(),
357
+                $user,
358
+                $redirect_url,
359
+                $result->getErrorMessage()
360
+            );
361
+        }
362
+
363
+        if ($result->getRedirectUrl() !== null) {
364
+            return new RedirectResponse($result->getRedirectUrl());
365
+        }
366
+        return $this->generateRedirect($redirect_url);
367
+    }
368
+
369
+    /**
370
+     * Creates a login failed response.
371
+     *
372
+     * @param string $user
373
+     * @param string $originalUser
374
+     * @param string $redirect_url
375
+     * @param string $loginMessage
376
+     *
377
+     * @return RedirectResponse
378
+     */
379
+    private function createLoginFailedResponse(
380
+        $user,
381
+        $originalUser,
382
+        $redirect_url,
383
+        string $loginMessage,
384
+        bool $throttle = true,
385
+    ) {
386
+        // Read current user and append if possible we need to
387
+        // return the unmodified user otherwise we will leak the login name
388
+        $args = $user !== null ? ['user' => $originalUser, 'direct' => 1] : [];
389
+        if ($redirect_url !== null) {
390
+            $args['redirect_url'] = $redirect_url;
391
+        }
392
+        $response = new RedirectResponse(
393
+            $this->urlGenerator->linkToRoute('core.login.showLoginForm', $args)
394
+        );
395
+        if ($throttle) {
396
+            $response->throttle(['user' => substr($user, 0, 64)]);
397
+        }
398
+        $this->session->set('loginMessages', [
399
+            [$loginMessage], []
400
+        ]);
401
+
402
+        return $response;
403
+    }
404
+
405
+    /**
406
+     * Confirm the user password
407
+     *
408
+     * @license GNU AGPL version 3 or any later version
409
+     *
410
+     * @param string $password The password of the user
411
+     *
412
+     * @return DataResponse<Http::STATUS_OK, array{lastLogin: int}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, list<empty>, array{}>
413
+     *
414
+     * 200: Password confirmation succeeded
415
+     * 403: Password confirmation failed
416
+     */
417
+    #[NoAdminRequired]
418
+    #[BruteForceProtection(action: 'sudo')]
419
+    #[UseSession]
420
+    #[NoCSRFRequired]
421
+    #[FrontpageRoute(verb: 'POST', url: '/login/confirm')]
422
+    #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
423
+    public function confirmPassword(string $password): DataResponse {
424
+        $loginName = $this->userSession->getLoginName();
425
+        $loginResult = $this->userManager->checkPassword($loginName, $password);
426
+        if ($loginResult === false) {
427
+            $response = new DataResponse([], Http::STATUS_FORBIDDEN);
428
+            $response->throttle(['loginName' => $loginName]);
429
+            return $response;
430
+        }
431
+
432
+        $confirmTimestamp = time();
433
+        $this->session->set('last-password-confirm', $confirmTimestamp);
434
+        $this->throttler->resetDelay($this->request->getRemoteAddress(), 'sudo', ['loginName' => $loginName]);
435
+        return new DataResponse(['lastLogin' => $confirmTimestamp], Http::STATUS_OK);
436
+    }
437 437
 }
Please login to merge, or discard this patch.
tests/lib/Authentication/Login/ALoginTestCommand.php 1 patch
Indentation   +87 added lines, -87 removed lines patch added patch discarded remove patch
@@ -16,91 +16,91 @@
 block discarded – undo
16 16
 use Test\TestCase;
17 17
 
18 18
 abstract class ALoginTestCommand extends TestCase {
19
-	/** @var IRequest|MockObject */
20
-	protected $request;
21
-
22
-	/** @var string */
23
-	protected $username = 'user123';
24
-
25
-	/** @var string */
26
-	protected $password = '123456';
27
-
28
-	/** @var string */
29
-	protected $redirectUrl = '/apps/contacts';
30
-
31
-	/** @var string */
32
-	protected $timezone = 'Europe/Vienna';
33
-
34
-	protected $timeZoneOffset = '2';
35
-
36
-	/** @var IUser|MockObject */
37
-	protected $user;
38
-
39
-	/** @var ALoginTestCommand */
40
-	protected $cmd;
41
-
42
-	protected function setUp(): void {
43
-		parent::setUp();
44
-
45
-		$this->request = $this->createMock(IRequest::class);
46
-		$this->user = $this->createMock(IUser::class);
47
-	}
48
-
49
-	protected function getBasicLoginData(): LoginData {
50
-		return new LoginData(
51
-			$this->request,
52
-			$this->username,
53
-			$this->password
54
-		);
55
-	}
56
-
57
-	protected function getInvalidLoginData(): LoginData {
58
-		return new LoginData(
59
-			$this->request,
60
-			$this->username,
61
-			$this->password
62
-		);
63
-	}
64
-
65
-	protected function getFailedLoginData(): LoginData {
66
-		$data = new LoginData(
67
-			$this->request,
68
-			$this->username,
69
-			$this->password
70
-		);
71
-		$data->setUser(false);
72
-		return $data;
73
-	}
74
-
75
-	protected function getLoggedInLoginData(): LoginData {
76
-		$basic = $this->getBasicLoginData();
77
-		$basic->setUser($this->user);
78
-		return $basic;
79
-	}
80
-
81
-	protected function getLoggedInLoginDataWithRedirectUrl(): LoginData {
82
-		$data = new LoginData(
83
-			$this->request,
84
-			$this->username,
85
-			$this->password,
86
-			true,
87
-			$this->redirectUrl
88
-		);
89
-		$data->setUser($this->user);
90
-		return $data;
91
-	}
92
-
93
-	protected function getLoggedInLoginDataWithTimezone(): LoginData {
94
-		$data = new LoginData(
95
-			$this->request,
96
-			$this->username,
97
-			$this->password,
98
-			true,
99
-			null,
100
-			$this->timezone,
101
-			$this->timeZoneOffset
102
-		);
103
-		$data->setUser($this->user);
104
-		return $data;
105
-	}
19
+    /** @var IRequest|MockObject */
20
+    protected $request;
21
+
22
+    /** @var string */
23
+    protected $username = 'user123';
24
+
25
+    /** @var string */
26
+    protected $password = '123456';
27
+
28
+    /** @var string */
29
+    protected $redirectUrl = '/apps/contacts';
30
+
31
+    /** @var string */
32
+    protected $timezone = 'Europe/Vienna';
33
+
34
+    protected $timeZoneOffset = '2';
35
+
36
+    /** @var IUser|MockObject */
37
+    protected $user;
38
+
39
+    /** @var ALoginTestCommand */
40
+    protected $cmd;
41
+
42
+    protected function setUp(): void {
43
+        parent::setUp();
44
+
45
+        $this->request = $this->createMock(IRequest::class);
46
+        $this->user = $this->createMock(IUser::class);
47
+    }
48
+
49
+    protected function getBasicLoginData(): LoginData {
50
+        return new LoginData(
51
+            $this->request,
52
+            $this->username,
53
+            $this->password
54
+        );
55
+    }
56
+
57
+    protected function getInvalidLoginData(): LoginData {
58
+        return new LoginData(
59
+            $this->request,
60
+            $this->username,
61
+            $this->password
62
+        );
63
+    }
64
+
65
+    protected function getFailedLoginData(): LoginData {
66
+        $data = new LoginData(
67
+            $this->request,
68
+            $this->username,
69
+            $this->password
70
+        );
71
+        $data->setUser(false);
72
+        return $data;
73
+    }
74
+
75
+    protected function getLoggedInLoginData(): LoginData {
76
+        $basic = $this->getBasicLoginData();
77
+        $basic->setUser($this->user);
78
+        return $basic;
79
+    }
80
+
81
+    protected function getLoggedInLoginDataWithRedirectUrl(): LoginData {
82
+        $data = new LoginData(
83
+            $this->request,
84
+            $this->username,
85
+            $this->password,
86
+            true,
87
+            $this->redirectUrl
88
+        );
89
+        $data->setUser($this->user);
90
+        return $data;
91
+    }
92
+
93
+    protected function getLoggedInLoginDataWithTimezone(): LoginData {
94
+        $data = new LoginData(
95
+            $this->request,
96
+            $this->username,
97
+            $this->password,
98
+            true,
99
+            null,
100
+            $this->timezone,
101
+            $this->timeZoneOffset
102
+        );
103
+        $data->setUser($this->user);
104
+        return $data;
105
+    }
106 106
 }
Please login to merge, or discard this patch.
tests/Core/Controller/LoginControllerTest.php 2 patches
Indentation   +688 added lines, -688 removed lines patch added patch discarded remove patch
@@ -36,692 +36,692 @@
 block discarded – undo
36 36
 use Test\TestCase;
37 37
 
38 38
 class LoginControllerTest extends TestCase {
39
-	/** @var LoginController */
40
-	private $loginController;
41
-
42
-	/** @var IRequest|MockObject */
43
-	private $request;
44
-
45
-	/** @var IUserManager|MockObject */
46
-	private $userManager;
47
-
48
-	/** @var IConfig|MockObject */
49
-	private $config;
50
-
51
-	/** @var ISession|MockObject */
52
-	private $session;
53
-
54
-	/** @var Session|MockObject */
55
-	private $userSession;
56
-
57
-	/** @var IURLGenerator|MockObject */
58
-	private $urlGenerator;
59
-
60
-	/** @var Manager|MockObject */
61
-	private $twoFactorManager;
62
-
63
-	/** @var Defaults|MockObject */
64
-	private $defaults;
65
-
66
-	/** @var IThrottler|MockObject */
67
-	private $throttler;
68
-
69
-	/** @var IInitialState|MockObject */
70
-	private $initialState;
71
-
72
-	/** @var \OC\Authentication\WebAuthn\Manager|MockObject */
73
-	private $webAuthnManager;
74
-
75
-	/** @var IManager|MockObject */
76
-	private $notificationManager;
77
-
78
-	/** @var IL10N|MockObject */
79
-	private $l;
80
-
81
-	/** @var IAppManager|MockObject */
82
-	private $appManager;
83
-
84
-	protected function setUp(): void {
85
-		parent::setUp();
86
-		$this->request = $this->createMock(IRequest::class);
87
-		$this->userManager = $this->createMock(\OC\User\Manager::class);
88
-		$this->config = $this->createMock(IConfig::class);
89
-		$this->session = $this->createMock(ISession::class);
90
-		$this->userSession = $this->createMock(Session::class);
91
-		$this->urlGenerator = $this->createMock(IURLGenerator::class);
92
-		$this->twoFactorManager = $this->createMock(Manager::class);
93
-		$this->defaults = $this->createMock(Defaults::class);
94
-		$this->throttler = $this->createMock(IThrottler::class);
95
-		$this->initialState = $this->createMock(IInitialState::class);
96
-		$this->webAuthnManager = $this->createMock(\OC\Authentication\WebAuthn\Manager::class);
97
-		$this->notificationManager = $this->createMock(IManager::class);
98
-		$this->l = $this->createMock(IL10N::class);
99
-		$this->appManager = $this->createMock(IAppManager::class);
100
-
101
-		$this->l->expects($this->any())
102
-			->method('t')
103
-			->willReturnCallback(function ($text, $parameters = []) {
104
-				return vsprintf($text, $parameters);
105
-			});
106
-
107
-
108
-		$this->request->method('getRemoteAddress')
109
-			->willReturn('1.2.3.4');
110
-		$this->request->method('getHeader')
111
-			->with('Origin')
112
-			->willReturn('domain.example.com');
113
-		$this->throttler->method('getDelay')
114
-			->with(
115
-				$this->equalTo('1.2.3.4'),
116
-				$this->equalTo('')
117
-			)->willReturn(1000);
118
-
119
-		$this->loginController = new LoginController(
120
-			'core',
121
-			$this->request,
122
-			$this->userManager,
123
-			$this->config,
124
-			$this->session,
125
-			$this->userSession,
126
-			$this->urlGenerator,
127
-			$this->defaults,
128
-			$this->throttler,
129
-			$this->initialState,
130
-			$this->webAuthnManager,
131
-			$this->notificationManager,
132
-			$this->l,
133
-			$this->appManager,
134
-		);
135
-	}
136
-
137
-	public function testLogoutWithoutToken(): void {
138
-		$this->request
139
-			->expects($this->once())
140
-			->method('getCookie')
141
-			->with('nc_token')
142
-			->willReturn(null);
143
-		$this->request
144
-			->method('getServerProtocol')
145
-			->willReturn('https');
146
-		$this->request
147
-			->expects($this->once())
148
-			->method('isUserAgent')
149
-			->willReturn(false);
150
-		$this->config
151
-			->expects($this->never())
152
-			->method('deleteUserValue');
153
-		$this->urlGenerator
154
-			->expects($this->once())
155
-			->method('linkToRouteAbsolute')
156
-			->with('core.login.showLoginForm')
157
-			->willReturn('/login');
158
-
159
-		$expected = new RedirectResponse('/login');
160
-		$expected->addHeader('Clear-Site-Data', '"cache", "storage"');
161
-		$this->assertEquals($expected, $this->loginController->logout());
162
-	}
163
-
164
-	public function testLogoutNoClearSiteData(): void {
165
-		$this->request
166
-			->expects($this->once())
167
-			->method('getCookie')
168
-			->with('nc_token')
169
-			->willReturn(null);
170
-		$this->request
171
-			->method('getServerProtocol')
172
-			->willReturn('https');
173
-		$this->request
174
-			->expects($this->once())
175
-			->method('isUserAgent')
176
-			->willReturn(true);
177
-		$this->urlGenerator
178
-			->expects($this->once())
179
-			->method('linkToRouteAbsolute')
180
-			->with('core.login.showLoginForm')
181
-			->willReturn('/login');
182
-
183
-		$expected = new RedirectResponse('/login');
184
-		$this->assertEquals($expected, $this->loginController->logout());
185
-	}
186
-
187
-	public function testLogoutWithToken(): void {
188
-		$this->request
189
-			->expects($this->once())
190
-			->method('getCookie')
191
-			->with('nc_token')
192
-			->willReturn('MyLoginToken');
193
-		$this->request
194
-			->method('getServerProtocol')
195
-			->willReturn('https');
196
-		$this->request
197
-			->expects($this->once())
198
-			->method('isUserAgent')
199
-			->willReturn(false);
200
-		$user = $this->createMock(IUser::class);
201
-		$user
202
-			->expects($this->once())
203
-			->method('getUID')
204
-			->willReturn('JohnDoe');
205
-		$this->userSession
206
-			->expects($this->once())
207
-			->method('getUser')
208
-			->willReturn($user);
209
-		$this->config
210
-			->expects($this->once())
211
-			->method('deleteUserValue')
212
-			->with('JohnDoe', 'login_token', 'MyLoginToken');
213
-		$this->urlGenerator
214
-			->expects($this->once())
215
-			->method('linkToRouteAbsolute')
216
-			->with('core.login.showLoginForm')
217
-			->willReturn('/login');
218
-
219
-		$expected = new RedirectResponse('/login');
220
-		$expected->addHeader('Clear-Site-Data', '"cache", "storage"');
221
-		$this->assertEquals($expected, $this->loginController->logout());
222
-	}
223
-
224
-	public function testShowLoginFormForLoggedInUsers(): void {
225
-		$this->userSession
226
-			->expects($this->once())
227
-			->method('isLoggedIn')
228
-			->willReturn(true);
229
-		$this->urlGenerator
230
-			->expects($this->once())
231
-			->method('linkToDefaultPageUrl')
232
-			->willReturn('/default/foo');
233
-
234
-		$expectedResponse = new RedirectResponse('/default/foo');
235
-		$this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', ''));
236
-	}
237
-
238
-	public function testShowLoginFormWithErrorsInSession(): void {
239
-		$this->userSession
240
-			->expects($this->once())
241
-			->method('isLoggedIn')
242
-			->willReturn(false);
243
-		$this->session
244
-			->expects($this->once())
245
-			->method('get')
246
-			->with('loginMessages')
247
-			->willReturn(
248
-				[
249
-					[
250
-						'ErrorArray1',
251
-						'ErrorArray2',
252
-					],
253
-					[
254
-						'MessageArray1',
255
-						'MessageArray2',
256
-					],
257
-				]
258
-			);
259
-
260
-		$calls = [
261
-			[
262
-				'loginMessages',
263
-				[
264
-					'MessageArray1',
265
-					'MessageArray2',
266
-					'This community release of Nextcloud is unsupported and push notifications are limited.',
267
-				],
268
-			],
269
-			[
270
-				'loginErrors',
271
-				[
272
-					'ErrorArray1',
273
-					'ErrorArray2',
274
-				],
275
-			],
276
-			[
277
-				'loginUsername',
278
-				'',
279
-			]
280
-		];
281
-		$this->initialState->expects($this->exactly(14))
282
-			->method('provideInitialState')
283
-			->willReturnCallback(function () use (&$calls): void {
284
-				$expected = array_shift($calls);
285
-				if (!empty($expected)) {
286
-					$this->assertEquals($expected, func_get_args());
287
-				}
288
-			});
289
-
290
-		$expectedResponse = new TemplateResponse(
291
-			'core',
292
-			'login',
293
-			[
294
-				'alt_login' => [],
295
-				'pageTitle' => 'Login'
296
-			],
297
-			'guest'
298
-		);
299
-		$this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', ''));
300
-	}
301
-
302
-	public function testShowLoginFormForFlowAuth(): void {
303
-		$this->userSession
304
-			->expects($this->once())
305
-			->method('isLoggedIn')
306
-			->willReturn(false);
307
-		$calls = [
308
-			[], [], [],
309
-			[
310
-				'loginAutocomplete',
311
-				false
312
-			],
313
-			[
314
-				'loginCanRememberme',
315
-				false
316
-			],
317
-			[
318
-				'loginRedirectUrl',
319
-				'login/flow'
320
-			],
321
-		];
322
-		$this->initialState->expects($this->exactly(15))
323
-			->method('provideInitialState')
324
-			->willReturnCallback(function () use (&$calls): void {
325
-				$expected = array_shift($calls);
326
-				if (!empty($expected)) {
327
-					$this->assertEquals($expected, func_get_args());
328
-				}
329
-			});
330
-
331
-		$expectedResponse = new TemplateResponse(
332
-			'core',
333
-			'login',
334
-			[
335
-				'alt_login' => [],
336
-				'pageTitle' => 'Login'
337
-			],
338
-			'guest'
339
-		);
340
-		$this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', 'login/flow'));
341
-	}
342
-
343
-	/**
344
-	 * @return array
345
-	 */
346
-	public static function passwordResetDataProvider(): array {
347
-		return [
348
-			[
349
-				true,
350
-				true,
351
-			],
352
-			[
353
-				false,
354
-				false,
355
-			],
356
-		];
357
-	}
358
-
359
-	#[DataProvider('passwordResetDataProvider')]
360
-	public function testShowLoginFormWithPasswordResetOption($canChangePassword,
361
-		$expectedResult): void {
362
-		$this->userSession
363
-			->expects($this->once())
364
-			->method('isLoggedIn')
365
-			->willReturn(false);
366
-		$this->config
367
-			->expects(self::once())
368
-			->method('getSystemValue')
369
-			->willReturnMap([
370
-				['login_form_autocomplete', true, true],
371
-			]);
372
-		$this->config
373
-			->expects(self::once())
374
-			->method('getSystemValueString')
375
-			->willReturnMap([
376
-				['lost_password_link', '', ''],
377
-			]);
378
-		$user = $this->createMock(IUser::class);
379
-		$user
380
-			->expects($this->once())
381
-			->method('canChangePassword')
382
-			->willReturn($canChangePassword);
383
-		$this->userManager
384
-			->expects($this->once())
385
-			->method('get')
386
-			->with('LdapUser')
387
-			->willReturn($user);
388
-		$calls = [
389
-			[], [],
390
-			[
391
-				'loginUsername',
392
-				'LdapUser'
393
-			],
394
-			[], [], [], [],
395
-			[
396
-				'loginCanResetPassword',
397
-				$expectedResult
398
-			],
399
-		];
400
-		$this->initialState->expects($this->exactly(14))
401
-			->method('provideInitialState')
402
-			->willReturnCallback(function () use (&$calls): void {
403
-				$expected = array_shift($calls);
404
-				if (!empty($expected)) {
405
-					$this->assertEquals($expected, func_get_args());
406
-				}
407
-			});
408
-
409
-		$expectedResponse = new TemplateResponse(
410
-			'core',
411
-			'login',
412
-			[
413
-				'alt_login' => [],
414
-				'pageTitle' => 'Login'
415
-			],
416
-			'guest'
417
-		);
418
-		$this->assertEquals($expectedResponse, $this->loginController->showLoginForm('LdapUser', ''));
419
-	}
420
-
421
-	public function testShowLoginFormForUserNamed0(): void {
422
-		$this->userSession
423
-			->expects($this->once())
424
-			->method('isLoggedIn')
425
-			->willReturn(false);
426
-		$this->config
427
-			->expects(self::once())
428
-			->method('getSystemValue')
429
-			->willReturnMap([
430
-				['login_form_autocomplete', true, true],
431
-			]);
432
-		$this->config
433
-			->expects(self::once())
434
-			->method('getSystemValueString')
435
-			->willReturnMap([
436
-				['lost_password_link', '', ''],
437
-			]);
438
-		$user = $this->createMock(IUser::class);
439
-		$user->expects($this->once())
440
-			->method('canChangePassword')
441
-			->willReturn(false);
442
-		$this->userManager
443
-			->expects($this->once())
444
-			->method('get')
445
-			->with('0')
446
-			->willReturn($user);
447
-		$calls = [
448
-			[], [], [],
449
-			[
450
-				'loginAutocomplete',
451
-				true
452
-			],
453
-			[
454
-				'loginCanRememberme',
455
-				false
456
-			],
457
-			[],
458
-			[
459
-				'loginResetPasswordLink',
460
-				false
461
-			],
462
-			[
463
-				'loginCanResetPassword',
464
-				false
465
-			],
466
-		];
467
-		$this->initialState->expects($this->exactly(14))
468
-			->method('provideInitialState')
469
-			->willReturnCallback(function () use (&$calls): void {
470
-				$expected = array_shift($calls);
471
-				if (!empty($expected)) {
472
-					$this->assertEquals($expected, func_get_args());
473
-				}
474
-			});
475
-
476
-		$expectedResponse = new TemplateResponse(
477
-			'core',
478
-			'login',
479
-			[
480
-				'alt_login' => [],
481
-				'pageTitle' => 'Login'
482
-			],
483
-			'guest'
484
-		);
485
-		$this->assertEquals($expectedResponse, $this->loginController->showLoginForm('0', ''));
486
-	}
487
-
488
-	public static function remembermeProvider(): array {
489
-		return [
490
-			[
491
-				true,
492
-			],
493
-			[
494
-				false,
495
-			],
496
-		];
497
-	}
498
-
499
-	#[DataProvider('remembermeProvider')]
500
-	public function testLoginWithInvalidCredentials(bool $rememberme): void {
501
-		$user = 'MyUserName';
502
-		$password = 'secret';
503
-		$loginPageUrl = '/login?redirect_url=/apps/files';
504
-		$loginChain = $this->createMock(LoginChain::class);
505
-		$trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
506
-		$trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
507
-		$this->request
508
-			->expects($this->once())
509
-			->method('passesCSRFCheck')
510
-			->willReturn(true);
511
-		$loginData = new LoginData(
512
-			$this->request,
513
-			$user,
514
-			$password,
515
-			$rememberme,
516
-			'/apps/files'
517
-		);
518
-		$loginResult = LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD);
519
-		$loginChain->expects($this->once())
520
-			->method('process')
521
-			->with($this->equalTo($loginData))
522
-			->willReturn($loginResult);
523
-		$this->urlGenerator->expects($this->once())
524
-			->method('linkToRoute')
525
-			->with('core.login.showLoginForm', [
526
-				'user' => $user,
527
-				'redirect_url' => '/apps/files',
528
-				'direct' => 1,
529
-			])
530
-			->willReturn($loginPageUrl);
531
-		$expected = new RedirectResponse($loginPageUrl);
532
-		$expected->throttle(['user' => 'MyUserName']);
533
-
534
-		$response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, $rememberme, '/apps/files');
535
-
536
-		$this->assertEquals($expected, $response);
537
-	}
538
-
539
-	#[DataProvider('remembermeProvider')]
540
-	public function testLoginWithValidCredentials(bool $rememberme): void {
541
-		$user = 'MyUserName';
542
-		$password = 'secret';
543
-		$loginChain = $this->createMock(LoginChain::class);
544
-		$trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
545
-		$trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
546
-		$this->request
547
-			->expects($this->once())
548
-			->method('passesCSRFCheck')
549
-			->willReturn(true);
550
-		$loginData = new LoginData(
551
-			$this->request,
552
-			$user,
553
-			$password,
554
-			$rememberme,
555
-		);
556
-		$loginResult = LoginResult::success($loginData);
557
-		$loginChain->expects($this->once())
558
-			->method('process')
559
-			->with($this->equalTo($loginData))
560
-			->willReturn($loginResult);
561
-		$this->urlGenerator
562
-			->expects($this->once())
563
-			->method('linkToDefaultPageUrl')
564
-			->willReturn('/default/foo');
565
-
566
-		$expected = new RedirectResponse('/default/foo');
567
-		$this->assertEquals($expected, $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, $rememberme));
568
-	}
569
-
570
-	#[DataProvider('remembermeProvider')]
571
-	public function testLoginWithoutPassedCsrfCheckAndNotLoggedIn(bool $rememberme): void {
572
-		/** @var IUser|MockObject $user */
573
-		$user = $this->createMock(IUser::class);
574
-		$user->expects($this->any())
575
-			->method('getUID')
576
-			->willReturn('jane');
577
-		$password = 'secret';
578
-		$originalUrl = 'another%20url';
579
-		$loginChain = $this->createMock(LoginChain::class);
580
-		$trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
581
-		$trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
582
-		$this->request
583
-			->expects($this->once())
584
-			->method('passesCSRFCheck')
585
-			->willReturn(false);
586
-		$this->userSession
587
-			->method('isLoggedIn')
588
-			->with()
589
-			->willReturn(false);
590
-		$this->config->expects($this->never())
591
-			->method('deleteUserValue');
592
-		$this->userSession->expects($this->never())
593
-			->method('createRememberMeToken');
594
-
595
-		$response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $rememberme, $originalUrl);
596
-
597
-		$expected = new RedirectResponse('');
598
-		$expected->throttle(['user' => 'Jane']);
599
-		$this->assertEquals($expected, $response);
600
-	}
601
-
602
-	#[DataProvider('remembermeProvider')]
603
-	public function testLoginWithoutPassedCsrfCheckAndLoggedIn(bool $rememberme): void {
604
-		/** @var IUser|MockObject $user */
605
-		$user = $this->createMock(IUser::class);
606
-		$user->expects($this->any())
607
-			->method('getUID')
608
-			->willReturn('jane');
609
-		$password = 'secret';
610
-		$originalUrl = 'another url';
611
-		$redirectUrl = 'http://localhost/another url';
612
-		$loginChain = $this->createMock(LoginChain::class);
613
-		$trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
614
-		$trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
615
-		$this->request
616
-			->expects($this->once())
617
-			->method('passesCSRFCheck')
618
-			->willReturn(false);
619
-		$this->userSession
620
-			->method('isLoggedIn')
621
-			->with()
622
-			->willReturn(true);
623
-		$this->urlGenerator->expects($this->once())
624
-			->method('getAbsoluteURL')
625
-			->with(urldecode($originalUrl))
626
-			->willReturn($redirectUrl);
627
-		$this->config->expects($this->never())
628
-			->method('deleteUserValue');
629
-		$this->userSession->expects($this->never())
630
-			->method('createRememberMeToken');
631
-		$this->config
632
-			->method('getSystemValue')
633
-			->with('remember_login_cookie_lifetime')
634
-			->willReturn(1234);
635
-
636
-		$response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $rememberme, $originalUrl);
637
-
638
-		$expected = new RedirectResponse($redirectUrl);
639
-		$this->assertEquals($expected, $response);
640
-	}
641
-
642
-	#[DataProvider('remembermeProvider')]
643
-	public function testLoginWithValidCredentialsAndRedirectUrl(bool $rememberme): void {
644
-		$user = 'MyUserName';
645
-		$password = 'secret';
646
-		$redirectUrl = 'https://next.cloud/apps/mail';
647
-		$loginChain = $this->createMock(LoginChain::class);
648
-		$trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
649
-		$trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
650
-		$this->request
651
-			->expects($this->once())
652
-			->method('passesCSRFCheck')
653
-			->willReturn(true);
654
-		$loginData = new LoginData(
655
-			$this->request,
656
-			$user,
657
-			$password,
658
-			$rememberme,
659
-			'/apps/mail'
660
-		);
661
-		$loginResult = LoginResult::success($loginData);
662
-		$loginChain->expects($this->once())
663
-			->method('process')
664
-			->with($this->equalTo($loginData))
665
-			->willReturn($loginResult);
666
-		$this->userSession->expects($this->once())
667
-			->method('isLoggedIn')
668
-			->willReturn(true);
669
-		$this->urlGenerator->expects($this->once())
670
-			->method('getAbsoluteURL')
671
-			->with('/apps/mail')
672
-			->willReturn($redirectUrl);
673
-		$expected = new RedirectResponse($redirectUrl);
674
-
675
-		$response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, $rememberme, '/apps/mail');
676
-
677
-		$this->assertEquals($expected, $response);
678
-	}
679
-
680
-	#[DataProvider('remembermeProvider')]
681
-	public function testToNotLeakLoginName(bool $rememberme): void {
682
-		$loginChain = $this->createMock(LoginChain::class);
683
-		$trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
684
-		$trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
685
-		$this->request
686
-			->expects($this->once())
687
-			->method('passesCSRFCheck')
688
-			->willReturn(true);
689
-		$loginPageUrl = '/login?redirect_url=/apps/files';
690
-		$loginData = new LoginData(
691
-			$this->request,
692
-			'[email protected]',
693
-			'just wrong',
694
-			$rememberme,
695
-			'/apps/files'
696
-		);
697
-		$loginResult = LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD);
698
-		$loginChain->expects($this->once())
699
-			->method('process')
700
-			->with($this->equalTo($loginData))
701
-			->willReturnCallback(function (LoginData $data) use ($loginResult) {
702
-				$data->setUsername('john');
703
-				return $loginResult;
704
-			});
705
-		$this->urlGenerator->expects($this->once())
706
-			->method('linkToRoute')
707
-			->with('core.login.showLoginForm', [
708
-				'user' => '[email protected]',
709
-				'redirect_url' => '/apps/files',
710
-				'direct' => 1,
711
-			])
712
-			->willReturn($loginPageUrl);
713
-		$expected = new RedirectResponse($loginPageUrl);
714
-		$expected->throttle(['user' => 'john']);
715
-
716
-		$response = $this->loginController->tryLogin(
717
-			$loginChain,
718
-			$trustedDomainHelper,
719
-			'[email protected]',
720
-			'just wrong',
721
-			$rememberme,
722
-			'/apps/files'
723
-		);
724
-
725
-		$this->assertEquals($expected, $response);
726
-	}
39
+    /** @var LoginController */
40
+    private $loginController;
41
+
42
+    /** @var IRequest|MockObject */
43
+    private $request;
44
+
45
+    /** @var IUserManager|MockObject */
46
+    private $userManager;
47
+
48
+    /** @var IConfig|MockObject */
49
+    private $config;
50
+
51
+    /** @var ISession|MockObject */
52
+    private $session;
53
+
54
+    /** @var Session|MockObject */
55
+    private $userSession;
56
+
57
+    /** @var IURLGenerator|MockObject */
58
+    private $urlGenerator;
59
+
60
+    /** @var Manager|MockObject */
61
+    private $twoFactorManager;
62
+
63
+    /** @var Defaults|MockObject */
64
+    private $defaults;
65
+
66
+    /** @var IThrottler|MockObject */
67
+    private $throttler;
68
+
69
+    /** @var IInitialState|MockObject */
70
+    private $initialState;
71
+
72
+    /** @var \OC\Authentication\WebAuthn\Manager|MockObject */
73
+    private $webAuthnManager;
74
+
75
+    /** @var IManager|MockObject */
76
+    private $notificationManager;
77
+
78
+    /** @var IL10N|MockObject */
79
+    private $l;
80
+
81
+    /** @var IAppManager|MockObject */
82
+    private $appManager;
83
+
84
+    protected function setUp(): void {
85
+        parent::setUp();
86
+        $this->request = $this->createMock(IRequest::class);
87
+        $this->userManager = $this->createMock(\OC\User\Manager::class);
88
+        $this->config = $this->createMock(IConfig::class);
89
+        $this->session = $this->createMock(ISession::class);
90
+        $this->userSession = $this->createMock(Session::class);
91
+        $this->urlGenerator = $this->createMock(IURLGenerator::class);
92
+        $this->twoFactorManager = $this->createMock(Manager::class);
93
+        $this->defaults = $this->createMock(Defaults::class);
94
+        $this->throttler = $this->createMock(IThrottler::class);
95
+        $this->initialState = $this->createMock(IInitialState::class);
96
+        $this->webAuthnManager = $this->createMock(\OC\Authentication\WebAuthn\Manager::class);
97
+        $this->notificationManager = $this->createMock(IManager::class);
98
+        $this->l = $this->createMock(IL10N::class);
99
+        $this->appManager = $this->createMock(IAppManager::class);
100
+
101
+        $this->l->expects($this->any())
102
+            ->method('t')
103
+            ->willReturnCallback(function ($text, $parameters = []) {
104
+                return vsprintf($text, $parameters);
105
+            });
106
+
107
+
108
+        $this->request->method('getRemoteAddress')
109
+            ->willReturn('1.2.3.4');
110
+        $this->request->method('getHeader')
111
+            ->with('Origin')
112
+            ->willReturn('domain.example.com');
113
+        $this->throttler->method('getDelay')
114
+            ->with(
115
+                $this->equalTo('1.2.3.4'),
116
+                $this->equalTo('')
117
+            )->willReturn(1000);
118
+
119
+        $this->loginController = new LoginController(
120
+            'core',
121
+            $this->request,
122
+            $this->userManager,
123
+            $this->config,
124
+            $this->session,
125
+            $this->userSession,
126
+            $this->urlGenerator,
127
+            $this->defaults,
128
+            $this->throttler,
129
+            $this->initialState,
130
+            $this->webAuthnManager,
131
+            $this->notificationManager,
132
+            $this->l,
133
+            $this->appManager,
134
+        );
135
+    }
136
+
137
+    public function testLogoutWithoutToken(): void {
138
+        $this->request
139
+            ->expects($this->once())
140
+            ->method('getCookie')
141
+            ->with('nc_token')
142
+            ->willReturn(null);
143
+        $this->request
144
+            ->method('getServerProtocol')
145
+            ->willReturn('https');
146
+        $this->request
147
+            ->expects($this->once())
148
+            ->method('isUserAgent')
149
+            ->willReturn(false);
150
+        $this->config
151
+            ->expects($this->never())
152
+            ->method('deleteUserValue');
153
+        $this->urlGenerator
154
+            ->expects($this->once())
155
+            ->method('linkToRouteAbsolute')
156
+            ->with('core.login.showLoginForm')
157
+            ->willReturn('/login');
158
+
159
+        $expected = new RedirectResponse('/login');
160
+        $expected->addHeader('Clear-Site-Data', '"cache", "storage"');
161
+        $this->assertEquals($expected, $this->loginController->logout());
162
+    }
163
+
164
+    public function testLogoutNoClearSiteData(): void {
165
+        $this->request
166
+            ->expects($this->once())
167
+            ->method('getCookie')
168
+            ->with('nc_token')
169
+            ->willReturn(null);
170
+        $this->request
171
+            ->method('getServerProtocol')
172
+            ->willReturn('https');
173
+        $this->request
174
+            ->expects($this->once())
175
+            ->method('isUserAgent')
176
+            ->willReturn(true);
177
+        $this->urlGenerator
178
+            ->expects($this->once())
179
+            ->method('linkToRouteAbsolute')
180
+            ->with('core.login.showLoginForm')
181
+            ->willReturn('/login');
182
+
183
+        $expected = new RedirectResponse('/login');
184
+        $this->assertEquals($expected, $this->loginController->logout());
185
+    }
186
+
187
+    public function testLogoutWithToken(): void {
188
+        $this->request
189
+            ->expects($this->once())
190
+            ->method('getCookie')
191
+            ->with('nc_token')
192
+            ->willReturn('MyLoginToken');
193
+        $this->request
194
+            ->method('getServerProtocol')
195
+            ->willReturn('https');
196
+        $this->request
197
+            ->expects($this->once())
198
+            ->method('isUserAgent')
199
+            ->willReturn(false);
200
+        $user = $this->createMock(IUser::class);
201
+        $user
202
+            ->expects($this->once())
203
+            ->method('getUID')
204
+            ->willReturn('JohnDoe');
205
+        $this->userSession
206
+            ->expects($this->once())
207
+            ->method('getUser')
208
+            ->willReturn($user);
209
+        $this->config
210
+            ->expects($this->once())
211
+            ->method('deleteUserValue')
212
+            ->with('JohnDoe', 'login_token', 'MyLoginToken');
213
+        $this->urlGenerator
214
+            ->expects($this->once())
215
+            ->method('linkToRouteAbsolute')
216
+            ->with('core.login.showLoginForm')
217
+            ->willReturn('/login');
218
+
219
+        $expected = new RedirectResponse('/login');
220
+        $expected->addHeader('Clear-Site-Data', '"cache", "storage"');
221
+        $this->assertEquals($expected, $this->loginController->logout());
222
+    }
223
+
224
+    public function testShowLoginFormForLoggedInUsers(): void {
225
+        $this->userSession
226
+            ->expects($this->once())
227
+            ->method('isLoggedIn')
228
+            ->willReturn(true);
229
+        $this->urlGenerator
230
+            ->expects($this->once())
231
+            ->method('linkToDefaultPageUrl')
232
+            ->willReturn('/default/foo');
233
+
234
+        $expectedResponse = new RedirectResponse('/default/foo');
235
+        $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', ''));
236
+    }
237
+
238
+    public function testShowLoginFormWithErrorsInSession(): void {
239
+        $this->userSession
240
+            ->expects($this->once())
241
+            ->method('isLoggedIn')
242
+            ->willReturn(false);
243
+        $this->session
244
+            ->expects($this->once())
245
+            ->method('get')
246
+            ->with('loginMessages')
247
+            ->willReturn(
248
+                [
249
+                    [
250
+                        'ErrorArray1',
251
+                        'ErrorArray2',
252
+                    ],
253
+                    [
254
+                        'MessageArray1',
255
+                        'MessageArray2',
256
+                    ],
257
+                ]
258
+            );
259
+
260
+        $calls = [
261
+            [
262
+                'loginMessages',
263
+                [
264
+                    'MessageArray1',
265
+                    'MessageArray2',
266
+                    'This community release of Nextcloud is unsupported and push notifications are limited.',
267
+                ],
268
+            ],
269
+            [
270
+                'loginErrors',
271
+                [
272
+                    'ErrorArray1',
273
+                    'ErrorArray2',
274
+                ],
275
+            ],
276
+            [
277
+                'loginUsername',
278
+                '',
279
+            ]
280
+        ];
281
+        $this->initialState->expects($this->exactly(14))
282
+            ->method('provideInitialState')
283
+            ->willReturnCallback(function () use (&$calls): void {
284
+                $expected = array_shift($calls);
285
+                if (!empty($expected)) {
286
+                    $this->assertEquals($expected, func_get_args());
287
+                }
288
+            });
289
+
290
+        $expectedResponse = new TemplateResponse(
291
+            'core',
292
+            'login',
293
+            [
294
+                'alt_login' => [],
295
+                'pageTitle' => 'Login'
296
+            ],
297
+            'guest'
298
+        );
299
+        $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', ''));
300
+    }
301
+
302
+    public function testShowLoginFormForFlowAuth(): void {
303
+        $this->userSession
304
+            ->expects($this->once())
305
+            ->method('isLoggedIn')
306
+            ->willReturn(false);
307
+        $calls = [
308
+            [], [], [],
309
+            [
310
+                'loginAutocomplete',
311
+                false
312
+            ],
313
+            [
314
+                'loginCanRememberme',
315
+                false
316
+            ],
317
+            [
318
+                'loginRedirectUrl',
319
+                'login/flow'
320
+            ],
321
+        ];
322
+        $this->initialState->expects($this->exactly(15))
323
+            ->method('provideInitialState')
324
+            ->willReturnCallback(function () use (&$calls): void {
325
+                $expected = array_shift($calls);
326
+                if (!empty($expected)) {
327
+                    $this->assertEquals($expected, func_get_args());
328
+                }
329
+            });
330
+
331
+        $expectedResponse = new TemplateResponse(
332
+            'core',
333
+            'login',
334
+            [
335
+                'alt_login' => [],
336
+                'pageTitle' => 'Login'
337
+            ],
338
+            'guest'
339
+        );
340
+        $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', 'login/flow'));
341
+    }
342
+
343
+    /**
344
+     * @return array
345
+     */
346
+    public static function passwordResetDataProvider(): array {
347
+        return [
348
+            [
349
+                true,
350
+                true,
351
+            ],
352
+            [
353
+                false,
354
+                false,
355
+            ],
356
+        ];
357
+    }
358
+
359
+    #[DataProvider('passwordResetDataProvider')]
360
+    public function testShowLoginFormWithPasswordResetOption($canChangePassword,
361
+        $expectedResult): void {
362
+        $this->userSession
363
+            ->expects($this->once())
364
+            ->method('isLoggedIn')
365
+            ->willReturn(false);
366
+        $this->config
367
+            ->expects(self::once())
368
+            ->method('getSystemValue')
369
+            ->willReturnMap([
370
+                ['login_form_autocomplete', true, true],
371
+            ]);
372
+        $this->config
373
+            ->expects(self::once())
374
+            ->method('getSystemValueString')
375
+            ->willReturnMap([
376
+                ['lost_password_link', '', ''],
377
+            ]);
378
+        $user = $this->createMock(IUser::class);
379
+        $user
380
+            ->expects($this->once())
381
+            ->method('canChangePassword')
382
+            ->willReturn($canChangePassword);
383
+        $this->userManager
384
+            ->expects($this->once())
385
+            ->method('get')
386
+            ->with('LdapUser')
387
+            ->willReturn($user);
388
+        $calls = [
389
+            [], [],
390
+            [
391
+                'loginUsername',
392
+                'LdapUser'
393
+            ],
394
+            [], [], [], [],
395
+            [
396
+                'loginCanResetPassword',
397
+                $expectedResult
398
+            ],
399
+        ];
400
+        $this->initialState->expects($this->exactly(14))
401
+            ->method('provideInitialState')
402
+            ->willReturnCallback(function () use (&$calls): void {
403
+                $expected = array_shift($calls);
404
+                if (!empty($expected)) {
405
+                    $this->assertEquals($expected, func_get_args());
406
+                }
407
+            });
408
+
409
+        $expectedResponse = new TemplateResponse(
410
+            'core',
411
+            'login',
412
+            [
413
+                'alt_login' => [],
414
+                'pageTitle' => 'Login'
415
+            ],
416
+            'guest'
417
+        );
418
+        $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('LdapUser', ''));
419
+    }
420
+
421
+    public function testShowLoginFormForUserNamed0(): void {
422
+        $this->userSession
423
+            ->expects($this->once())
424
+            ->method('isLoggedIn')
425
+            ->willReturn(false);
426
+        $this->config
427
+            ->expects(self::once())
428
+            ->method('getSystemValue')
429
+            ->willReturnMap([
430
+                ['login_form_autocomplete', true, true],
431
+            ]);
432
+        $this->config
433
+            ->expects(self::once())
434
+            ->method('getSystemValueString')
435
+            ->willReturnMap([
436
+                ['lost_password_link', '', ''],
437
+            ]);
438
+        $user = $this->createMock(IUser::class);
439
+        $user->expects($this->once())
440
+            ->method('canChangePassword')
441
+            ->willReturn(false);
442
+        $this->userManager
443
+            ->expects($this->once())
444
+            ->method('get')
445
+            ->with('0')
446
+            ->willReturn($user);
447
+        $calls = [
448
+            [], [], [],
449
+            [
450
+                'loginAutocomplete',
451
+                true
452
+            ],
453
+            [
454
+                'loginCanRememberme',
455
+                false
456
+            ],
457
+            [],
458
+            [
459
+                'loginResetPasswordLink',
460
+                false
461
+            ],
462
+            [
463
+                'loginCanResetPassword',
464
+                false
465
+            ],
466
+        ];
467
+        $this->initialState->expects($this->exactly(14))
468
+            ->method('provideInitialState')
469
+            ->willReturnCallback(function () use (&$calls): void {
470
+                $expected = array_shift($calls);
471
+                if (!empty($expected)) {
472
+                    $this->assertEquals($expected, func_get_args());
473
+                }
474
+            });
475
+
476
+        $expectedResponse = new TemplateResponse(
477
+            'core',
478
+            'login',
479
+            [
480
+                'alt_login' => [],
481
+                'pageTitle' => 'Login'
482
+            ],
483
+            'guest'
484
+        );
485
+        $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('0', ''));
486
+    }
487
+
488
+    public static function remembermeProvider(): array {
489
+        return [
490
+            [
491
+                true,
492
+            ],
493
+            [
494
+                false,
495
+            ],
496
+        ];
497
+    }
498
+
499
+    #[DataProvider('remembermeProvider')]
500
+    public function testLoginWithInvalidCredentials(bool $rememberme): void {
501
+        $user = 'MyUserName';
502
+        $password = 'secret';
503
+        $loginPageUrl = '/login?redirect_url=/apps/files';
504
+        $loginChain = $this->createMock(LoginChain::class);
505
+        $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
506
+        $trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
507
+        $this->request
508
+            ->expects($this->once())
509
+            ->method('passesCSRFCheck')
510
+            ->willReturn(true);
511
+        $loginData = new LoginData(
512
+            $this->request,
513
+            $user,
514
+            $password,
515
+            $rememberme,
516
+            '/apps/files'
517
+        );
518
+        $loginResult = LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD);
519
+        $loginChain->expects($this->once())
520
+            ->method('process')
521
+            ->with($this->equalTo($loginData))
522
+            ->willReturn($loginResult);
523
+        $this->urlGenerator->expects($this->once())
524
+            ->method('linkToRoute')
525
+            ->with('core.login.showLoginForm', [
526
+                'user' => $user,
527
+                'redirect_url' => '/apps/files',
528
+                'direct' => 1,
529
+            ])
530
+            ->willReturn($loginPageUrl);
531
+        $expected = new RedirectResponse($loginPageUrl);
532
+        $expected->throttle(['user' => 'MyUserName']);
533
+
534
+        $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, $rememberme, '/apps/files');
535
+
536
+        $this->assertEquals($expected, $response);
537
+    }
538
+
539
+    #[DataProvider('remembermeProvider')]
540
+    public function testLoginWithValidCredentials(bool $rememberme): void {
541
+        $user = 'MyUserName';
542
+        $password = 'secret';
543
+        $loginChain = $this->createMock(LoginChain::class);
544
+        $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
545
+        $trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
546
+        $this->request
547
+            ->expects($this->once())
548
+            ->method('passesCSRFCheck')
549
+            ->willReturn(true);
550
+        $loginData = new LoginData(
551
+            $this->request,
552
+            $user,
553
+            $password,
554
+            $rememberme,
555
+        );
556
+        $loginResult = LoginResult::success($loginData);
557
+        $loginChain->expects($this->once())
558
+            ->method('process')
559
+            ->with($this->equalTo($loginData))
560
+            ->willReturn($loginResult);
561
+        $this->urlGenerator
562
+            ->expects($this->once())
563
+            ->method('linkToDefaultPageUrl')
564
+            ->willReturn('/default/foo');
565
+
566
+        $expected = new RedirectResponse('/default/foo');
567
+        $this->assertEquals($expected, $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, $rememberme));
568
+    }
569
+
570
+    #[DataProvider('remembermeProvider')]
571
+    public function testLoginWithoutPassedCsrfCheckAndNotLoggedIn(bool $rememberme): void {
572
+        /** @var IUser|MockObject $user */
573
+        $user = $this->createMock(IUser::class);
574
+        $user->expects($this->any())
575
+            ->method('getUID')
576
+            ->willReturn('jane');
577
+        $password = 'secret';
578
+        $originalUrl = 'another%20url';
579
+        $loginChain = $this->createMock(LoginChain::class);
580
+        $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
581
+        $trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
582
+        $this->request
583
+            ->expects($this->once())
584
+            ->method('passesCSRFCheck')
585
+            ->willReturn(false);
586
+        $this->userSession
587
+            ->method('isLoggedIn')
588
+            ->with()
589
+            ->willReturn(false);
590
+        $this->config->expects($this->never())
591
+            ->method('deleteUserValue');
592
+        $this->userSession->expects($this->never())
593
+            ->method('createRememberMeToken');
594
+
595
+        $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $rememberme, $originalUrl);
596
+
597
+        $expected = new RedirectResponse('');
598
+        $expected->throttle(['user' => 'Jane']);
599
+        $this->assertEquals($expected, $response);
600
+    }
601
+
602
+    #[DataProvider('remembermeProvider')]
603
+    public function testLoginWithoutPassedCsrfCheckAndLoggedIn(bool $rememberme): void {
604
+        /** @var IUser|MockObject $user */
605
+        $user = $this->createMock(IUser::class);
606
+        $user->expects($this->any())
607
+            ->method('getUID')
608
+            ->willReturn('jane');
609
+        $password = 'secret';
610
+        $originalUrl = 'another url';
611
+        $redirectUrl = 'http://localhost/another url';
612
+        $loginChain = $this->createMock(LoginChain::class);
613
+        $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
614
+        $trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
615
+        $this->request
616
+            ->expects($this->once())
617
+            ->method('passesCSRFCheck')
618
+            ->willReturn(false);
619
+        $this->userSession
620
+            ->method('isLoggedIn')
621
+            ->with()
622
+            ->willReturn(true);
623
+        $this->urlGenerator->expects($this->once())
624
+            ->method('getAbsoluteURL')
625
+            ->with(urldecode($originalUrl))
626
+            ->willReturn($redirectUrl);
627
+        $this->config->expects($this->never())
628
+            ->method('deleteUserValue');
629
+        $this->userSession->expects($this->never())
630
+            ->method('createRememberMeToken');
631
+        $this->config
632
+            ->method('getSystemValue')
633
+            ->with('remember_login_cookie_lifetime')
634
+            ->willReturn(1234);
635
+
636
+        $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $rememberme, $originalUrl);
637
+
638
+        $expected = new RedirectResponse($redirectUrl);
639
+        $this->assertEquals($expected, $response);
640
+    }
641
+
642
+    #[DataProvider('remembermeProvider')]
643
+    public function testLoginWithValidCredentialsAndRedirectUrl(bool $rememberme): void {
644
+        $user = 'MyUserName';
645
+        $password = 'secret';
646
+        $redirectUrl = 'https://next.cloud/apps/mail';
647
+        $loginChain = $this->createMock(LoginChain::class);
648
+        $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
649
+        $trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
650
+        $this->request
651
+            ->expects($this->once())
652
+            ->method('passesCSRFCheck')
653
+            ->willReturn(true);
654
+        $loginData = new LoginData(
655
+            $this->request,
656
+            $user,
657
+            $password,
658
+            $rememberme,
659
+            '/apps/mail'
660
+        );
661
+        $loginResult = LoginResult::success($loginData);
662
+        $loginChain->expects($this->once())
663
+            ->method('process')
664
+            ->with($this->equalTo($loginData))
665
+            ->willReturn($loginResult);
666
+        $this->userSession->expects($this->once())
667
+            ->method('isLoggedIn')
668
+            ->willReturn(true);
669
+        $this->urlGenerator->expects($this->once())
670
+            ->method('getAbsoluteURL')
671
+            ->with('/apps/mail')
672
+            ->willReturn($redirectUrl);
673
+        $expected = new RedirectResponse($redirectUrl);
674
+
675
+        $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, $rememberme, '/apps/mail');
676
+
677
+        $this->assertEquals($expected, $response);
678
+    }
679
+
680
+    #[DataProvider('remembermeProvider')]
681
+    public function testToNotLeakLoginName(bool $rememberme): void {
682
+        $loginChain = $this->createMock(LoginChain::class);
683
+        $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
684
+        $trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
685
+        $this->request
686
+            ->expects($this->once())
687
+            ->method('passesCSRFCheck')
688
+            ->willReturn(true);
689
+        $loginPageUrl = '/login?redirect_url=/apps/files';
690
+        $loginData = new LoginData(
691
+            $this->request,
692
+            '[email protected]',
693
+            'just wrong',
694
+            $rememberme,
695
+            '/apps/files'
696
+        );
697
+        $loginResult = LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD);
698
+        $loginChain->expects($this->once())
699
+            ->method('process')
700
+            ->with($this->equalTo($loginData))
701
+            ->willReturnCallback(function (LoginData $data) use ($loginResult) {
702
+                $data->setUsername('john');
703
+                return $loginResult;
704
+            });
705
+        $this->urlGenerator->expects($this->once())
706
+            ->method('linkToRoute')
707
+            ->with('core.login.showLoginForm', [
708
+                'user' => '[email protected]',
709
+                'redirect_url' => '/apps/files',
710
+                'direct' => 1,
711
+            ])
712
+            ->willReturn($loginPageUrl);
713
+        $expected = new RedirectResponse($loginPageUrl);
714
+        $expected->throttle(['user' => 'john']);
715
+
716
+        $response = $this->loginController->tryLogin(
717
+            $loginChain,
718
+            $trustedDomainHelper,
719
+            '[email protected]',
720
+            'just wrong',
721
+            $rememberme,
722
+            '/apps/files'
723
+        );
724
+
725
+        $this->assertEquals($expected, $response);
726
+    }
727 727
 }
Please login to merge, or discard this patch.
Spacing   +6 added lines, -6 removed lines patch added patch discarded remove patch
@@ -100,7 +100,7 @@  discard block
 block discarded – undo
100 100
 
101 101
 		$this->l->expects($this->any())
102 102
 			->method('t')
103
-			->willReturnCallback(function ($text, $parameters = []) {
103
+			->willReturnCallback(function($text, $parameters = []) {
104 104
 				return vsprintf($text, $parameters);
105 105
 			});
106 106
 
@@ -280,7 +280,7 @@  discard block
 block discarded – undo
280 280
 		];
281 281
 		$this->initialState->expects($this->exactly(14))
282 282
 			->method('provideInitialState')
283
-			->willReturnCallback(function () use (&$calls): void {
283
+			->willReturnCallback(function() use (&$calls): void {
284 284
 				$expected = array_shift($calls);
285 285
 				if (!empty($expected)) {
286 286
 					$this->assertEquals($expected, func_get_args());
@@ -321,7 +321,7 @@  discard block
 block discarded – undo
321 321
 		];
322 322
 		$this->initialState->expects($this->exactly(15))
323 323
 			->method('provideInitialState')
324
-			->willReturnCallback(function () use (&$calls): void {
324
+			->willReturnCallback(function() use (&$calls): void {
325 325
 				$expected = array_shift($calls);
326 326
 				if (!empty($expected)) {
327 327
 					$this->assertEquals($expected, func_get_args());
@@ -399,7 +399,7 @@  discard block
 block discarded – undo
399 399
 		];
400 400
 		$this->initialState->expects($this->exactly(14))
401 401
 			->method('provideInitialState')
402
-			->willReturnCallback(function () use (&$calls): void {
402
+			->willReturnCallback(function() use (&$calls): void {
403 403
 				$expected = array_shift($calls);
404 404
 				if (!empty($expected)) {
405 405
 					$this->assertEquals($expected, func_get_args());
@@ -466,7 +466,7 @@  discard block
 block discarded – undo
466 466
 		];
467 467
 		$this->initialState->expects($this->exactly(14))
468 468
 			->method('provideInitialState')
469
-			->willReturnCallback(function () use (&$calls): void {
469
+			->willReturnCallback(function() use (&$calls): void {
470 470
 				$expected = array_shift($calls);
471 471
 				if (!empty($expected)) {
472 472
 					$this->assertEquals($expected, func_get_args());
@@ -698,7 +698,7 @@  discard block
 block discarded – undo
698 698
 		$loginChain->expects($this->once())
699 699
 			->method('process')
700 700
 			->with($this->equalTo($loginData))
701
-			->willReturnCallback(function (LoginData $data) use ($loginResult) {
701
+			->willReturnCallback(function(LoginData $data) use ($loginResult) {
702 702
 				$data->setUsername('john');
703 703
 				return $loginResult;
704 704
 			});
Please login to merge, or discard this patch.