Passed
Push — master ( 7149ed...962901 )
by John
13:20 queued 11s
created

Session::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 9
c 1
b 0
f 0
nc 1
nop 9
dl 0
loc 18
rs 9.9666

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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 Joas Schilling <[email protected]>
12
 * @author Jörn Friedrich Dreyer <[email protected]>
13
 * @author Lukas Reschke <[email protected]>
14
 * @author Morris Jobke <[email protected]>
15
 * @author Robin Appelman <[email protected]>
16
 * @author Robin McCorkell <[email protected]>
17
 * @author Roeland Jago Douma <[email protected]>
18
 * @author Sandro Lutz <[email protected]>
19
 * @author Thomas Müller <[email protected]>
20
 * @author Vincent Petry <[email protected]>
21
 *
22
 * @license AGPL-3.0
23
 *
24
 * This code is free software: you can redistribute it and/or modify
25
 * it under the terms of the GNU Affero General Public License, version 3,
26
 * as published by the Free Software Foundation.
27
 *
28
 * This program is distributed in the hope that it will be useful,
29
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
30
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31
 * GNU Affero General Public License for more details.
32
 *
33
 * You should have received a copy of the GNU Affero General Public License, version 3,
34
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
35
 *
36
 */
37
38
namespace OC\User;
39
40
use OC;
41
use OC\Authentication\Exceptions\ExpiredTokenException;
42
use OC\Authentication\Exceptions\InvalidTokenException;
43
use OC\Authentication\Exceptions\PasswordlessTokenException;
44
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
45
use OC\Authentication\Token\IProvider;
46
use OC\Authentication\Token\IToken;
47
use OC\Hooks\Emitter;
48
use OC\Hooks\PublicEmitter;
49
use OC_User;
50
use OC_Util;
51
use OCA\DAV\Connector\Sabre\Auth;
52
use OCP\AppFramework\Utility\ITimeFactory;
53
use OCP\EventDispatcher\IEventDispatcher;
54
use OCP\Files\NotPermittedException;
55
use OCP\IConfig;
56
use OCP\ILogger;
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\ISecureRandom;
63
use OCP\Session\Exceptions\SessionNotAvailableException;
64
use OCP\Util;
65
use Symfony\Component\EventDispatcher\GenericEvent;
66
67
/**
68
 * Class Session
69
 *
70
 * Hooks available in scope \OC\User:
71
 * - preSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
72
 * - postSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
73
 * - preDelete(\OC\User\User $user)
74
 * - postDelete(\OC\User\User $user)
75
 * - preCreateUser(string $uid, string $password)
76
 * - postCreateUser(\OC\User\User $user)
77
 * - assignedUserId(string $uid)
78
 * - preUnassignedUserId(string $uid)
79
 * - postUnassignedUserId(string $uid)
80
 * - preLogin(string $user, string $password)
81
 * - postLogin(\OC\User\User $user, string $password)
82
 * - preRememberedLogin(string $uid)
83
 * - postRememberedLogin(\OC\User\User $user)
84
 * - logout()
85
 * - postLogout()
86
 *
87
 * @package OC\User
88
 */
