Completed
Push — master ( 986ae9...c12854 )
by Sujith
21:16 queued 09:58
created

Session::logout()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 18
nc 4
nop 0
dl 0
loc 27
rs 8.5806
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Arthur Schiwon <[email protected]>
4
 * @author Bernhard Posselt <[email protected]>
5
 * @author Christoph Wurst <[email protected]>
6
 * @author Felix Rupp <[email protected]>
7
 * @author Jörn Friedrich Dreyer <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Morris Jobke <[email protected]>
10
 * @author Robin Appelman <[email protected]>
11
 * @author Robin McCorkell <[email protected]>
12
 * @author Semih Serhat Karakaya <[email protected]>
13
 * @author Thomas Müller <[email protected]>
14
 * @author Vincent Petry <[email protected]>
15
 *
16
 * @copyright Copyright (c) 2017, ownCloud GmbH
17
 * @license AGPL-3.0
18
 *
19
 * This code is free software: you can redistribute it and/or modify
20
 * it under the terms of the GNU Affero General Public License, version 3,
21
 * as published by the Free Software Foundation.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 * GNU Affero General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU Affero General Public License, version 3,
29
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
30
 *
31
 */
32
33
namespace OC\User;
34
35
use Exception;
36
use OC;
37
use OC\Authentication\Exceptions\InvalidTokenException;
38
use OC\Authentication\Exceptions\PasswordlessTokenException;
39
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
40
use OC\Authentication\Token\IProvider;
41
use OC\Authentication\Token\IToken;
42
use OC\Hooks\Emitter;
43
use OC_App;
44
use OC_User;
45
use OC_Util;
46
use OCA\DAV\Connector\Sabre\Auth;
47
use OCP\App\IAppManager;
48
use OCP\AppFramework\QueryException;
49
use OCP\AppFramework\Utility\ITimeFactory;
50
use OCP\Authentication\IAuthModule;
51
use OCP\Files\NotPermittedException;
52
use OCP\IConfig;
53
use OCP\IRequest;
54
use OCP\ISession;
55
use OCP\IUser;
56
use OCP\IUserManager;
57
use OCP\IUserSession;
58
use OCP\Session\Exceptions\SessionNotAvailableException;
59
use OCP\Util;
60
use Symfony\Component\EventDispatcher\Event;
61
use Symfony\Component\EventDispatcher\GenericEvent;
62
63
/**
64
 * Class Session
65
 *
66
 * Hooks available in scope \OC\User:
67
 * - preSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
68
 * - postSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
69
 * - preDelete(\OC\User\User $user)
70
 * - postDelete(\OC\User\User $user)
71
 * - preCreateUser(string $uid, string $password)
72
 * - postCreateUser(\OC\User\User $user)
73
 * - preLogin(string $user, string $password)
74
 * - postLogin(\OC\User\User $user, string $password)
75
 * - failedLogin(string $user)
76
 * - preRememberedLogin(string $uid)
77
 * - postRememberedLogin(\OC\User\User $user)
78
 * - logout()
79
 * - postLogout()
80
 *
81
 * @package OC\User
82
 */
