Issues (2553)

lib/private/User/Session.php (1 issue)

1
<?php
2
/**
3
 * @copyright Copyright (c) 2017, Sandro Lutz <[email protected]>
4
 * @copyright Copyright (c) 2016, ownCloud, Inc.
5
 *
6
 * @author Arthur Schiwon <[email protected]>
7
 * @author Bernhard Posselt <[email protected]>
8
 * @author Bjoern Schiessle <[email protected]>
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Felix Rupp <[email protected]>
11
 * @author Greta Doci <[email protected]>
12
 * @author Joas Schilling <[email protected]>
13
 * @author Jörn Friedrich Dreyer <[email protected]>
14
 * @author Lionel Elie Mamane <[email protected]>
15
 * @author Lukas Reschke <[email protected]>
16
 * @author Morris Jobke <[email protected]>
17
 * @author Robin Appelman <[email protected]>
18
 * @author Robin McCorkell <[email protected]>
19
 * @author Roeland Jago Douma <[email protected]>
20
 * @author Sandro Lutz <[email protected]>
21
 * @author Thomas Müller <[email protected]>
22
 * @author Vincent Petry <[email protected]>
23
 *
24
 * @license AGPL-3.0
25
 *
26
 * This code is free software: you can redistribute it and/or modify
27
 * it under the terms of the GNU Affero General Public License, version 3,
28
 * as published by the Free Software Foundation.
29
 *
30
 * This program is distributed in the hope that it will be useful,
31
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
32
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33
 * GNU Affero General Public License for more details.
34
 *
35
 * You should have received a copy of the GNU Affero General Public License, version 3,
36
 * along with this program. If not, see <http://www.gnu.org/licenses/>
37
 *
38
 */
39
namespace OC\User;
40
41
use OC;
42
use OC\Authentication\Exceptions\ExpiredTokenException;
43
use OC\Authentication\Exceptions\InvalidTokenException;
44
use OC\Authentication\Exceptions\PasswordlessTokenException;
45
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
46
use OC\Authentication\Token\IProvider;
47
use OC\Authentication\Token\IToken;
48
use OC\Hooks\Emitter;
49
use OC\Hooks\PublicEmitter;
50
use OC_User;
51
use OC_Util;
52
use OCA\DAV\Connector\Sabre\Auth;
53
use OCP\AppFramework\Utility\ITimeFactory;
54
use OCP\EventDispatcher\IEventDispatcher;
55
use OCP\Files\NotPermittedException;
56
use OCP\IConfig;
57
use OCP\IRequest;
58
use OCP\ISession;
59
use OCP\IUser;
60
use OCP\IUserSession;
61
use OCP\Lockdown\ILockdownManager;
62
use OCP\Security\Bruteforce\IThrottler;
63
use OCP\Security\ISecureRandom;
64
use OCP\Session\Exceptions\SessionNotAvailableException;
65
use OCP\User\Events\PostLoginEvent;
66
use OCP\Util;
67
use Psr\Log\LoggerInterface;
68
use Symfony\Component\EventDispatcher\GenericEvent;
69
70
/**
71
 * Class Session
72
 *
73
 * Hooks available in scope \OC\User:
74
 * - preSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
75
 * - postSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
76
 * - preDelete(\OC\User\User $user)
77
 * - postDelete(\OC\User\User $user)
78
 * - preCreateUser(string $uid, string $password)
79
 * - postCreateUser(\OC\User\User $user)
80
 * - assignedUserId(string $uid)
81
 * - preUnassignedUserId(string $uid)
82
 * - postUnassignedUserId(string $uid)
83
 * - preLogin(string $user, string $password)
84
 * - postLogin(\OC\User\User $user, string $loginName, string $password, boolean $isTokenLogin)
85
 * - preRememberedLogin(string $uid)
86
 * - postRememberedLogin(\OC\User\User $user)
87
 * - logout()
88
 * - postLogout()
89
 *
90
 * @package OC\User
91
 */