89
class Session implements IUserSession, Emitter {
90
91
	/** @var Manager|PublicEmitter $manager */
92
	private $manager;
93
94
	/** @var ISession $session */
95
	private $session;
96
97
	/** @var ITimeFactory */
98
	private $timeFactory;
99
100
	/** @var IProvider */
101
	private $tokenProvider;
102
103
	/** @var IConfig */
104
	private $config;
105
106
	/** @var User $activeUser */
107
	protected $activeUser;
108
109
	/** @var ISecureRandom */
110
	private $random;
111
112
	/** @var ILockdownManager  */
113
	private $lockdownManager;
114
115
	/** @var ILogger */
116
	private $logger;
117
	/** @var IEventDispatcher */
118
	private $dispatcher;
119
120
	/**
121
	 * @param Manager $manager
122
	 * @param ISession $session
123
	 * @param ITimeFactory $timeFactory
124
	 * @param IProvider $tokenProvider
125
	 * @param IConfig $config
126
	 * @param ISecureRandom $random
127
	 * @param ILockdownManager $lockdownManager
128
	 * @param ILogger $logger
129
	 */
130
	public function __construct(Manager $manager,
131
								ISession $session,
132
								ITimeFactory $timeFactory,
133
								$tokenProvider,
134
								IConfig $config,
135
								ISecureRandom $random,
136
								ILockdownManager $lockdownManager,
137
								ILogger $logger,
138
								IEventDispatcher $dispatcher) {
139
		$this->manager = $manager;
140
		$this->session = $session;
141
		$this->timeFactory = $timeFactory;
142
		$this->tokenProvider = $tokenProvider;
143
		$this->config = $config;
144
		$this->random = $random;
145
		$this->lockdownManager = $lockdownManager;
146
		$this->logger = $logger;
147
		$this->dispatcher = $dispatcher;
148
	}
149
150
	/**
151
	 * @param IProvider $provider
152
	 */
153
	public function setTokenProvider(IProvider $provider) {
154
		$this->tokenProvider = $provider;
155
	}
156
157
	/**
158
	 * @param string $scope
159
	 * @param string $method
160
	 * @param callable $callback
161
	 */
162
	public function listen($scope, $method, callable $callback) {
163
		$this->manager->listen($scope, $method, $callback);
164
	}
165
166
	/**
167
	 * @param string $scope optional
168
	 * @param string $method optional
169
	 * @param callable $callback optional
170
	 */
171
	public function removeListener($scope = null, $method = null, callable $callback = null) {
172
		$this->manager->removeListener($scope, $method, $callback);
173
	}
174
175
	/**
176
	 * get the manager object
177
	 *
178
	 * @return Manager|PublicEmitter
179
	 */
180
	public function getManager() {
181
		return $this->manager;
182
	}
183
184
	/**
185
	 * get the session object
186
	 *
187
	 * @return ISession
188
	 */
189
	public function getSession() {
190
		return $this->session;
191
	}
192
193
	/**
194
	 * set the session object
195
	 *
196
	 * @param ISession $session
197
	 */
198
	public function setSession(ISession $session) {
199
		if ($this->session instanceof ISession) {
0 ignored issues
show
introduced by
$this->session is always a sub-type of OCP\ISession.
Loading history...
200
			$this->session->close();
201
		}
202
		$this->session = $session;
203
		$this->activeUser = null;
204
	}
205
206
	/**
207
	 * set the currently active user
208
	 *
209
	 * @param IUser|null $user
210
	 */
211
	public function setUser($user) {
212
		if (is_null($user)) {
213
			$this->session->remove('user_id');
214
		} else {
215
			$this->session->set('user_id', $user->getUID());
216
		}
217
		$this->activeUser = $user;
218
	}
219
220
	/**
221
	 * get the current active user
222
	 *
223
	 * @return IUser|null Current user, otherwise null
224
	 */
225
	public function getUser() {
226
		// FIXME: This is a quick'n dirty work-around for the incognito mode as
227
		// described at https://github.com/owncloud/core/pull/12912#issuecomment-67391155
228
		if (OC_User::isIncognitoMode()) {
229
			return null;
230
		}
231
		if (is_null($this->activeUser)) {
232
			$uid = $this->session->get('user_id');
233
			if (is_null($uid)) {
234
				return null;
235
			}
236
			$this->activeUser = $this->manager->get($uid);
0 ignored issues
show
Bug introduced by
The method get() does not exist on OC\Hooks\PublicEmitter. It seems like you code against a sub-type of OC\Hooks\PublicEmitter such as OC\User\Manager or OC\Group\Manager. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

236
			/** @scrutinizer ignore-call */ 
237
   $this->activeUser = $this->manager->get($uid);
Loading history...
237
			if (is_null($this->activeUser)) {
238
				return null;
239
			}
240
			$this->validateSession();
241
		}
242
		return $this->activeUser;
243
	}
244
245
	/**
246
	 * Validate whether the current session is valid
247
	 *
248
	 * - For token-authenticated clients, the token validity is checked
249
	 * - For browsers, the session token validity is checked
250
	 */
251
	protected function validateSession() {
252
		$token = null;
253
		$appPassword = $this->session->get('app_password');
254
255
		if (is_null($appPassword)) {
256
			try {
257
				$token = $this->session->getId();
258
			} catch (SessionNotAvailableException $ex) {
259
				return;
260
			}
261
		} else {
262
			$token = $appPassword;
263
		}
264
265
		if (!$this->validateToken($token)) {
266
			// Session was invalidated
267
			$this->logout();
268
		}
269
	}
270
271
	/**
272
	 * Checks whether the user is logged in
273
	 *
274
	 * @return bool if logged in
275
	 */
276
	public function isLoggedIn() {
277
		$user = $this->getUser();
278
		if (is_null($user)) {
279
			return false;
280
		}
281
282
		return $user->isEnabled();
283
	}
284
285
	/**
286
	 * set the login name
287
	 *
288
	 * @param string|null $loginName for the logged in user
289
	 */
290
	public function setLoginName($loginName) {
291
		if (is_null($loginName)) {
292
			$this->session->remove('loginname');
293
		} else {
294
			$this->session->set('loginname', $loginName);
295
		}
296
	}
297
298
	/**
299
	 * get the login name of the current user
300
	 *
301
	 * @return string
302
	 */
303
	public function getLoginName() {
304
		if ($this->activeUser) {
305
			return $this->session->get('loginname');
306
		}
307
308
		$uid = $this->session->get('user_id');
309
		if ($uid) {
310
			$this->activeUser = $this->manager->get($uid);
311
			return $this->session->get('loginname');
312
		}
313
314
		return null;
315
	}
316
317
	/**
318
	 * @return mixed
319
	 */
320
	public function getImpersonatingUserID(): ?string {
321
322
		return $this->session->get('oldUserId');
323
324
	}
325
326
	public function setImpersonatingUserID(bool $useCurrentUser = true): void {
327
		if ($useCurrentUser === false) {
328
			$this->session->remove('oldUserId');
329
			return;
330
		}
331
332
		$currentUser = $this->getUser();
333
334
		if ($currentUser === null) {
335
			throw new \OC\User\NoUserException();
336
		}
337
		$this->session->set('oldUserId', $currentUser->getUID());
338
339
	}
340
	/**
341
	 * set the token id
342
	 *
343
	 * @param int|null $token that was used to log in
344
	 */
345
	protected function setToken($token) {
346
		if ($token === null) {
347
			$this->session->remove('token-id');
348
		} else {
349
			$this->session->set('token-id', $token);
350
		}
351
	}
352
353
	/**
354
	 * try to log in with the provided credentials
355
	 *
356
	 * @param string $uid
357
	 * @param string $password
358
	 * @return boolean|null
359
	 * @throws LoginException
360
	 */
361
	public function login($uid, $password) {
362
		$this->session->regenerateId();
363
		if ($this->validateToken($password, $uid)) {
364
			return $this->loginWithToken($password);
365
		}
366
		return $this->loginWithPassword($uid, $password);
367
	}
368
369
	/**
370
	 * @param IUser $user
371
	 * @param array $loginDetails
372
	 * @param bool $regenerateSessionId
373
	 * @return true returns true if login successful or an exception otherwise
374
	 * @throws LoginException
375
	 */
376
	public function completeLogin(IUser $user, array $loginDetails, $regenerateSessionId = true) {
377
		if (!$user->isEnabled()) {
378
			// disabled users can not log in
379
			// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
380
			$message = \OC::$server->getL10N('lib')->t('User disabled');
381
			throw new LoginException($message);
382
		}
383
384
		if($regenerateSessionId) {
385
			$this->session->regenerateId();
386
		}
387
388
		$this->setUser($user);
389
		$this->setLoginName($loginDetails['loginName']);
390
391
		$isToken = isset($loginDetails['token']) && $loginDetails['token'] instanceof IToken;
392
		if ($isToken) {
393
			$this->setToken($loginDetails['token']->getId());
394
			$this->lockdownManager->setToken($loginDetails['token']);
395
			$firstTimeLogin = false;
396
		} else {
397
			$this->setToken(null);
398
			$firstTimeLogin = $user->updateLastLoginTimestamp();
399
		}
400
401
		$postLoginEvent = new OC\User\Events\PostLoginEvent(
402
			$user,
403
			$loginDetails['password'],
404
			$isToken
405
		);
406
		$this->dispatcher->dispatch(OC\User\Events\PostLoginEvent::class, $postLoginEvent);
407
408
		$this->manager->emit('\OC\User', 'postLogin', [
409
			$user,
410
			$loginDetails['password'],
411
			$isToken,
412
		]);
413
		if($this->isLoggedIn()) {
414
			$this->prepareUserLogin($firstTimeLogin, $regenerateSessionId);
415
			return true;
416
		}
417
418
		$message = \OC::$server->getL10N('lib')->t('Login canceled by app');
419
		throw new LoginException($message);
420
	}
421
422
	/**
423
	 * Tries to log in a client
424
	 *
425
	 * Checks token auth enforced
426
	 * Checks 2FA enabled
427
	 *
428
	 * @param string $user
429
	 * @param string $password
430
	 * @param IRequest $request
431
	 * @param OC\Security\Bruteforce\Throttler $throttler
432
	 * @throws LoginException
433
	 * @throws PasswordLoginForbiddenException
434
	 * @return boolean
435
	 */
436
	public function logClientIn($user,
437
								$password,
438
								IRequest $request,
439
								OC\Security\Bruteforce\Throttler $throttler) {
440
		$currentDelay = $throttler->sleepDelay($request->getRemoteAddress(), 'login');
441
442
		if ($this->manager instanceof PublicEmitter) {
0 ignored issues
show
introduced by
$this->manager is always a sub-type of OC\Hooks\PublicEmitter.
Loading history...
443
			$this->manager->emit('\OC\User', 'preLogin', array($user, $password));
444
		}
445
446
		try {
447
			$isTokenPassword = $this->isTokenPassword($password);
448
		} catch (ExpiredTokenException $e) {
449
			// Just return on an expired token no need to check further or record a failed login
450
			return false;
451
		}
452
453
		if (!$isTokenPassword && $this->isTokenAuthEnforced()) {
454
			throw new PasswordLoginForbiddenException();
455
		}
456
		if (!$isTokenPassword && $this->isTwoFactorEnforced($user)) {
457
			throw new PasswordLoginForbiddenException();
458
		}
459
460
		// Try to login with this username and password
461
		if (!$this->login($user, $password) ) {
462
463
			// Failed, maybe the user used their email address
464
			$users = $this->manager->getByEmail($user);
0 ignored issues
show
Bug introduced by
The method getByEmail() does not exist on OC\Hooks\PublicEmitter. It seems like you code against a sub-type of OC\Hooks\PublicEmitter such as OC\User\Manager. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

464
			/** @scrutinizer ignore-call */ 
465
   $users = $this->manager->getByEmail($user);
Loading history...
465
			if (!(\count($users) === 1 && $this->login($users[0]->getUID(), $password))) {
466
467
				$this->logger->warning('Login failed: \'' . $user . '\' (Remote IP: \'' . \OC::$server->getRequest()->getRemoteAddress() . '\')', ['app' => 'core']);
468
469
				$throttler->registerAttempt('login', $request->getRemoteAddress(), ['user' => $user]);
470
				if ($currentDelay === 0) {
471
					$throttler->sleepDelay($request->getRemoteAddress(), 'login');
472
				}
473
				return false;
474
			}
475
		}
476
477
		if ($isTokenPassword) {
478
			$this->session->set('app_password', $password);
479
		} else if($this->supportsCookies($request)) {
480
			// Password login, but cookies supported -> create (browser) session token
481
			$this->createSessionToken($request, $this->getUser()->getUID(), $user, $password);
482
		}
483
484
		return true;
485
	}
486
487
	protected function supportsCookies(IRequest $request) {
488
		if (!is_null($request->getCookie('cookie_test'))) {
489
			return true;
490
		}
491
		setcookie('cookie_test', 'test', $this->timeFactory->getTime() + 3600);
492
		return false;
493
	}
494
495
	private function isTokenAuthEnforced() {
496
		return $this->config->getSystemValue('token_auth_enforced', false);
497
	}
498
499
	protected function isTwoFactorEnforced($username) {
500
		Util::emitHook(
501
			'\OCA\Files_Sharing\API\Server2Server',
502
			'preLoginNameUsedAsUserName',
503
			array('uid' => &$username)
504
		);
505
		$user = $this->manager->get($username);
506
		if (is_null($user)) {
507
			$users = $this->manager->getByEmail($username);
508
			if (empty($users)) {
509
				return false;
510
			}
511
			if (count($users) !== 1) {
512
				return true;
513
			}
514
			$user = $users[0];
515
		}
516
		// DI not possible due to cyclic dependencies :'-/
517
		return OC::$server->getTwoFactorAuthManager()->isTwoFactorAuthenticated($user);
518
	}
519
520
	/**
521
	 * Check if the given 'password' is actually a device token
522
	 *
523
	 * @param string $password
524
	 * @return boolean
525
	 * @throws ExpiredTokenException
526
	 */
527
	public function isTokenPassword($password) {
528
		try {
529
			$this->tokenProvider->getToken($password);
530
			return true;
531
		} catch (ExpiredTokenException $e) {
532
			throw $e;
533
		} catch (InvalidTokenException $ex) {
534
			return false;
535
		}
536
	}
537
538
	protected function prepareUserLogin($firstTimeLogin, $refreshCsrfToken = true) {
539
		if ($refreshCsrfToken) {
540
			// TODO: mock/inject/use non-static
541
			// Refresh the token
542
			\OC::$server->getCsrfTokenManager()->refreshToken();
543
		}
544
545
		//we need to pass the user name, which may differ from login name
546
		$user = $this->getUser()->getUID();
547
		OC_Util::setupFS($user);
548
549
		if ($firstTimeLogin) {
550
			// TODO: lock necessary?
551
			//trigger creation of user home and /files folder
552
			$userFolder = \OC::$server->getUserFolder($user);
553
554
			try {
555
				// copy skeleton
556
				\OC_Util::copySkeleton($user, $userFolder);
557
			} catch (NotPermittedException $ex) {
558
				// read only uses
559
			}
560
561
			// trigger any other initialization
562
			\OC::$server->getEventDispatcher()->dispatch(IUser::class . '::firstLogin', new GenericEvent($this->getUser()));
0 ignored issues
show
Bug introduced by
OCP\IUser::class . '::firstLogin' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

562
			\OC::$server->getEventDispatcher()->dispatch(/** @scrutinizer ignore-type */ IUser::class . '::firstLogin', new GenericEvent($this->getUser()));
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...Event($this->getUser()). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

562
			\OC::$server->getEventDispatcher()->/** @scrutinizer ignore-call */ dispatch(IUser::class . '::firstLogin', new GenericEvent($this->getUser()));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
563
		}
564
	}
565
566
	/**
567
	 * Tries to login the user with HTTP Basic Authentication
568
	 *
569
	 * @todo do not allow basic auth if the user is 2FA enforced
570
	 * @param IRequest $request
571
	 * @param OC\Security\Bruteforce\Throttler $throttler
572
	 * @return boolean if the login was successful
573
	 */
574
	public function tryBasicAuthLogin(IRequest $request,
575
									  OC\Security\Bruteforce\Throttler $throttler) {
576
		if (!empty($request->server['PHP_AUTH_USER']) && !empty($request->server['PHP_AUTH_PW'])) {
577
			try {
578
				if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request, $throttler)) {
579
					/**
580
					 * Add DAV authenticated. This should in an ideal world not be
581
					 * necessary but the iOS App reads cookies from anywhere instead
582
					 * only the DAV endpoint.
583
					 * This makes sure that the cookies will be valid for the whole scope
584
					 * @see https://github.com/owncloud/core/issues/22893
585
					 */
586
					$this->session->set(
587
						Auth::DAV_AUTHENTICATED, $this->getUser()->getUID()
588
					);
589
590
					// Set the last-password-confirm session to make the sudo mode work
591
					 $this->session->set('last-password-confirm', $this->timeFactory->getTime());
592
593
					return true;
594
				}
595
			} catch (PasswordLoginForbiddenException $ex) {
596
				// Nothing to do
597
			}
598
		}