83
class Session implements IUserSession, Emitter {
84
85
	/** @var IUserManager $manager */
86
	private $manager;
87
88
	/** @var ISession $session */
89
	private $session;
90
91
	/** @var ITimeFactory */
92
	private $timeFactory;
93
94
	/** @var IProvider */
95
	private $tokenProvider;
96
97
	/** @var IConfig */
98
	private $config;
99
100
	/** @var User $activeUser */
101
	protected $activeUser;
102
103
	/**
104
	 * @param IUserManager $manager
105
	 * @param ISession $session
106
	 * @param ITimeFactory $timeFactory
107
	 * @param IProvider $tokenProvider
108
	 * @param IConfig $config
109
	 */
110
	public function __construct(IUserManager $manager, ISession $session, ITimeFactory $timeFactory, $tokenProvider, IConfig $config) {
111
		$this->manager = $manager;
112
		$this->session = $session;
113
		$this->timeFactory = $timeFactory;
114
		$this->tokenProvider = $tokenProvider;
115
		$this->config = $config;
116
	}
117
118
	/**
119
	 * @param IProvider $provider
120
	 */
121
	public function setTokenProvider(IProvider $provider) {
122
		$this->tokenProvider = $provider;
123
	}
124
125
	/**
126
	 * @param string $scope
127
	 * @param string $method
128
	 * @param callable $callback
129
	 */
130
	public function listen($scope, $method, callable $callback) {
131
		$this->manager->listen($scope, $method, $callback);
132
	}
133
134
	/**
135
	 * @param string $scope optional
136
	 * @param string $method optional
137
	 * @param callable $callback optional
138
	 */
139
	public function removeListener($scope = null, $method = null, callable $callback = null) {
140
		$this->manager->removeListener($scope, $method, $callback);
141
	}
142
143
	/**
144
	 * get the manager object
145
	 *
146
	 * @return Manager
147
	 */
148
	public function getManager() {
149
		return $this->manager;
150
	}
151
152
	/**
153
	 * get the session object
154
	 *
155
	 * @return ISession
156
	 */
157
	public function getSession() {
158
		return $this->session;
159
	}
160
161
	/**
162
	 * set the session object
163
	 *
164
	 * @param ISession $session
165
	 */
166
	public function setSession(ISession $session) {
167
		if ($this->session instanceof ISession) {
168
			$this->session->close();
169
		}
170
		$this->session = $session;
171
		$this->activeUser = null;
172
	}
173
174
	/**
175
	 * set the currently active user
176
	 *
177
	 * @param IUser|null $user
178
	 */
179
	public function setUser($user) {
180
		if (is_null($user)) {
181
			$this->session->remove('user_id');
182
		} else {
183
			$this->session->set('user_id', $user->getUID());
184
		}
185
		$this->activeUser = $user;
0 ignored issues
show
Documentation Bug introduced by
It seems like $user can also be of type object<OCP\IUser>. However, the property $activeUser is declared as type object<OC\User\User>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
186
	}
187
188
	/**
189
	 * get the current active user
190
	 *
191
	 * @return IUser|null Current user, otherwise null
192
	 */
193
	public function getUser() {
194
		// FIXME: This is a quick'n dirty work-around for the incognito mode as
195
		// described at https://github.com/owncloud/core/pull/12912#issuecomment-67391155
196
		if (OC_User::isIncognitoMode()) {
197
			return null;
198
		}
199
		if (is_null($this->activeUser)) {
200
			$uid = $this->session->get('user_id');
201
			if (is_null($uid)) {
202
				return null;
203
			}
204
			$this->activeUser = $this->manager->get($uid);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->manager->get($uid) can also be of type object<OCP\IUser>. However, the property $activeUser is declared as type object<OC\User\User>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
205
			if (is_null($this->activeUser)) {
206
				return null;
207
			}
208
		}
209
		return $this->activeUser;
210
	}
211
212
	/**
213
	 * Validate whether the current session is valid
214
	 *
215
	 * - For token-authenticated clients, the token validity is checked
216
	 * - For browsers, the session token validity is checked
217
	 */
218
	public function validateSession() {
219
		if (!$this->getUser()) {
220
			return;
221
		}
222
223
		$token = null;
224
		$appPassword = $this->session->get('app_password');
225
226
		if (is_null($appPassword)) {
227
			try {
228
				$token = $this->session->getId();
229
			} catch (SessionNotAvailableException $ex) {
230
				return;
231
			}
232
		} else {
233
			$token = $appPassword;
234
		}
235
236
		if (!$this->validateToken($token)) {
237
			// Session was invalidated
238
			$this->logout();
239
		}
240
	}
241
242
	/**
243
	 * Checks whether the user is logged in
244
	 *
245
	 * @return bool if logged in
246
	 */
247
	public function isLoggedIn() {
248
		$user = $this->getUser();
249
		if (is_null($user)) {
250
			return false;
251
		}
252
253
		return $user->isEnabled();
254
	}
255
256
	/**
257
	 * set the login name
258
	 *
259
	 * @param string|null $loginName for the logged in user
260
	 */
261
	public function setLoginName($loginName) {
262
		if (is_null($loginName)) {
263
			$this->session->remove('loginname');
264
		} else {
265
			$this->session->set('loginname', $loginName);
266
		}
267
	}
268
269
	/**
270
	 * get the login name of the current user
271
	 *
272
	 * @return string
273
	 */
274
	public function getLoginName() {
275
		if ($this->activeUser) {
276
			return $this->session->get('loginname');
277
		} else {
278
			$uid = $this->session->get('user_id');
279
			if ($uid) {
280
				$this->activeUser = $this->manager->get($uid);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->manager->get($uid) can also be of type object<OCP\IUser>. However, the property $activeUser is declared as type object<OC\User\User>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
281
				return $this->session->get('loginname');
282
			} else {
283
				return null;
284
			}
285
		}
286
	}
287
288
	/**
289
	 * try to log in with the provided credentials
290
	 *
291
	 * @param string $uid
292
	 * @param string $password
293
	 * @return boolean|null
294
	 * @throws LoginException
295
	 */
296
	public function login($uid, $password) {
297
		$this->session->regenerateId();
298
299
		if ($this->validateToken($password, $uid)) {
300
			return $this->loginWithToken($password);
301
		}
302
		return $this->loginWithPassword($uid, $password);
303
	}
304
305
	/**
306
	 * Tries to log in a client
307
	 *
308
	 * Checks token auth enforced
309
	 * Checks 2FA enabled
310
	 *
311
	 * @param string $user
312
	 * @param string $password
313
	 * @param IRequest $request
314
	 * @throws LoginException
315
	 * @throws PasswordLoginForbiddenException
316
	 * @return boolean
317
	 */
318
	public function logClientIn($user, $password, IRequest $request) {
319
		$isTokenPassword = $this->isTokenPassword($password);
320
		if ($user === null || trim($user) === '') {
321
			throw new \InvalidArgumentException('$user cannot be empty');
322
		}
323
		if (!$isTokenPassword && $this->isTokenAuthEnforced()) {
324
			throw new PasswordLoginForbiddenException();
325
		}
326
		if (!$isTokenPassword && $this->isTwoFactorEnforced($user)) {
327
			throw new PasswordLoginForbiddenException();
328
		}
329
		if (!$this->login($user, $password) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->login($user, $password) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
330
			$users = $this->manager->getByEmail($user);
331
			if (count($users) === 1) {
332
				return $this->login($users[0]->getUID(), $password);
333
			}
334
			return false;
335
		}
336
337
		if ($isTokenPassword) {
338
			$this->session->set('app_password', $password);
339
		} else if($this->supportsCookies($request)) {
340
			// Password login, but cookies supported -> create (browser) session token
341
			$this->createSessionToken($request, $this->getUser()->getUID(), $user, $password);
342
		}
343
344
		return true;
345
	}
346
347
	protected function supportsCookies(IRequest $request) {
348
		if (!is_null($request->getCookie('cookie_test'))) {
349
			return true;
350
		}
351
		setcookie('cookie_test', 'test', $this->timeFactory->getTime() + 3600);
352
		return false;
353
	}
354
355
	private function isTokenAuthEnforced() {
356
		return $this->config->getSystemValue('token_auth_enforced', false);
357
	}
358
359
	protected function isTwoFactorEnforced($username) {
360
		Util::emitHook(
361
			'\OCA\Files_Sharing\API\Server2Server',
362
			'preLoginNameUsedAsUserName',
363
			['uid' => &$username]
364
		);
365
		$user = $this->manager->get($username);
366
		if (is_null($user)) {
367
			$users = $this->manager->getByEmail($username);
368
			if (empty($users)) {
369
				return false;
370
			}
371
			if (count($users) !== 1) {
372
				return true;
373
			}
374
			$user = $users[0];
375
		}
376
		// DI not possible due to cyclic dependencies :'-/
377
		return OC::$server->getTwoFactorAuthManager()->isTwoFactorAuthenticated($user);
378
	}
379
380
	/**
381
	 * Check if the given 'password' is actually a device token
382
	 *
383
	 * @param string $password
384
	 * @return boolean
385
	 */
386
	public function isTokenPassword($password) {
387
		try {
388
			$this->tokenProvider->getToken($password);
389
			return true;
390
		} catch (InvalidTokenException $ex) {
391
			return false;
392
		}
393
	}
394
395
	/**
396
	 * Unintentional public
397
	 *
398
	 * @param bool $firstTimeLogin
399
	 */
400
	public function prepareUserLogin($firstTimeLogin = false) {
401
		// TODO: mock/inject/use non-static
402
		// Refresh the token
403
		\OC::$server->getCsrfTokenManager()->refreshToken();
404
		//we need to pass the user name, which may differ from login name
405
		$user = $this->getUser()->getUID();
406
		OC_Util::setupFS($user);
407
408
		if ($firstTimeLogin) {
409
			// TODO: lock necessary?
410
			//trigger creation of user home and /files folder
411
			$userFolder = \OC::$server->getUserFolder($user);
412
413
			try {
414
				// copy skeleton
415
				\OC_Util::copySkeleton($user, $userFolder);
0 ignored issues
show
Bug introduced by
It seems like $userFolder defined by \OC::$server->getUserFolder($user) on line 411 can be null; however, OC_Util::copySkeleton() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
416
			} catch (NotPermittedException $ex) {
417
				// possible if files directory is in an readonly jail
418
				\OC::$server->getLogger()->warning(
419
					'Skeleton not created due to missing write permission'
420
				);
421
			}
422
423
			// trigger any other initialization
424
			\OC::$server->getEventDispatcher()->dispatch(IUser::class . '::firstLogin', new GenericEvent($this->getUser()));
425
		}
426
	}
427
428
	/**
429
	 * Tries to login the user with HTTP Basic Authentication
430
	 *
431
	 * @todo do not allow basic auth if the user is 2FA enforced
432
	 * @param IRequest $request
433
	 * @return boolean if the login was successful
434
	 */
435
	public function tryBasicAuthLogin(IRequest $request) {
436
		if (!empty($request->server['PHP_AUTH_USER']) && !empty($request->server['PHP_AUTH_PW'])) {
0 ignored issues
show
Bug introduced by
Accessing server on the interface OCP\IRequest suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
437
			try {
438
				if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request)) {
0 ignored issues
show
Bug introduced by
Accessing server on the interface OCP\IRequest suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
439
					/**
440
					 * Add DAV authenticated. This should in an ideal world not be
441
					 * necessary but the iOS App reads cookies from anywhere instead
442
					 * only the DAV endpoint.
443
					 * This makes sure that the cookies will be valid for the whole scope
444
					 * @see https://github.com/owncloud/core/issues/22893
445
					 */
446
					$this->session->set(
447
						Auth::DAV_AUTHENTICATED, $this->getUser()->getUID()
448
					);
449
					return true;
450
				}
451
			} catch (PasswordLoginForbiddenException $ex) {
452
				// Nothing to do
453
			}
454
		}