92
class Session implements IUserSession, Emitter {
93
	/** @var Manager $manager */
94
	private $manager;
95
96
	/** @var ISession $session */
97
	private $session;
98
99
	/** @var ITimeFactory */
100
	private $timeFactory;
101
102
	/** @var IProvider */
103
	private $tokenProvider;
104
105
	/** @var IConfig */
106
	private $config;
107
108
	/** @var User $activeUser */
109
	protected $activeUser;
110
111
	/** @var ISecureRandom */
112
	private $random;
113
114
	/** @var ILockdownManager  */
115
	private $lockdownManager;
116
117
	private LoggerInterface $logger;
118
	/** @var IEventDispatcher */
119
	private $dispatcher;
120
121
	public function __construct(Manager $manager,
122
								ISession $session,
123
								ITimeFactory $timeFactory,
124
								?IProvider $tokenProvider,
125
								IConfig $config,
126
								ISecureRandom $random,
127
								ILockdownManager $lockdownManager,
128
								LoggerInterface $logger,
129
								IEventDispatcher $dispatcher
130
	) {
131
		$this->manager = $manager;
132
		$this->session = $session;
133
		$this->timeFactory = $timeFactory;
134
		$this->tokenProvider = $tokenProvider;
135
		$this->config = $config;
136
		$this->random = $random;
137
		$this->lockdownManager = $lockdownManager;
138
		$this->logger = $logger;
139
		$this->dispatcher = $dispatcher;
140
	}
141
142
	/**
143
	 * @param IProvider $provider
144
	 */
145
	public function setTokenProvider(IProvider $provider) {
146
		$this->tokenProvider = $provider;
147
	}
148
149
	/**
150
	 * @param string $scope
151
	 * @param string $method
152
	 * @param callable $callback
153
	 */
154
	public function listen($scope, $method, callable $callback) {
155
		$this->manager->listen($scope, $method, $callback);
156
	}
157
158
	/**
159
	 * @param string $scope optional
160
	 * @param string $method optional
161
	 * @param callable $callback optional
162
	 */
163
	public function removeListener($scope = null, $method = null, callable $callback = null) {
164
		$this->manager->removeListener($scope, $method, $callback);
165
	}
166
167
	/**
168
	 * get the manager object
169
	 *
170
	 * @return Manager|PublicEmitter
171
	 */
172
	public function getManager() {
173
		return $this->manager;
174
	}
175
176
	/**
177
	 * get the session object
178
	 *
179
	 * @return ISession
180
	 */
181
	public function getSession() {
182
		return $this->session;
183
	}
184
185
	/**
186
	 * set the session object
187
	 *
188
	 * @param ISession $session
189
	 */
190
	public function setSession(ISession $session) {
191
		if ($this->session instanceof ISession) {
192
			$this->session->close();
193
		}
194
		$this->session = $session;
195
		$this->activeUser = null;
196
	}
197
198
	/**
199
	 * set the currently active user
200
	 *
201
	 * @param IUser|null $user
202
	 */
203
	public function setUser($user) {
204
		if (is_null($user)) {
205
			$this->session->remove('user_id');
206
		} else {
207
			$this->session->set('user_id', $user->getUID());
208
		}
209
		$this->activeUser = $user;
210
	}
211
212
	/**
213
	 * get the current active user
214
	 *
215
	 * @return IUser|null Current user, otherwise null
216
	 */
217
	public function getUser() {
218
		// FIXME: This is a quick'n dirty work-around for the incognito mode as
219
		// described at https://github.com/owncloud/core/pull/12912#issuecomment-67391155
220
		if (OC_User::isIncognitoMode()) {
221
			return null;
222
		}
223
		if (is_null($this->activeUser)) {
224
			$uid = $this->session->get('user_id');
225
			if (is_null($uid)) {
226
				return null;
227
			}
228
			$this->activeUser = $this->manager->get($uid);
229
			if (is_null($this->activeUser)) {
230
				return null;
231
			}
232
			$this->validateSession();
233
		}
234
		return $this->activeUser;
235
	}
236
237
	/**
238
	 * Validate whether the current session is valid
239
	 *
240
	 * - For token-authenticated clients, the token validity is checked
241
	 * - For browsers, the session token validity is checked
242
	 */
243
	protected function validateSession() {
244
		$token = null;
245
		$appPassword = $this->session->get('app_password');
246
247
		if (is_null($appPassword)) {
248
			try {
249
				$token = $this->session->getId();
250
			} catch (SessionNotAvailableException $ex) {
251
				return;
252
			}
253
		} else {
254
			$token = $appPassword;
255
		}
256
257
		if (!$this->validateToken($token)) {
258
			// Session was invalidated
259
			$this->logout();
260
		}
261
	}
262
263
	/**
264
	 * Checks whether the user is logged in
265
	 *
266
	 * @return bool if logged in
267
	 */
268
	public function isLoggedIn() {
269
		$user = $this->getUser();
270
		if (is_null($user)) {
271
			return false;
272
		}
273
274
		return $user->isEnabled();
275
	}
276
277
	/**
278
	 * set the login name
279
	 *
280
	 * @param string|null $loginName for the logged in user
281
	 */
282
	public function setLoginName($loginName) {
283
		if (is_null($loginName)) {
284
			$this->session->remove('loginname');
285
		} else {
286
			$this->session->set('loginname', $loginName);
287
		}
288
	}
289
290
	/**
291
	 * Get the login name of the current user
292
	 *
293
	 * @return ?string
294
	 */
295
	public function getLoginName() {
296
		if ($this->activeUser) {
297
			return $this->session->get('loginname');
298
		}
299
300
		$uid = $this->session->get('user_id');
301
		if ($uid) {
302
			$this->activeUser = $this->manager->get($uid);
303
			return $this->session->get('loginname');
304
		}
305
306
		return null;
307
	}
308
309
	/**
310
	 * @return null|string
311
	 */
312
	public function getImpersonatingUserID(): ?string {
313
		return $this->session->get('oldUserId');
314
	}
315
316
	public function setImpersonatingUserID(bool $useCurrentUser = true): void {
317
		if ($useCurrentUser === false) {
318
			$this->session->remove('oldUserId');
319
			return;
320
		}
321
322
		$currentUser = $this->getUser();
323
324
		if ($currentUser === null) {
325
			throw new \OC\User\NoUserException();
326
		}
327
		$this->session->set('oldUserId', $currentUser->getUID());
328
	}
329
	/**
330
	 * set the token id
331
	 *
332
	 * @param int|null $token that was used to log in
333
	 */
334
	protected function setToken($token) {
335
		if ($token === null) {
336
			$this->session->remove('token-id');
337
		} else {
338
			$this->session->set('token-id', $token);
339
		}
340
	}
341
342
	/**
343
	 * try to log in with the provided credentials
344
	 *
345
	 * @param string $uid
346
	 * @param string $password
347
	 * @return boolean|null
348
	 * @throws LoginException
349
	 */
350
	public function login($uid, $password) {
351
		$this->session->regenerateId();
352
		if ($this->validateToken($password, $uid)) {
353
			return $this->loginWithToken($password);
354
		}
355
		return $this->loginWithPassword($uid, $password);
356
	}
357
358
	/**
359
	 * @param IUser $user
360
	 * @param array $loginDetails
361
	 * @param bool $regenerateSessionId
362
	 * @return true returns true if login successful or an exception otherwise
363
	 * @throws LoginException
364
	 */
365
	public function completeLogin(IUser $user, array $loginDetails, $regenerateSessionId = true) {
366
		if (!$user->isEnabled()) {
367
			// disabled users can not log in
368
			// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
369
			$message = \OC::$server->getL10N('lib')->t('User disabled');
370
			throw new LoginException($message);
371
		}
372
373
		if ($regenerateSessionId) {
374
			$this->session->regenerateId();
375
			$this->session->remove(Auth::DAV_AUTHENTICATED);
376
		}
377
378
		$this->setUser($user);
379
		$this->setLoginName($loginDetails['loginName']);
380
381
		$isToken = isset($loginDetails['token']) && $loginDetails['token'] instanceof IToken;
382
		if ($isToken) {
383
			$this->setToken($loginDetails['token']->getId());
384
			$this->lockdownManager->setToken($loginDetails['token']);
385
			$firstTimeLogin = false;
386
		} else {
387
			$this->setToken(null);
388
			$firstTimeLogin = $user->updateLastLoginTimestamp();
389
		}
390
391
		$this->dispatcher->dispatchTyped(new PostLoginEvent(
392
			$user,
393
			$loginDetails['loginName'],
394
			$loginDetails['password'],
395
			$isToken
396
		));
397
		$this->manager->emit('\OC\User', 'postLogin', [
398
			$user,
399
			$loginDetails['loginName'],
400
			$loginDetails['password'],
401
			$isToken,
402
		]);
403
		if ($this->isLoggedIn()) {
404
			$this->prepareUserLogin($firstTimeLogin, $regenerateSessionId);
405
			return true;
406
		}
407
408
		$message = \OC::$server->getL10N('lib')->t('Login canceled by app');
409
		throw new LoginException($message);
410
	}
411
412
	/**
413
	 * Tries to log in a client
414
	 *
415
	 * Checks token auth enforced
416
	 * Checks 2FA enabled
417
	 *
418
	 * @param string $user
419
	 * @param string $password
420
	 * @param IRequest $request
421
	 * @param OC\Security\Bruteforce\Throttler $throttler
422
	 * @throws LoginException
423
	 * @throws PasswordLoginForbiddenException
424
	 * @return boolean
425
	 */
426
	public function logClientIn($user,
427
								$password,
428
								IRequest $request,
429
								OC\Security\Bruteforce\Throttler $throttler) {
430
		$remoteAddress = $request->getRemoteAddress();
431
		$currentDelay = $throttler->sleepDelayOrThrowOnMax($remoteAddress, 'login');
432
433
		if ($this->manager instanceof PublicEmitter) {
0 ignored issues
show
$this->manager is always a sub-type of OC\Hooks\PublicEmitter.
Loading history...
434
			$this->manager->emit('\OC\User', 'preLogin', [$user, $password]);
435
		}
436
437
		try {
438
			$isTokenPassword = $this->isTokenPassword($password);
439
		} catch (ExpiredTokenException $e) {
440
			// Just return on an expired token no need to check further or record a failed login
441
			return false;
442
		}
443
444
		if (!$isTokenPassword && $this->isTokenAuthEnforced()) {
445
			throw new PasswordLoginForbiddenException();
446
		}
447
		if (!$isTokenPassword && $this->isTwoFactorEnforced($user)) {
448
			throw new PasswordLoginForbiddenException();
449
		}
450
451
		// Try to login with this username and password
452
		if (!$this->login($user, $password)) {
453
			// Failed, maybe the user used their email address
454
			if (!filter_var($user, FILTER_VALIDATE_EMAIL)) {
455
				$this->handleLoginFailed($throttler, $currentDelay, $remoteAddress, $user, $password);
456
				return false;
457
			}
458
			$users = $this->manager->getByEmail($user);
459
			if (!(\count($users) === 1 && $this->login($users[0]->getUID(), $password))) {
460
				$this->handleLoginFailed($throttler, $currentDelay, $remoteAddress, $user, $password);
461
				return false;
462
			}
463
		}
464
465
		if ($isTokenPassword) {
466
			$this->session->set('app_password', $password);
467
		} elseif ($this->supportsCookies($request)) {
468
			// Password login, but cookies supported -> create (browser) session token
469
			$this->createSessionToken($request, $this->getUser()->getUID(), $user, $password);
470
		}
471
472
		return true;
473
	}
474
475
	private function handleLoginFailed(IThrottler $throttler, int $currentDelay, string $remoteAddress, string $user, ?string $password) {
476
		$this->logger->warning("Login failed: '" . $user . "' (Remote IP: '" . $remoteAddress . "')", ['app' => 'core']);
477
478
		$throttler->registerAttempt('login', $remoteAddress, ['user' => $user]);
479
		$this->dispatcher->dispatchTyped(new OC\Authentication\Events\LoginFailed($user, $password));
480
481
		if ($currentDelay === 0) {
482
			$throttler->sleepDelayOrThrowOnMax($remoteAddress, 'login');
483
		}
484
	}
485
486
	protected function supportsCookies(IRequest $request) {
487
		if (!is_null($request->getCookie('cookie_test'))) {
488
			return true;
489
		}
490
		setcookie('cookie_test', 'test', $this->timeFactory->getTime() + 3600);
491
		return false;
492
	}
493
494
	private function isTokenAuthEnforced(): bool {
495
		return $this->config->getSystemValueBool('token_auth_enforced', false);
496
	}
497
498
	protected function isTwoFactorEnforced($username) {
499
		Util::emitHook(
500
			'\OCA\Files_Sharing\API\Server2Server',
501
			'preLoginNameUsedAsUserName',
502
			['uid' => &$username]
503
		);
504
		$user = $this->manager->get($username);
505
		if (is_null($user)) {
506
			$users = $this->manager->getByEmail($username);
507
			if (empty($users)) {
508
				return false;
509
			}
510
			if (count($users) !== 1) {
511
				return true;
512
			}
513
			$user = $users[0];
514
		}
515
		// DI not possible due to cyclic dependencies :'-/
516
		return OC::$server->getTwoFactorAuthManager()->isTwoFactorAuthenticated($user);
517
	}
518
519
	/**
520
	 * Check if the given 'password' is actually a device token
521
	 *
522
	 * @param string $password
523
	 * @return boolean
524
	 * @throws ExpiredTokenException
525
	 */
526
	public function isTokenPassword($password) {
527
		try {
528
			$this->tokenProvider->getToken($password);
529
			return true;
530
		} catch (ExpiredTokenException $e) {
531
			throw $e;
532
		} catch (InvalidTokenException $ex) {
533
			$this->logger->debug('Token is not valid: ' . $ex->getMessage(), [
534
				'exception' => $ex,
535
			]);
536
			return false;
537
		}
538
	}
539
540
	protected function prepareUserLogin($firstTimeLogin, $refreshCsrfToken = true) {
541
		if ($refreshCsrfToken) {
542
			// TODO: mock/inject/use non-static
543
			// Refresh the token
544
			\OC::$server->getCsrfTokenManager()->refreshToken();
545
		}
546
547
		if ($firstTimeLogin) {
548
			//we need to pass the user name, which may differ from login name
549
			$user = $this->getUser()->getUID();
550
			OC_Util::setupFS($user);
551
552
			// TODO: lock necessary?
553
			//trigger creation of user home and /files folder
554
			$userFolder = \OC::$server->getUserFolder($user);
555
556
			try {
557
				// copy skeleton
558
				\OC_Util::copySkeleton($user, $userFolder);
559
			} catch (NotPermittedException $ex) {
560
				// read only uses
561
			}
562
563
			// trigger any other initialization
564
			\OC::$server->getEventDispatcher()->dispatch(IUser::class . '::firstLogin', new GenericEvent($this->getUser()));
565
		}
566
	}
567
568
	/**
569
	 * Tries to login the user with HTTP Basic Authentication
570
	 *
571
	 * @todo do not allow basic auth if the user is 2FA enforced
572
	 * @param IRequest $request
573
	 * @param OC\Security\Bruteforce\Throttler $throttler
574
	 * @return boolean if the login was successful
575
	 */
576
	public function tryBasicAuthLogin(IRequest $request,
577
									  OC\Security\Bruteforce\Throttler $throttler) {
578
		if (!empty($request->server['PHP_AUTH_USER']) && !empty($request->server['PHP_AUTH_PW'])) {
579
			try {
580
				if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request, $throttler)) {
581
					/**
582
					 * Add DAV authenticated. This should in an ideal world not be
583
					 * necessary but the iOS App reads cookies from anywhere instead
584
					 * only the DAV endpoint.
585
					 * This makes sure that the cookies will be valid for the whole scope
586
					 * @see https://github.com/owncloud/core/issues/22893
587
					 */
588
					$this->session->set(
589
						Auth::DAV_AUTHENTICATED, $this->getUser()->getUID()
590
					);
591
592
					// Set the last-password-confirm session to make the sudo mode work
593
					$this->session->set('last-password-confirm', $this->timeFactory->getTime());
594
595
					return true;
596
				}
597
				// If credentials were provided, they need to be valid, otherwise we do boom
598
				throw new LoginException();
599
			} catch (PasswordLoginForbiddenException $ex) {
600
				// Nothing to do
601
			}
602
		}