599
		return false;
600
	}
601
602
	/**
603
	 * Log an user in via login name and password
604
	 *
605
	 * @param string $uid
606
	 * @param string $password
607
	 * @return boolean
608
	 * @throws LoginException if an app canceld the login process or the user is not enabled
609
	 */
610
	private function loginWithPassword($uid, $password) {
611
		$user = $this->manager->checkPasswordNoLogging($uid, $password);
0 ignored issues
show
Bug introduced by
The method checkPasswordNoLogging() does not exist on OC\Hooks\PublicEmitter. It seems like you code against a sub-type of OC\Hooks\PublicEmitter such as OC\User\Manager. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

611
		/** @scrutinizer ignore-call */ 
612
  $user = $this->manager->checkPasswordNoLogging($uid, $password);
Loading history...
612
		if ($user === false) {
0 ignored issues
show
introduced by
The condition $user === false is always false.
Loading history...
613
			// Password check failed
614
			return false;
615
		}
616
617
		return $this->completeLogin($user, ['loginName' => $uid, 'password' => $password], false);
618
	}
619
620
	/**
621
	 * Log an user in with a given token (id)
622
	 *
623
	 * @param string $token
624
	 * @return boolean
625
	 * @throws LoginException if an app canceled the login process or the user is not enabled
626
	 */