455
		return false;
456
	}
457
458
	/**
459
	 * Log an user in via login name and password
460
	 *
461
	 * @param string $uid
462
	 * @param string $password
463
	 * @return boolean
464
	 * @throws LoginException if an app canceld the login process or the user is not enabled
465
	 */
466
	private function loginWithPassword($uid, $password) {
467
		$this->manager->emit('\OC\User', 'preLogin', [$uid, $password]);
468
		$user = $this->manager->checkPassword($uid, $password);
469
		if ($user === false) {
470
			$this->manager->emit('\OC\User', 'failedLogin', [$uid]);
471
			return false;
472
		}
473
474
		if ($user->isEnabled()) {
475
			$this->setUser($user);
476
			$this->setLoginName($uid);
477
			$firstTimeLogin = $user->updateLastLoginTimestamp();
478
			$this->manager->emit('\OC\User', 'postLogin', [$user, $password]);
479 View Code Duplication
			if ($this->isLoggedIn()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
480
				$this->prepareUserLogin($firstTimeLogin);
481
				return true;
482
			} else {
483
				// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
484
				$message = \OC::$server->getL10N('lib')->t('Login canceled by app');
485
				throw new LoginException($message);
486
			}
487
		} else {
488
			// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
489
			$message = \OC::$server->getL10N('lib')->t('User disabled');
490
			throw new LoginException($message);
491
		}
492
	}