603
		return false;
604
	}
605
606
	/**
607
	 * Log an user in via login name and password
608
	 *
609
	 * @param string $uid
610
	 * @param string $password
611
	 * @return boolean
612
	 * @throws LoginException if an app canceld the login process or the user is not enabled
613
	 */
614
	private function loginWithPassword($uid, $password) {
615
		$user = $this->manager->checkPasswordNoLogging($uid, $password);
616
		if ($user === false) {
617
			// Password check failed
618
			return false;
619
		}
620
621
		return $this->completeLogin($user, ['loginName' => $uid, 'password' => $password], false);
622
	}
623
624
	/**
625
	 * Log an user in with a given token (id)
626
	 *
627
	 * @param string $token
628
	 * @return boolean
629
	 * @throws LoginException if an app canceled the login process or the user is not enabled
630
	 */
631
	private function loginWithToken($token) {
632
		try {
633
			$dbToken = $this->tokenProvider->getToken($token);
634
		} catch (InvalidTokenException $ex) {
635
			return false;
636
		}
637
		$uid = $dbToken->getUID();
638
639
		// When logging in with token, the password must be decrypted first before passing to login hook
640
		$password = '';
641
		try {
642
			$password = $this->tokenProvider->getPassword($dbToken, $token);
643
		} catch (PasswordlessTokenException $ex) {
644
			// Ignore and use empty string instead
645
		}
646
647
		$this->manager->emit('\OC\User', 'preLogin', [$dbToken->getLoginName(), $password]);
648
649
		$user = $this->manager->get($uid);
650
		if (is_null($user)) {
651
			// user does not exist
652
			return false;
653
		}
654
655
		return $this->completeLogin(
656
			$user,
657
			[
658
				'loginName' => $dbToken->getLoginName(),
659
				'password' => $password,
660
				'token' => $dbToken
661
			],
662
			false);
663
	}