627
	private function loginWithToken($token) {
628
		try {
629
			$dbToken = $this->tokenProvider->getToken($token);
630
		} catch (InvalidTokenException $ex) {
631
			return false;
632
		}
633
		$uid = $dbToken->getUID();
634
635
		// When logging in with token, the password must be decrypted first before passing to login hook
636
		$password = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $password is dead and can be removed.
Loading history...
637
		try {
638
			$password = $this->tokenProvider->getPassword($dbToken, $token);
639
		} catch (PasswordlessTokenException $ex) {
640
			// Ignore and use empty string instead
641
		}
642
643
		$this->manager->emit('\OC\User', 'preLogin', array($uid, $password));
644
645
		$user = $this->manager->get($uid);
646
		if (is_null($user)) {
647
			// user does not exist
648
			return false;
649
		}
650
651
		return $this->completeLogin(
652
			$user,
653
			[
654
				'loginName' => $dbToken->getLoginName(),
655
				'password' => $password,
656
				'token' => $dbToken
657
			],
658
			false);
659
	}
660
661
	/**
662
	 * Create a new session token for the given user credentials
663
	 *
664
	 * @param IRequest $request
665
	 * @param string $uid user UID
666
	 * @param string $loginName login name
667
	 * @param string $password
668
	 * @param int $remember
669
	 * @return boolean
670
	 */