493
494
	/**
495
	 * Log an user in with a given token (id)
496
	 *
497
	 * @param string $token
498
	 * @return boolean
499
	 * @throws LoginException if an app canceld the login process or the user is not enabled
500
	 */
501
	private function loginWithToken($token) {
502
		try {
503
			$dbToken = $this->tokenProvider->getToken($token);
504
		} catch (InvalidTokenException $ex) {
505
			return false;
506
		}
507
		$uid = $dbToken->getUID();
508
509
		// When logging in with token, the password must be decrypted first before passing to login hook
510
		$password = '';
511
		try {
512
			$password = $this->tokenProvider->getPassword($dbToken, $token);
513
		} catch (PasswordlessTokenException $ex) {
514
			// Ignore and use empty string instead
515
		}
516
517
		$this->manager->emit('\OC\User', 'preLogin', [$uid, $password]);
518
519
		$user = $this->manager->get($uid);
520
		if (is_null($user)) {
521
			// user does not exist
522
			return false;
523
		}
524 View Code Duplication
		if (!$user->isEnabled()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
525
			// disabled users can not log in
526
			// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
527
			$message = \OC::$server->getL10N('lib')->t('User disabled');
528
			throw new LoginException($message);
529
		}
530
531
		//login
532
		$this->setUser($user);
533
		$this->setLoginName($dbToken->getLoginName());
534
		$this->manager->emit('\OC\User', 'postLogin', [$user, $password]);
535
536 View Code Duplication
		if ($this->isLoggedIn()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
537
			$this->prepareUserLogin();
538
		} else {
539
			// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
540
			$message = \OC::$server->getL10N('lib')->t('Login canceled by app');
541
			throw new LoginException($message);
542
		}
543
544
		// set the app password
545
		$this->session->set('app_password', $token);
546
547
		return true;
548
	}