664
665
	/**
666
	 * Create a new session token for the given user credentials
667
	 *
668
	 * @param IRequest $request
669
	 * @param string $uid user UID
670
	 * @param string $loginName login name
671
	 * @param string $password
672
	 * @param int $remember
673
	 * @return boolean
674
	 */
675
	public function createSessionToken(IRequest $request, $uid, $loginName, $password = null, $remember = IToken::DO_NOT_REMEMBER) {
676
		if (is_null($this->manager->get($uid))) {
677
			// User does not exist
678
			return false;
679
		}
680
		$name = isset($request->server['HTTP_USER_AGENT']) ? mb_convert_encoding($request->server['HTTP_USER_AGENT'], 'UTF-8', 'ISO-8859-1') : 'unknown browser';
681
		try {
682
			$sessionId = $this->session->getId();
683
			$pwd = $this->getPassword($password);
684
			// Make sure the current sessionId has no leftover tokens
685
			$this->tokenProvider->invalidateToken($sessionId);
686
			$this->tokenProvider->generateToken($sessionId, $uid, $loginName, $pwd, $name, IToken::TEMPORARY_TOKEN, $remember);
687
			return true;
688
		} catch (SessionNotAvailableException $ex) {
689
			// This can happen with OCC, where a memory session is used
690
			// if a memory session is used, we shouldn't create a session token anyway
691
			return false;
692
		}
693
	}