671
	public function createSessionToken(IRequest $request, $uid, $loginName, $password = null, $remember = IToken::DO_NOT_REMEMBER) {
672
		if (is_null($this->manager->get($uid))) {
673
			// User does not exist
674
			return false;
675
		}
676
		$name = isset($request->server['HTTP_USER_AGENT']) ? $request->server['HTTP_USER_AGENT'] : 'unknown browser';
677
		try {
678
			$sessionId = $this->session->getId();
679
			$pwd = $this->getPassword($password);
680
			// Make sure the current sessionId has no leftover tokens
681
			$this->tokenProvider->invalidateToken($sessionId);
682
			$this->tokenProvider->generateToken($sessionId, $uid, $loginName, $pwd, $name, IToken::TEMPORARY_TOKEN, $remember);
683
			return true;
684
		} catch (SessionNotAvailableException $ex) {
685
			// This can happen with OCC, where a memory session is used
686
			// if a memory session is used, we shouldn't create a session token anyway
687
			return false;
688
		}
689
	}
690
691
	/**
692
	 * Checks if the given password is a token.
693
	 * If yes, the password is extracted from the token.
694
	 * If no, the same password is returned.
695
	 *
696
	 * @param string $password either the login password or a device token
697
	 * @return string|null the password or null if none was set in the token
698
	 */