549
550
	/**
551
	 * Create a new session token for the given user credentials
552
	 *
553
	 * @param IRequest $request
554
	 * @param string $uid user UID
555
	 * @param string $loginName login name
556
	 * @param string $password
557
	 * @return boolean
558
	 */
559
	public function createSessionToken(IRequest $request, $uid, $loginName, $password = null) {
560
		if (is_null($this->manager->get($uid))) {
561
			// User does not exist
562
			return false;
563
		}
564
		$name = isset($request->server['HTTP_USER_AGENT']) ? $request->server['HTTP_USER_AGENT'] : 'unknown browser';
0 ignored issues
show
Bug introduced by
Accessing server on the interface OCP\IRequest suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
565
		try {
566
			$sessionId = $this->session->getId();
567
			$pwd = $this->getPassword($password);
568
			$this->tokenProvider->generateToken($sessionId, $uid, $loginName, $pwd, $name);
569
			return true;
570
		} catch (SessionNotAvailableException $ex) {
571
			// This can happen with OCC, where a memory session is used
572
			// if a memory session is used, we shouldn't create a session token anyway
573
			return false;
574
		}
575
	}
576
577
	/**
578
	 * Checks if the given password is a token.
579
	 * If yes, the password is extracted from the token.
580
	 * If no, the same password is returned.
581
	 *
582
	 * @param string $password either the login password or a device token
583
	 * @return string|null the password or null if none was set in the token
584
	 */