694
695
	/**
696
	 * Checks if the given password is a token.
697
	 * If yes, the password is extracted from the token.
698
	 * If no, the same password is returned.
699
	 *
700
	 * @param string $password either the login password or a device token
701
	 * @return string|null the password or null if none was set in the token
702
	 */
703
	private function getPassword($password) {
704
		if (is_null($password)) {
705
			// This is surely no token ;-)
706
			return null;
707
		}
708
		try {
709
			$token = $this->tokenProvider->getToken($password);
710
			try {
711
				return $this->tokenProvider->getPassword($token, $password);
712
			} catch (PasswordlessTokenException $ex) {
713
				return null;
714
			}
715
		} catch (InvalidTokenException $ex) {
716
			return $password;
717
		}
718
	}
719
720
	/**
721
	 * @param IToken $dbToken
722
	 * @param string $token
723
	 * @return boolean
724
	 */
725
	private function checkTokenCredentials(IToken $dbToken, $token) {
726
		// Check whether login credentials are still valid and the user was not disabled
727
		// This check is performed each 5 minutes
728
		$lastCheck = $dbToken->getLastCheck() ? : 0;
729
		$now = $this->timeFactory->getTime();
730
		if ($lastCheck > ($now - 60 * 5)) {
731
			// Checked performed recently, nothing to do now
732
			return true;
733
		}
734
735
		try {
736
			$pwd = $this->tokenProvider->getPassword($dbToken, $token);
737
		} catch (InvalidTokenException $ex) {
738
			// An invalid token password was used -> log user out
739
			return false;
740
		} catch (PasswordlessTokenException $ex) {
741
			// Token has no password
742
743
			if (!is_null($this->activeUser) && !$this->activeUser->isEnabled()) {
744
				$this->tokenProvider->invalidateToken($token);
745
				return false;
746
			}
747
748
			$dbToken->setLastCheck($now);
749
			$this->tokenProvider->updateToken($dbToken);
750
			return true;
751
		}
752
753
		// Invalidate token if the user is no longer active
754
		if (!is_null($this->activeUser) && !$this->activeUser->isEnabled()) {
755
			$this->tokenProvider->invalidateToken($token);
756
			return false;
757
		}
758
759
		// If the token password is no longer valid mark it as such
760
		if ($this->manager->checkPassword($dbToken->getLoginName(), $pwd) === false) {
761
			$this->tokenProvider->markPasswordInvalid($dbToken, $token);
762
			// User is logged out
763
			return false;
764
		}
765
766
		$dbToken->setLastCheck($now);
767
		$this->tokenProvider->updateToken($dbToken);
768
		return true;
769
	}