699
	private function getPassword($password) {
700
		if (is_null($password)) {
0 ignored issues
show
introduced by
The condition is_null($password) is always false.
Loading history...
701
			// This is surely no token ;-)
702
			return null;
703
		}
704
		try {
705
			$token = $this->tokenProvider->getToken($password);
706
			try {
707
				return $this->tokenProvider->getPassword($token, $password);
708
			} catch (PasswordlessTokenException $ex) {
709
				return null;
710
			}
711
		} catch (InvalidTokenException $ex) {
712
			return $password;
713
		}
714
	}
715
716
	/**
717
	 * @param IToken $dbToken
718
	 * @param string $token
719
	 * @return boolean
720
	 */
721
	private function checkTokenCredentials(IToken $dbToken, $token) {
722
		// Check whether login credentials are still valid and the user was not disabled
723
		// This check is performed each 5 minutes
724
		$lastCheck = $dbToken->getLastCheck() ? : 0;
725
		$now = $this->timeFactory->getTime();
726
		if ($lastCheck > ($now - 60 * 5)) {
727
			// Checked performed recently, nothing to do now
728
			return true;
729
		}
730
731
		try {
732
			$pwd = $this->tokenProvider->getPassword($dbToken, $token);
733
		} catch (InvalidTokenException $ex) {
734
			// An invalid token password was used -> log user out
735
			return false;
736
		} catch (PasswordlessTokenException $ex) {
737
			// Token has no password
738
739
			if (!is_null($this->activeUser) && !$this->activeUser->isEnabled()) {
740
				$this->tokenProvider->invalidateToken($token);
741
				return false;
742
			}
743
744
			$dbToken->setLastCheck($now);
745
			return true;
746
		}
747
748
		// Invalidate token if the user is no longer active
749
		if (!is_null($this->activeUser) && !$this->activeUser->isEnabled()) {
750
			$this->tokenProvider->invalidateToken($token);
751
			return false;
752
		}
753
754
		// If the token password is no longer valid mark it as such
755
		if ($this->manager->checkPassword($dbToken->getLoginName(), $pwd) === false) {
0 ignored issues
show
Bug introduced by
The method checkPassword() does not exist on OC\Hooks\PublicEmitter. It seems like you code against a sub-type of OC\Hooks\PublicEmitter such as OC\User\Manager. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

755
		if ($this->manager->/** @scrutinizer ignore-call */ checkPassword($dbToken->getLoginName(), $pwd) === false) {
Loading history...
introduced by
The condition $this->manager->checkPas...Name(), $pwd) === false is always false.
Loading history...
756
			$this->tokenProvider->markPasswordInvalid($dbToken, $token);
757
			// User is logged out
758
			return false;
759
		}
760
761
		$dbToken->setLastCheck($now);
762
		return true;
763
	}