585
	private function getPassword($password) {
586
		if (is_null($password)) {
587
			// This is surely no token ;-)
588
			return null;
589
		}
590
		try {
591
			$token = $this->tokenProvider->getToken($password);
592
			try {
593
				return $this->tokenProvider->getPassword($token, $password);
594
			} catch (PasswordlessTokenException $ex) {
595
				return null;
596
			}
597
		} catch (InvalidTokenException $ex) {
598
			return $password;
599
		}
600
	}
601
602
	/**
603
	 * @param IToken $dbToken
604
	 * @param string $token
605
	 * @return boolean
606
	 */
607
	private function checkTokenCredentials(IToken $dbToken, $token) {
608
		// Check whether login credentials are still valid and the user was not disabled
609
		// This check is performed each 5 minutes per default
610
		// However, we try to read last_check_timeout from the appconfig table so the
611
		// administrator could change this 5 minutes timeout
612
		$lastCheck = $dbToken->getLastCheck() ? : 0;
613
		$now = $this->timeFactory->getTime();
614
		$last_check_timeout = intval($this->config->getAppValue('core', 'last_check_timeout', 5));
615
		if ($lastCheck > ($now - 60 * $last_check_timeout)) {
616
			// Checked performed recently, nothing to do now
617
			return true;
618
		}
619
620
		try {
621
			$pwd = $this->tokenProvider->getPassword($dbToken, $token);
622
		} catch (InvalidTokenException $ex) {
623
			// An invalid token password was used -> log user out
624
			return false;
625
		} catch (PasswordlessTokenException $ex) {
626
			// Token has no password
627
628
			if (!is_null($this->activeUser) && !$this->activeUser->isEnabled()) {
629
				$this->tokenProvider->invalidateToken($token);
630
				return false;
631
			}
632
633
			$dbToken->setLastCheck($now);
634
			$this->tokenProvider->updateToken($dbToken);
635
			return true;
636
		}
637
638
		if ($this->manager->checkPassword($dbToken->getLoginName(), $pwd) === false
639
			|| (!is_null($this->activeUser) && !$this->activeUser->isEnabled())) {
640
			$this->tokenProvider->invalidateToken($token);
641
			// Password has changed or user was disabled -> log user out
642
			return false;
643
		}
644
		$dbToken->setLastCheck($now);
645
		$this->tokenProvider->updateToken($dbToken);
646
		return true;
647
	}
648
649
	/**
650
	 * Check if the given token exists and performs password/user-enabled checks
651
	 *
652
	 * Invalidates the token if checks fail
653
	 *
654
	 * @param string $token
655
	 * @param string $user login name
656
	 * @return boolean
657
	 */
658
	private function validateToken($token, $user = null) {
659
		try {
660
			$dbToken = $this->tokenProvider->getToken($token);
661
		} catch (InvalidTokenException $ex) {
662
			return false;
663
		}
664
665
		// Check if login names match
666
		if (!is_null($user) && $dbToken->getLoginName() !== $user) {
667
			// TODO: this makes it imposssible to use different login names on browser and client
668
			// e.g. login by e-mail '[email protected]' on browser for generating the token will not
669
			//      allow to use the client token with the login name 'user'.
670
			return false;
671
		}
672
673
		if (!$this->checkTokenCredentials($dbToken, $token)) {
674
			return false;
675
		}
676
677
		$this->tokenProvider->updateTokenActivity($dbToken);
678
679
		return true;
680
	}
681
682
	/**
683
	 * Tries to login the user with auth token header
684
	 *
685
	 * @param IRequest $request
686
	 * @todo check remember me cookie
687
	 * @return boolean
688
	 */