770
771
	/**
772
	 * Check if the given token exists and performs password/user-enabled checks
773
	 *
774
	 * Invalidates the token if checks fail
775
	 *
776
	 * @param string $token
777
	 * @param string $user login name
778
	 * @return boolean
779
	 */
780
	private function validateToken($token, $user = null) {
781
		try {
782
			$dbToken = $this->tokenProvider->getToken($token);
783
		} catch (InvalidTokenException $ex) {
784
			return false;
785
		}
786
787
		// Check if login names match
788
		if (!is_null($user) && $dbToken->getLoginName() !== $user) {
789
			// TODO: this makes it impossible to use different login names on browser and client
790
			// e.g. login by e-mail '[email protected]' on browser for generating the token will not
791
			//      allow to use the client token with the login name 'user'.
792
			$this->logger->error('App token login name does not match', [
793
				'tokenLoginName' => $dbToken->getLoginName(),
794
				'sessionLoginName' => $user,
795
			]);
796
797
			return false;
798
		}
799
800
		if (!$this->checkTokenCredentials($dbToken, $token)) {
801
			return false;
802
		}
803
804
		// Update token scope
805
		$this->lockdownManager->setToken($dbToken);
806
807
		$this->tokenProvider->updateTokenActivity($dbToken);
808
809
		return true;
810
	}
