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

Session::loginWithCookie()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 47
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 32
nc 8
nop 3
dl 0
loc 47
rs 8.7857
c 0
b 0
f 0
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