689
	public function tryTokenLogin(IRequest $request) {
690
		$authHeader = $request->getHeader('Authorization');
691
		if (strpos($authHeader, 'token ') === false) {
692
			// No auth header, let's try session id
693
			try {
694
				$token = $this->session->getId();
695
			} catch (SessionNotAvailableException $ex) {
696
				return false;
697
			}
698
		} else {
699
			$token = substr($authHeader, 6);
700
		}
701
702
		if (!$this->loginWithToken($token)) {
703
			return false;
704
		}
705
		if(!$this->validateToken($token)) {
706
			return false;
707
		}
708
		return true;
709
	}
710
711
	/**
712
	 * Tries to login with an AuthModule provided by an app
713
	 *
714
	 * @param IRequest $request The request
715
	 * @return bool True if request can be authenticated, false otherwise
716
	 * @throws Exception If the auth module could not be loaded
717
	 */
718
	public function tryAuthModuleLogin(IRequest $request) {
719
		/** @var IAppManager $appManager */
720
		$appManager = OC::$server->query('AppManager');
721
		$allApps = $appManager->getInstalledApps();
722
723
		foreach ($allApps as $appId) {
724
			$info = $appManager->getAppInfo($appId);
725
726
			if (isset($info['auth-modules'])) {
727
				$authModules = $info['auth-modules'];
728
729
				foreach ($authModules as $class) {
730
					try {
731
						if (!OC_App::isAppLoaded($appId)) {
732
							OC_App::loadApp($appId);
733
						}
734
735
						/** @var IAuthModule $authModule */
736
						$authModule = OC::$server->query($class);
737
738
						if ($authModule instanceof IAuthModule) {
739
							return $this->loginUser($authModule->auth($request), $authModule->getUserPassword($request));
0 ignored issues
show
Bug introduced by
It seems like $authModule->auth($request) can be null; however, loginUser() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
740
						} else {
741
							throw new Exception("Could not load the auth module $class");
742
						}
743
					} catch (QueryException $exc) {
744
						throw new Exception("Could not load the auth module $class");
745
					}
746
				}
747
			}
748
		}
749
750
		return false;
751
	}
752
753
	/**
754
	 * Log an user in
755
	 *
756
	 * @param IUser $user The user
757
	 * @param String $password The user's password
758
	 * @return boolean True if the user can be authenticated, false otherwise
759
	 * @throws LoginException if an app canceld the login process or the user is not enabled
760
	 */
761
	private function loginUser($user, $password) {
762
		if (is_null($user)) {
763
			return false;
764
		}
765
766
		$this->manager->emit('\OC\User', 'preLogin', [$user, $password]);
767
768 View Code Duplication
		if (!$user->isEnabled()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
769
			$message = \OC::$server->getL10N('lib')->t('User disabled');
770
			throw new LoginException($message);
771
		}
772
773
		$this->setUser($user);
774
		$this->setLoginName($user->getDisplayName());
775
776
		$this->manager->emit('\OC\User', 'postLogin', [$user, $password]);
777
778 View Code Duplication
		if ($this->isLoggedIn()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
779
			$this->prepareUserLogin(false);
780
		} else {
781
			$message = \OC::$server->getL10N('lib')->t('Login canceled by app');
782
			throw new LoginException($message);
783
		}
784
785
		return true;
786
	}
787
788
	/**
789
	 * perform login using the magic cookie (remember login)
790
	 *
791
	 * @param string $uid the username
792
	 * @param string $currentToken
793
	 * @return bool
794
	 */