811
812
	/**
813
	 * Tries to login the user with auth token header
814
	 *
815
	 * @param IRequest $request
816
	 * @todo check remember me cookie
817
	 * @return boolean
818
	 */
819
	public function tryTokenLogin(IRequest $request) {
820
		$authHeader = $request->getHeader('Authorization');
821
		if (str_starts_with($authHeader, 'Bearer ')) {
822
			$token = substr($authHeader, 7);
823
		} else {
824
			// No auth header, let's try session id
825
			try {
826
				$token = $this->session->getId();
827
			} catch (SessionNotAvailableException $ex) {
828
				return false;
829
			}
830
		}
831
832
		if (!$this->loginWithToken($token)) {
833
			return false;
834
		}
835
		if (!$this->validateToken($token)) {
836
			return false;
837
		}
838
839
		try {
840
			$dbToken = $this->tokenProvider->getToken($token);
841
		} catch (InvalidTokenException $e) {
842
			// Can't really happen but better save than sorry
843
			return true;
844
		}
845
846
		// Remember me tokens are not app_passwords
847
		if ($dbToken->getRemember() === IToken::DO_NOT_REMEMBER) {
848
			// Set the session variable so we know this is an app password
849
			$this->session->set('app_password', $token);
850
		}
851
852
		return true;
853
	}
854
855
	/**
856
	 * perform login using the magic cookie (remember login)
857
	 *
858
	 * @param string $uid the username
859
	 * @param string $currentToken
860
	 * @param string $oldSessionId
861
	 * @return bool
862
	 */
863
	public function loginWithCookie($uid, $currentToken, $oldSessionId) {
864
		$this->session->regenerateId();
865
		$this->manager->emit('\OC\User', 'preRememberedLogin', [$uid]);
866
		$user = $this->manager->get($uid);
867
		if (is_null($user)) {
868
			// user does not exist
869
			return false;
870
		}
871
872
		// get stored tokens
873
		$tokens = $this->config->getUserKeys($uid, 'login_token');
874
		// test cookies token against stored tokens
875
		if (!in_array($currentToken, $tokens, true)) {
876
			$this->logger->info('Tried to log in {uid} but could not verify token', [
877
				'app' => 'core',
878
				'uid' => $uid,
879
			]);
880
			return false;
881
		}
882
		// replace successfully used token with a new one
883
		$this->config->deleteUserValue($uid, 'login_token', $currentToken);
884
		$newToken = $this->random->generate(32);
885
		$this->config->setUserValue($uid, 'login_token', $newToken, (string)$this->timeFactory->getTime());
886
887
		try {
888
			$sessionId = $this->session->getId();
889
			$token = $this->tokenProvider->renewSessionToken($oldSessionId, $sessionId);
890
		} catch (SessionNotAvailableException $ex) {
891
			$this->logger->warning('Could not renew session token for {uid} because the session is unavailable', [
892
				'app' => 'core',
893
				'uid' => $uid,
894
			]);
895
			return false;
896
		} catch (InvalidTokenException $ex) {
897
			$this->logger->warning('Renewing session token failed', ['app' => 'core']);
898
			return false;
899
		}
900
901
		$this->setMagicInCookie($user->getUID(), $newToken);
902
903
		//login
904
		$this->setUser($user);
905
		$this->setLoginName($token->getLoginName());
906
		$this->setToken($token->getId());
907
		$this->lockdownManager->setToken($token);
908
		$user->updateLastLoginTimestamp();
909
		$password = null;
910
		try {
911
			$password = $this->tokenProvider->getPassword($token, $sessionId);
912
		} catch (PasswordlessTokenException $ex) {
913
			// Ignore
914
		}
915
		$this->manager->emit('\OC\User', 'postRememberedLogin', [$user, $password]);
916
		return true;
917
	}