764
765
	/**
766
	 * Check if the given token exists and performs password/user-enabled checks
767
	 *
768
	 * Invalidates the token if checks fail
769
	 *
770
	 * @param string $token
771
	 * @param string $user login name
772
	 * @return boolean
773
	 */
774
	private function validateToken($token, $user = null) {
775
		try {
776
			$dbToken = $this->tokenProvider->getToken($token);
777
		} catch (InvalidTokenException $ex) {
778
			return false;
779
		}
780
781
		// Check if login names match
782
		if (!is_null($user) && $dbToken->getLoginName() !== $user) {
783
			// TODO: this makes it imposssible to use different login names on browser and client
784
			// e.g. login by e-mail '[email protected]' on browser for generating the token will not
785
			//      allow to use the client token with the login name 'user'.
786
			return false;
787
		}
788
789
		if (!$this->checkTokenCredentials($dbToken, $token)) {
790
			return false;
791
		}
792
793
		// Update token scope
794
		$this->lockdownManager->setToken($dbToken);
795
796
		$this->tokenProvider->updateTokenActivity($dbToken);
797
798
		return true;
799
	}
800
801
	/**
802
	 * Tries to login the user with auth token header
803
	 *
804
	 * @param IRequest $request
805
	 * @todo check remember me cookie
806
	 * @return boolean
807
	 */
808
	public function tryTokenLogin(IRequest $request) {
809
		$authHeader = $request->getHeader('Authorization');
810
		if (strpos($authHeader, 'Bearer ') === false) {
811
			// No auth header, let's try session id
812
			try {
813
				$token = $this->session->getId();
814
			} catch (SessionNotAvailableException $ex) {
815
				return false;
816
			}
817
		} else {
818
			$token = substr($authHeader, 7);
819
		}
820
821
		if (!$this->loginWithToken($token)) {
822
			return false;
823
		}
824
		if(!$this->validateToken($token)) {
825
			return false;
826
		}
827
828
		// Set the session variable so we know this is an app password
829
		$this->session->set('app_password', $token);
830
831
		return true;
832
	}
833
834
	/**
835
	 * perform login using the magic cookie (remember login)
836
	 *
837
	 * @param string $uid the username
838
	 * @param string $currentToken
839
	 * @param string $oldSessionId
840
	 * @return bool
841
	 */
842
	public function loginWithCookie($uid, $currentToken, $oldSessionId) {
843
		$this->session->regenerateId();
844
		$this->manager->emit('\OC\User', 'preRememberedLogin', array($uid));
845
		$user = $this->manager->get($uid);
846
		if (is_null($user)) {
847
			// user does not exist
848
			return false;
849
		}
850
851
		// get stored tokens
852
		$tokens = $this->config->getUserKeys($uid, 'login_token');
853
		// test cookies token against stored tokens
854
		if (!in_array($currentToken, $tokens, true)) {
855
			return false;
856
		}
857
		// replace successfully used token with a new one
858
		$this->config->deleteUserValue($uid, 'login_token', $currentToken);
859
		$newToken = $this->random->generate(32);
860
		$this->config->setUserValue($uid, 'login_token', $newToken, $this->timeFactory->getTime());
861
862
		try {
863
			$sessionId = $this->session->getId();
864
			$this->tokenProvider->renewSessionToken($oldSessionId, $sessionId);
865
		} catch (SessionNotAvailableException $ex) {
866
			return false;
867
		} catch (InvalidTokenException $ex) {
868
			\OC::$server->getLogger()->warning('Renewing session token failed', ['app' => 'core']);
869
			return false;
870
		}
871
872
		$this->setMagicInCookie($user->getUID(), $newToken);
873
		$token = $this->tokenProvider->getToken($sessionId);
874
875
		//login
876
		$this->setUser($user);
877
		$this->setLoginName($token->getLoginName());
878
		$this->setToken($token->getId());
879
		$this->lockdownManager->setToken($token);
880
		$user->updateLastLoginTimestamp();
881
		$password = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $password is dead and can be removed.
Loading history...
882
		try {
883
			$password = $this->tokenProvider->getPassword($token, $sessionId);
884
		} catch (PasswordlessTokenException $ex) {
885
			// Ignore
886
		}
887
		$this->manager->emit('\OC\User', 'postRememberedLogin', [$user, $password]);
888
		return true;
889
	}