795
	public function loginWithCookie($uid, $currentToken) {
796
		$this->session->regenerateId();
797
		$this->manager->emit('\OC\User', 'preRememberedLogin', [$uid]);
798
		$user = $this->manager->get($uid);
799
		if (is_null($user)) {
800
			// user does not exist
801
			return false;
802
		}
803
804
		// get stored tokens
805
		$tokens = OC::$server->getConfig()->getUserKeys($uid, 'login_token');
806
		// test cookies token against stored tokens
807
		if (!in_array($currentToken, $tokens, true)) {
808
			return false;
809
		}
810
		// replace successfully used token with a new one
811
		OC::$server->getConfig()->deleteUserValue($uid, 'login_token', $currentToken);
812
		$newToken = OC::$server->getSecureRandom()->generate(32);
813
		OC::$server->getConfig()->setUserValue($uid, 'login_token', $newToken, time());
814
		$this->setMagicInCookie($user->getUID(), $newToken);
815
816
		//login
817
		$this->setUser($user);
818
		$user->updateLastLoginTimestamp();
819
		$this->manager->emit('\OC\User', 'postRememberedLogin', [$user]);
820
		return true;
821
	}
822
823
	/**
824
	 * logout the user from the session
825
	 *
826
	 * @return bool
827
	 */
828
	public function logout() {
829
830
		$event = new GenericEvent(null, ['cancel' => false]);
831
		$eventDispatcher = \OC::$server->getEventDispatcher();
832
		$eventDispatcher->dispatch('\OC\User\Session::pre_logout', $event);
833
834
		$this->manager->emit('\OC\User', 'preLogout');
835
836
		if ($event['cancel'] === true) {
837
			return true;
838
		}
839
840
		$this->manager->emit('\OC\User', 'logout');
841
		$user = $this->getUser();
842
		if (!is_null($user)) {
843
			try {
844
				$this->tokenProvider->invalidateToken($this->session->getId());
845
			} catch (SessionNotAvailableException $ex) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
846
				
847
			}
848
		}
849
		$this->setUser(null);
850
		$this->setLoginName(null);
851
		$this->unsetMagicInCookie();
852
		$this->session->clear();
853
		$this->manager->emit('\OC\User', 'postLogout');
854
	}
855
856
	/**
857
	 * Set cookie value to use in next page load
858
	 *
859
	 * @param string $username username to be set
860
	 * @param string $token
861
	 */
862
	public function setMagicInCookie($username, $token) {
863
		$secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https';
864
		$expires = time() + OC::$server->getConfig()->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
865
		setcookie('oc_username', $username, $expires, OC::$WEBROOT, '', $secureCookie, true);
866
		setcookie('oc_token', $token, $expires, OC::$WEBROOT, '', $secureCookie, true);
867
		setcookie('oc_remember_login', '1', $expires, OC::$WEBROOT, '', $secureCookie, true);
868
	}
869
870
	/**
871
	 * Remove cookie for "remember username"
872
	 */
873
	public function unsetMagicInCookie() {
874
		//TODO: DI for cookies and IRequest
875
		$secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https';
876
877
		unset($_COOKIE['oc_username']); //TODO: DI
878
		unset($_COOKIE['oc_token']);
879
		unset($_COOKIE['oc_remember_login']);
880
		setcookie('oc_username', '', time() - 3600, OC::$WEBROOT, '', $secureCookie, true);
881
		setcookie('oc_token', '', time() - 3600, OC::$WEBROOT, '', $secureCookie, true);
882
		setcookie('oc_remember_login', '', time() - 3600, OC::$WEBROOT, '', $secureCookie, true);
883
		// old cookies might be stored under /webroot/ instead of /webroot
884
		// and Firefox doesn't like it!
885
		setcookie('oc_username', '', time() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
886
		setcookie('oc_token', '', time() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
887
		setcookie('oc_remember_login', '', time() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
888
	}
889
890
	/**
891
	 * Update password of the browser session token if there is one
892
	 *
893
	 * @param string $password
894
	 */
895
	public function updateSessionTokenPassword($password) {
896
		try {
897
			$sessionId = $this->session->getId();
898
			$token = $this->tokenProvider->getToken($sessionId);
899
			$this->tokenProvider->setPassword($token, $sessionId, $password);
900
		} catch (SessionNotAvailableException $ex) {
901
			// Nothing to do
902
		} catch (InvalidTokenException $ex) {
903
			// Nothing to do
904
		}
905
	}
906
907
}
908