918
919
	/**
920
	 * @param IUser $user
921
	 */
922
	public function createRememberMeToken(IUser $user) {
923
		$token = $this->random->generate(32);
924
		$this->config->setUserValue($user->getUID(), 'login_token', $token, (string)$this->timeFactory->getTime());
925
		$this->setMagicInCookie($user->getUID(), $token);
926
	}
927
928
	/**
929
	 * logout the user from the session
930
	 */
931
	public function logout() {
932
		$user = $this->getUser();
933
		$this->manager->emit('\OC\User', 'logout', [$user]);
934
		if ($user !== null) {
935
			try {
936
				$this->tokenProvider->invalidateToken($this->session->getId());
937
			} catch (SessionNotAvailableException $ex) {
938
			}
939
		}
940
		$this->setUser(null);
941
		$this->setLoginName(null);
942
		$this->setToken(null);
943
		$this->unsetMagicInCookie();
944
		$this->session->clear();
945
		$this->manager->emit('\OC\User', 'postLogout', [$user]);
946
	}
947
948
	/**
949
	 * Set cookie value to use in next page load
950
	 *
951
	 * @param string $username username to be set
952
	 * @param string $token
953
	 */
954
	public function setMagicInCookie($username, $token) {
955
		$secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https';
956
		$webRoot = \OC::$WEBROOT;
957
		if ($webRoot === '') {
958
			$webRoot = '/';
959
		}
960
961
		$maxAge = $this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
962
		\OC\Http\CookieHelper::setCookie(
963
			'nc_username',
964
			$username,
965
			$maxAge,
966
			$webRoot,
967
			'',
968
			$secureCookie,
969
			true,
970
			\OC\Http\CookieHelper::SAMESITE_LAX
971
		);
972
		\OC\Http\CookieHelper::setCookie(
973
			'nc_token',
974
			$token,
975
			$maxAge,
976
			$webRoot,
977
			'',
978
			$secureCookie,
979
			true,
980
			\OC\Http\CookieHelper::SAMESITE_LAX
981
		);
982
		try {
983
			\OC\Http\CookieHelper::setCookie(
984
				'nc_session_id',
985
				$this->session->getId(),
986
				$maxAge,
987
				$webRoot,
988
				'',
989
				$secureCookie,
990
				true,
991
				\OC\Http\CookieHelper::SAMESITE_LAX
992
			);
993
		} catch (SessionNotAvailableException $ex) {
994
			// ignore
995
		}
996
	}
997
998
	/**
999
	 * Remove cookie for "remember username"
1000
	 */
1001
	public function unsetMagicInCookie() {
1002
		//TODO: DI for cookies and IRequest
1003
		$secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https';
1004
1005
		unset($_COOKIE['nc_username']); //TODO: DI
1006
		unset($_COOKIE['nc_token']);
1007
		unset($_COOKIE['nc_session_id']);
1008
		setcookie('nc_username', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true);
1009
		setcookie('nc_token', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true);
1010
		setcookie('nc_session_id', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true);
1011
		// old cookies might be stored under /webroot/ instead of /webroot
1012
		// and Firefox doesn't like it!
1013
		setcookie('nc_username', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
1014
		setcookie('nc_token', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
1015
		setcookie('nc_session_id', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
1016
	}
1017
1018
	/**
1019
	 * Update password of the browser session token if there is one
1020
	 *
1021
	 * @param string $password
1022
	 */
1023
	public function updateSessionTokenPassword($password) {
1024
		try {
1025
			$sessionId = $this->session->getId();
1026
			$token = $this->tokenProvider->getToken($sessionId);
1027
			$this->tokenProvider->setPassword($token, $sessionId, $password);
1028
		} catch (SessionNotAvailableException $ex) {
1029
			// Nothing to do
1030
		} catch (InvalidTokenException $ex) {
1031
			// Nothing to do
1032
		}
1033
	}
1034
1035
	public function updateTokens(string $uid, string $password) {
1036
		$this->tokenProvider->updatePasswords($uid, $password);
1037
	}
1038
}
1039