890
891
	/**
892
	 * @param IUser $user
893
	 */
894
	public function createRememberMeToken(IUser $user) {
895
		$token = $this->random->generate(32);
896
		$this->config->setUserValue($user->getUID(), 'login_token', $token, $this->timeFactory->getTime());
897
		$this->setMagicInCookie($user->getUID(), $token);
898
	}
899
900
	/**
901
	 * logout the user from the session
902
	 */
903
	public function logout() {
904
		$this->manager->emit('\OC\User', 'logout');
905
		$user = $this->getUser();
906
		if (!is_null($user)) {
907
			try {
908
				$this->tokenProvider->invalidateToken($this->session->getId());
909
			} catch (SessionNotAvailableException $ex) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
910
911
			}
912
		}
913
		$this->setUser(null);
914
		$this->setLoginName(null);
915
		$this->setToken(null);
916
		$this->unsetMagicInCookie();
917
		$this->session->clear();
918
		$this->manager->emit('\OC\User', 'postLogout');
919
	}
920
921
	/**
922
	 * Set cookie value to use in next page load
923
	 *
924
	 * @param string $username username to be set
925
	 * @param string $token
926
	 */
927
	public function setMagicInCookie($username, $token) {
928
		$secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https';
929
		$webRoot = \OC::$WEBROOT;
930
		if ($webRoot === '') {
931
			$webRoot = '/';
932
		}
933
934
		$maxAge = $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
935
		\OC\Http\CookieHelper::setCookie(
936
			'nc_username',
937
			$username,
938
			$maxAge,
939
			$webRoot,
940
			'',
941
			$secureCookie,
942
			true,
943
			\OC\Http\CookieHelper::SAMESITE_LAX
944
		);
945
		\OC\Http\CookieHelper::setCookie(
946
			'nc_token',
947
			$token,
948
			$maxAge,
949
			$webRoot,
950
			'',
951
			$secureCookie,
952
			true,
953
			\OC\Http\CookieHelper::SAMESITE_LAX
954
		);
955
		try {
956
			\OC\Http\CookieHelper::setCookie(
957
				'nc_session_id',
958
				$this->session->getId(),
959
				$maxAge,
960
				$webRoot,
961
				'',
962
				$secureCookie,
963
				true,
964
				\OC\Http\CookieHelper::SAMESITE_LAX
965
			);
966
		} catch (SessionNotAvailableException $ex) {
967
			// ignore
968
		}
969
	}
970
971
	/**
972
	 * Remove cookie for "remember username"
973
	 */
974
	public function unsetMagicInCookie() {
975
		//TODO: DI for cookies and IRequest
976
		$secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https';
977
978
		unset($_COOKIE['nc_username']); //TODO: DI
979
		unset($_COOKIE['nc_token']);
980
		unset($_COOKIE['nc_session_id']);
981
		setcookie('nc_username', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true);
982
		setcookie('nc_token', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true);
983
		setcookie('nc_session_id', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true);
984
		// old cookies might be stored under /webroot/ instead of /webroot
985
		// and Firefox doesn't like it!
986
		setcookie('nc_username', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
987
		setcookie('nc_token', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
988
		setcookie('nc_session_id', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
989
	}
990
991
	/**
992
	 * Update password of the browser session token if there is one
993
	 *
994
	 * @param string $password
995
	 */
996
	public function updateSessionTokenPassword($password) {
997
		try {
998
			$sessionId = $this->session->getId();
999
			$token = $this->tokenProvider->getToken($sessionId);
1000
			$this->tokenProvider->setPassword($token, $sessionId, $password);
1001
		} catch (SessionNotAvailableException $ex) {
1002
			// Nothing to do
1003
		} catch (InvalidTokenException $ex) {
1004
			// Nothing to do
1005
		}
1006
	}
1007
1008
	public function updateTokens(string $uid, string $password) {
1009
		$this->tokenProvider->updatePasswords($uid, $password);
1010
	}
1011
1012
1013
}
1014