Completed
Push — master ( 79a16b...19fc68 )
by Blizzz
07:01
created

Session::unsetMagicInCookie()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 11
nc 1
nop 0
dl 0
loc 16
rs 9.4285
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 Sandro Lutz <[email protected]>
7
 * @author Arthur Schiwon <[email protected]>
8
 * @author Bernhard Posselt <[email protected]>
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Jörn Friedrich Dreyer <[email protected]>
11
 * @author Lukas Reschke <[email protected]>
12
 * @author Morris Jobke <[email protected]>
13
 * @author Robin Appelman <[email protected]>
14
 * @author Robin McCorkell <[email protected]>
15
 * @author Thomas Müller <[email protected]>
16
 * @author Vincent Petry <[email protected]>
17
 *
18
 * @license AGPL-3.0
19
 *
20
 * This code is free software: you can redistribute it and/or modify
21
 * it under the terms of the GNU Affero General Public License, version 3,
22
 * as published by the Free Software Foundation.
23
 *
24
 * This program is distributed in the hope that it will be useful,
25
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27
 * GNU Affero General Public License for more details.
28
 *
29
 * You should have received a copy of the GNU Affero General Public License, version 3,
30
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
31
 *
32
 */
33
34
namespace OC\User;
35
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_User;
44
use OC_Util;
45
use OCA\DAV\Connector\Sabre\Auth;
46
use OCP\AppFramework\Utility\ITimeFactory;
47
use OCP\IConfig;
48
use OCP\IRequest;
49
use OCP\ISession;
50
use OCP\IUser;
51
use OCP\IUserManager;
52
use OCP\IUserSession;
53
use OCP\Security\ISecureRandom;
54
use OCP\Session\Exceptions\SessionNotAvailableException;
55
use OCP\Util;
56
use Symfony\Component\EventDispatcher\GenericEvent;
57
58
/**
59
 * Class Session
60
 *
61
 * Hooks available in scope \OC\User:
62
 * - preSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
63
 * - postSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
64
 * - preDelete(\OC\User\User $user)
65
 * - postDelete(\OC\User\User $user)
66
 * - preCreateUser(string $uid, string $password)
67
 * - postCreateUser(\OC\User\User $user)
68
 * - preLogin(string $user, string $password)
69
 * - postLogin(\OC\User\User $user, string $password)
70
 * - preRememberedLogin(string $uid)
71
 * - postRememberedLogin(\OC\User\User $user)
72
 * - logout()
73
 *
74
 * @package OC\User
75
 */
76
class Session implements IUserSession, Emitter {
77
78
	/** @var IUserManager $manager */
79
	private $manager;
80
81
	/** @var ISession $session */
82
	private $session;
83
84
	/** @var ITimeFactory */
85
	private $timeFacory;
86
87
	/** @var IProvider */
88
	private $tokenProvider;
89
90
	/** @var IConfig */
91
	private $config;
92
93
	/** @var User $activeUser */
94
	protected $activeUser;
95
96
	/** @var ISecureRandom */
97
	private $random;
98
99
	/**
100
	 * @param IUserManager $manager
101
	 * @param ISession $session
102
	 * @param ITimeFactory $timeFacory
103
	 * @param IProvider $tokenProvider
104
	 * @param IConfig $config
105
	 * @param ISecureRandom $random
106
	 */
107
	public function __construct(IUserManager $manager,
108
								ISession $session,
109
								ITimeFactory $timeFacory,
110
								$tokenProvider,
111
								IConfig $config,
112
								ISecureRandom $random) {
113
		$this->manager = $manager;
114
		$this->session = $session;
115
		$this->timeFacory = $timeFacory;
116
		$this->tokenProvider = $tokenProvider;
117
		$this->config = $config;
118
		$this->random = $random;
119
	}
120
121
	/**
122
	 * @param IProvider $provider
123
	 */
124
	public function setTokenProvider(IProvider $provider) {
125
		$this->tokenProvider = $provider;
126
	}
127
128
	/**
129
	 * @param string $scope
130
	 * @param string $method
131
	 * @param callable $callback
132
	 */
133
	public function listen($scope, $method, callable $callback) {
134
		$this->manager->listen($scope, $method, $callback);
135
	}
136
137
	/**
138
	 * @param string $scope optional
139
	 * @param string $method optional
140
	 * @param callable $callback optional
141
	 */
142
	public function removeListener($scope = null, $method = null, callable $callback = null) {
143
		$this->manager->removeListener($scope, $method, $callback);
144
	}
145
146
	/**
147
	 * get the manager object
148
	 *
149
	 * @return Manager
150
	 */
151
	public function getManager() {
152
		return $this->manager;
153
	}
154
155
	/**
156
	 * get the session object
157
	 *
158
	 * @return ISession
159
	 */
160
	public function getSession() {
161
		return $this->session;
162
	}
163
164
	/**
165
	 * set the session object
166
	 *
167
	 * @param ISession $session
168
	 */
169
	public function setSession(ISession $session) {
170
		if ($this->session instanceof ISession) {
171
			$this->session->close();
172
		}
173
		$this->session = $session;
174
		$this->activeUser = null;
175
	}
176
177
	/**
178
	 * set the currently active user
179
	 *
180
	 * @param IUser|null $user
181
	 */
182
	public function setUser($user) {
183
		if (is_null($user)) {
184
			$this->session->remove('user_id');
185
		} else {
186
			$this->session->set('user_id', $user->getUID());
187
		}
188
		$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...
189
	}
190
191
	/**
192
	 * get the current active user
193
	 *
194
	 * @return IUser|null Current user, otherwise null
195
	 */
196
	public function getUser() {
197
		// FIXME: This is a quick'n dirty work-around for the incognito mode as
198
		// described at https://github.com/owncloud/core/pull/12912#issuecomment-67391155
199
		if (OC_User::isIncognitoMode()) {
200
			return null;
201
		}
202
		if (is_null($this->activeUser)) {
203
			$uid = $this->session->get('user_id');
204
			if (is_null($uid)) {
205
				return null;
206
			}
207
			$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...
208
			if (is_null($this->activeUser)) {
209
				return null;
210
			}
211
			$this->validateSession();
212
		}
213
		return $this->activeUser;
214
	}
215
216
	/**
217
	 * Validate whether the current session is valid
218
	 *
219
	 * - For token-authenticated clients, the token validity is checked
220
	 * - For browsers, the session token validity is checked
221
	 */
222
	protected function validateSession() {
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
	 * set the token id
290
	 *
291
	 * @param int|null $token that was used to log in
292
	 */
293
	protected function setToken($token) {
294
		if ($token === null) {
295
			$this->session->remove('token-id');
296
		} else {
297
			$this->session->set('token-id', $token);
298
		}
299
	}
300
301
	/**
302
	 * try to log in with the provided credentials
303
	 *
304
	 * @param string $uid
305
	 * @param string $password
306
	 * @return boolean|null
307
	 * @throws LoginException
308
	 */
309
	public function login($uid, $password) {
310
		$this->session->regenerateId();
311
		if ($this->validateToken($password, $uid)) {
312
			return $this->loginWithToken($password);
313
		}
314
		return $this->loginWithPassword($uid, $password);
315
	}
316
317
	/**
318
	 * Tries to log in a client
319
	 *
320
	 * Checks token auth enforced
321
	 * Checks 2FA enabled
322
	 *
323
	 * @param string $user
324
	 * @param string $password
325
	 * @param IRequest $request
326
	 * @param OC\Security\Bruteforce\Throttler $throttler
327
	 * @throws LoginException
328
	 * @throws PasswordLoginForbiddenException
329
	 * @return boolean
330
	 */
331
	public function logClientIn($user,
332
								$password,
333
								IRequest $request,
334
								OC\Security\Bruteforce\Throttler $throttler) {
335
		$currentDelay = $throttler->sleepDelay($request->getRemoteAddress(), 'login');
336
337
		if ($this->manager instanceof PublicEmitter) {
0 ignored issues
show
Bug introduced by
The class OC\User\PublicEmitter does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
338
			$this->manager->emit('\OC\User', 'preLogin', array($user, $password));
339
		}
340
341
		$isTokenPassword = $this->isTokenPassword($password);
342
		if (!$isTokenPassword && $this->isTokenAuthEnforced()) {
343
			throw new PasswordLoginForbiddenException();
344
		}
345
		if (!$isTokenPassword && $this->isTwoFactorEnforced($user)) {
346
			throw new PasswordLoginForbiddenException();
347
		}
348
		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...
349
			$users = $this->manager->getByEmail($user);
350
			if (count($users) === 1) {
351
				return $this->login($users[0]->getUID(), $password);
352
			}
353
354
			$throttler->registerAttempt('login', $request->getRemoteAddress(), ['uid' => $user]);
355
			if($currentDelay === 0) {
356
				$throttler->sleepDelay($request->getRemoteAddress(), 'login');
357
			}
358
			return false;
359
		}
360
361
		if ($isTokenPassword) {
362
			$this->session->set('app_password', $password);
363
		} else if($this->supportsCookies($request)) {
364
			// Password login, but cookies supported -> create (browser) session token
365
			$this->createSessionToken($request, $this->getUser()->getUID(), $user, $password);
366
		}
367
368
		return true;
369
	}
370
371
	protected function supportsCookies(IRequest $request) {
372
		if (!is_null($request->getCookie('cookie_test'))) {
373
			return true;
374
		}
375
		setcookie('cookie_test', 'test', $this->timeFacory->getTime() + 3600);
376
		return false;
377
	}
378
379
	private function isTokenAuthEnforced() {
380
		return $this->config->getSystemValue('token_auth_enforced', false);
381
	}
382
383
	protected function isTwoFactorEnforced($username) {
384
		Util::emitHook(
385
			'\OCA\Files_Sharing\API\Server2Server',
386
			'preLoginNameUsedAsUserName',
387
			array('uid' => &$username)
388
		);
389
		$user = $this->manager->get($username);
390
		if (is_null($user)) {
391
			$users = $this->manager->getByEmail($username);
392
			if (empty($users)) {
393
				return false;
394
			}
395
			if (count($users) !== 1) {
396
				return true;
397
			}
398
			$user = $users[0];
399
		}
400
		// DI not possible due to cyclic dependencies :'-/
401
		return OC::$server->getTwoFactorAuthManager()->isTwoFactorAuthenticated($user);
402
	}
403
404
	/**
405
	 * Check if the given 'password' is actually a device token
406
	 *
407
	 * @param string $password
408
	 * @return boolean
409
	 */
410
	public function isTokenPassword($password) {
411
		try {
412
			$this->tokenProvider->getToken($password);
413
			return true;
414
		} catch (InvalidTokenException $ex) {
415
			return false;
416
		}
417
	}
418
419
	protected function prepareUserLogin($firstTimeLogin) {
420
		// TODO: mock/inject/use non-static
421
		// Refresh the token
422
		\OC::$server->getCsrfTokenManager()->refreshToken();
423
		//we need to pass the user name, which may differ from login name
424
		$user = $this->getUser()->getUID();
425
		OC_Util::setupFS($user);
426
427
		if ($firstTimeLogin) {
428
			// TODO: lock necessary?
429
			//trigger creation of user home and /files folder
430
			$userFolder = \OC::$server->getUserFolder($user);
431
432
			// copy skeleton
433
			\OC_Util::copySkeleton($user, $userFolder);
0 ignored issues
show
Bug introduced by
It seems like $userFolder defined by \OC::$server->getUserFolder($user) on line 430 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...
434
435
			// trigger any other initialization
436
			\OC::$server->getEventDispatcher()->dispatch(IUser::class . '::firstLogin', new GenericEvent($this->getUser()));
437
		}
438
	}
439
440
	/**
441
	 * Tries to login the user with HTTP Basic Authentication
442
	 *
443
	 * @todo do not allow basic auth if the user is 2FA enforced
444
	 * @param IRequest $request
445
	 * @param OC\Security\Bruteforce\Throttler $throttler
446
	 * @return boolean if the login was successful
447
	 */
448
	public function tryBasicAuthLogin(IRequest $request,
449
									  OC\Security\Bruteforce\Throttler $throttler) {
450
		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...
451
			try {
452
				if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request, $throttler)) {
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...
453
					/**
454
					 * Add DAV authenticated. This should in an ideal world not be
455
					 * necessary but the iOS App reads cookies from anywhere instead
456
					 * only the DAV endpoint.
457
					 * This makes sure that the cookies will be valid for the whole scope
458
					 * @see https://github.com/owncloud/core/issues/22893
459
					 */
460
					$this->session->set(
461
						Auth::DAV_AUTHENTICATED, $this->getUser()->getUID()
462
					);
463
464
					// Set the last-password-confirm session to make the sudo mode work
465
					 $this->session->set('last-password-confirm', $this->timeFacory->getTime());
466
467
					return true;
468
				}
469
			} catch (PasswordLoginForbiddenException $ex) {
470
				// Nothing to do
471
			}
472
		}
473
		return false;
474
	}
475
476
	/**
477
	 * Log an user in via login name and password
478
	 *
479
	 * @param string $uid
480
	 * @param string $password
481
	 * @return boolean
482
	 * @throws LoginException if an app canceld the login process or the user is not enabled
483
	 */
484
	private function loginWithPassword($uid, $password) {
485
		$user = $this->manager->checkPassword($uid, $password);
486
		if ($user === false) {
487
			// Password check failed
488
			return false;
489
		}
490
491
		if ($user->isEnabled()) {
492
			$this->setUser($user);
493
			$this->setLoginName($uid);
494
			$this->setToken(null);
495
			$firstTimeLogin = $user->updateLastLoginTimestamp();
496
			$this->manager->emit('\OC\User', 'postLogin', [$user, $password]);
497 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...
498
				$this->prepareUserLogin($firstTimeLogin);
499
				return true;
500
			} else {
501
				// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
502
				$message = \OC::$server->getL10N('lib')->t('Login canceled by app');
503
				throw new LoginException($message);
504
			}
505
		} else {
506
			// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
507
			$message = \OC::$server->getL10N('lib')->t('User disabled');
508
			throw new LoginException($message);
509
		}
510
	}
511
512
	/**
513
	 * Log an user in with a given token (id)
514
	 *
515
	 * @param string $token
516
	 * @return boolean
517
	 * @throws LoginException if an app canceled the login process or the user is not enabled
518
	 */
519
	private function loginWithToken($token) {
520
		try {
521
			$dbToken = $this->tokenProvider->getToken($token);
522
		} catch (InvalidTokenException $ex) {
523
			return false;
524
		}
525
		$uid = $dbToken->getUID();
526
527
		// When logging in with token, the password must be decrypted first before passing to login hook
528
		$password = '';
529
		try {
530
			$password = $this->tokenProvider->getPassword($dbToken, $token);
531
		} catch (PasswordlessTokenException $ex) {
532
			// Ignore and use empty string instead
533
		}
534
535
		$user = $this->manager->get($uid);
536
		if (is_null($user)) {
537
			// user does not exist
538
			return false;
539
		}
540
		if (!$user->isEnabled()) {
541
			// disabled users can not log in
542
			// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
543
			$message = \OC::$server->getL10N('lib')->t('User disabled');
544
			throw new LoginException($message);
545
		}
546
547
		//login
548
		$this->setUser($user);
549
		$this->setLoginName($dbToken->getLoginName());
550
		$this->setToken($dbToken->getId());
551
		\OC::$server->getLockdownManager()->setToken($dbToken);
552
		$this->manager->emit('\OC\User', 'postLogin', array($user, $password));
553
554 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...
555
			$this->prepareUserLogin(false); // token login cant be the first
556
		} else {
557
			// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
558
			$message = \OC::$server->getL10N('lib')->t('Login canceled by app');
559
			throw new LoginException($message);
560
		}
561
562
		return true;
563
	}
564
565
	/**
566
	 * Create a new session token for the given user credentials
567
	 *
568
	 * @param IRequest $request
569
	 * @param string $uid user UID
570
	 * @param string $loginName login name
571
	 * @param string $password
572
	 * @param int $remember
573
	 * @return boolean
574
	 */
575
	public function createSessionToken(IRequest $request, $uid, $loginName, $password = null, $remember = IToken::DO_NOT_REMEMBER) {
576
		if (is_null($this->manager->get($uid))) {
577
			// User does not exist
578
			return false;
579
		}
580
		$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...
581
		try {
582
			$sessionId = $this->session->getId();
583
			$pwd = $this->getPassword($password);
584
			$this->tokenProvider->generateToken($sessionId, $uid, $loginName, $pwd, $name, IToken::TEMPORARY_TOKEN, $remember);
585
			return true;
586
		} catch (SessionNotAvailableException $ex) {
587
			// This can happen with OCC, where a memory session is used
588
			// if a memory session is used, we shouldn't create a session token anyway
589
			return false;
590
		}
591
	}
592
593
	/**
594
	 * Checks if the given password is a token.
595
	 * If yes, the password is extracted from the token.
596
	 * If no, the same password is returned.
597
	 *
598
	 * @param string $password either the login password or a device token
599
	 * @return string|null the password or null if none was set in the token
600
	 */
601
	private function getPassword($password) {
602
		if (is_null($password)) {
603
			// This is surely no token ;-)
604
			return null;
605
		}
606
		try {
607
			$token = $this->tokenProvider->getToken($password);
608
			try {
609
				return $this->tokenProvider->getPassword($token, $password);
610
			} catch (PasswordlessTokenException $ex) {
611
				return null;
612
			}
613
		} catch (InvalidTokenException $ex) {
614
			return $password;
615
		}
616
	}
617
618
	/**
619
	 * @param IToken $dbToken
620
	 * @param string $token
621
	 * @return boolean
622
	 */
623
	private function checkTokenCredentials(IToken $dbToken, $token) {
624
		// Check whether login credentials are still valid and the user was not disabled
625
		// This check is performed each 5 minutes
626
		$lastCheck = $dbToken->getLastCheck() ? : 0;
627
		$now = $this->timeFacory->getTime();
628
		if ($lastCheck > ($now - 60 * 5)) {
629
			// Checked performed recently, nothing to do now
630
			return true;
631
		}
632
633
		try {
634
			$pwd = $this->tokenProvider->getPassword($dbToken, $token);
635
		} catch (InvalidTokenException $ex) {
636
			// An invalid token password was used -> log user out
637
			return false;
638
		} catch (PasswordlessTokenException $ex) {
639
			// Token has no password
640
641
			if (!is_null($this->activeUser) && !$this->activeUser->isEnabled()) {
642
				$this->tokenProvider->invalidateToken($token);
643
				return false;
644
			}
645
646
			$dbToken->setLastCheck($now);
647
			return true;
648
		}
649
650
		if ($this->manager->checkPassword($dbToken->getLoginName(), $pwd) === false
651
			|| (!is_null($this->activeUser) && !$this->activeUser->isEnabled())) {
652
			$this->tokenProvider->invalidateToken($token);
653
			// Password has changed or user was disabled -> log user out
654
			return false;
655
		}
656
		$dbToken->setLastCheck($now);
657
		return true;
658
	}
659
660
	/**
661
	 * Check if the given token exists and performs password/user-enabled checks
662
	 *
663
	 * Invalidates the token if checks fail
664
	 *
665
	 * @param string $token
666
	 * @param string $user login name
667
	 * @return boolean
668
	 */
669
	private function validateToken($token, $user = null) {
670
		try {
671
			$dbToken = $this->tokenProvider->getToken($token);
672
		} catch (InvalidTokenException $ex) {
673
			return false;
674
		}
675
676
		// Check if login names match
677
		if (!is_null($user) && $dbToken->getLoginName() !== $user) {
678
			// TODO: this makes it imposssible to use different login names on browser and client
679
			// e.g. login by e-mail '[email protected]' on browser for generating the token will not
680
			//      allow to use the client token with the login name 'user'.
681
			return false;
682
		}
683
684
		if (!$this->checkTokenCredentials($dbToken, $token)) {
685
			return false;
686
		}
687
688
		$this->tokenProvider->updateTokenActivity($dbToken);
689
690
		return true;
691
	}
692
693
	/**
694
	 * Tries to login the user with auth token header
695
	 *
696
	 * @param IRequest $request
697
	 * @todo check remember me cookie
698
	 * @return boolean
699
	 */
700
	public function tryTokenLogin(IRequest $request) {
701
		$authHeader = $request->getHeader('Authorization');
702
		if (strpos($authHeader, 'token ') === false) {
703
			// No auth header, let's try session id
704
			try {
705
				$token = $this->session->getId();
706
			} catch (SessionNotAvailableException $ex) {
707
				return false;
708
			}
709
		} else {
710
			$token = substr($authHeader, 6);
711
		}
712
713
		if (!$this->loginWithToken($token)) {
714
			return false;
715
		}
716
		if(!$this->validateToken($token)) {
717
			return false;
718
		}
719
		return true;
720
	}
721
722
	/**
723
	 * perform login using the magic cookie (remember login)
724
	 *
725
	 * @param string $uid the username
726
	 * @param string $currentToken
727
	 * @param string $oldSessionId
728
	 * @return bool
729
	 */
730
	public function loginWithCookie($uid, $currentToken, $oldSessionId) {
731
		$this->session->regenerateId();
732
		$this->manager->emit('\OC\User', 'preRememberedLogin', array($uid));
733
		$user = $this->manager->get($uid);
734
		if (is_null($user)) {
735
			// user does not exist
736
			return false;
737
		}
738
739
		// get stored tokens
740
		$tokens = $this->config->getUserKeys($uid, 'login_token');
741
		// test cookies token against stored tokens
742
		if (!in_array($currentToken, $tokens, true)) {
743
			return false;
744
		}
745
		// replace successfully used token with a new one
746
		$this->config->deleteUserValue($uid, 'login_token', $currentToken);
747
		$newToken = $this->random->generate(32);
748
		$this->config->setUserValue($uid, 'login_token', $newToken, $this->timeFacory->getTime());
749
750
		try {
751
			$sessionId = $this->session->getId();
752
			$this->tokenProvider->renewSessionToken($oldSessionId, $sessionId);
753
		} catch (SessionNotAvailableException $ex) {
754
			return false;
755
		} catch (InvalidTokenException $ex) {
756
			\OC::$server->getLogger()->warning('Renewing session token failed', ['app' => 'core']);
757
			return false;
758
		}
759
760
		$this->setMagicInCookie($user->getUID(), $newToken);
761
		$token = $this->tokenProvider->getToken($sessionId);
762
763
		//login
764
		$this->setUser($user);
765
		$this->setLoginName($token->getLoginName());
766
		$this->setToken($token->getId());
767
		$user->updateLastLoginTimestamp();
768
		$this->manager->emit('\OC\User', 'postRememberedLogin', [$user]);
769
		return true;
770
	}
771
772
	/**
773
	 * @param IUser $user
774
	 */
775
	public function createRememberMeToken(IUser $user) {
776
		$token = $this->random->generate(32);
777
		$this->config->setUserValue($user->getUID(), 'login_token', $token, $this->timeFacory->getTime());
778
		$this->setMagicInCookie($user->getUID(), $token);
779
	}
780
781
	/**
782
	 * logout the user from the session
783
	 */
784
	public function logout() {
785
		$this->manager->emit('\OC\User', 'logout');
786
		$user = $this->getUser();
787
		if (!is_null($user)) {
788
			try {
789
				$this->tokenProvider->invalidateToken($this->session->getId());
790
			} catch (SessionNotAvailableException $ex) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
791
792
			}
793
		}
794
		$this->setUser(null);
795
		$this->setLoginName(null);
796
		$this->setToken(null);
797
		$this->unsetMagicInCookie();
798
		$this->session->clear();
799
	}
800
801
	/**
802
	 * Set cookie value to use in next page load
803
	 *
804
	 * @param string $username username to be set
805
	 * @param string $token
806
	 */
807
	public function setMagicInCookie($username, $token) {
808
		$secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https';
809
		$webRoot = \OC::$WEBROOT;
810
		if ($webRoot === '') {
811
			$webRoot = '/';
812
		}
813
814
		$expires = $this->timeFacory->getTime() + $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
815
		setcookie('nc_username', $username, $expires, $webRoot, '', $secureCookie, true);
816
		setcookie('nc_token', $token, $expires, $webRoot, '', $secureCookie, true);
817
		try {
818
			setcookie('nc_session_id', $this->session->getId(), $expires, $webRoot, '', $secureCookie, true);
819
		} catch (SessionNotAvailableException $ex) {
820
			// ignore
821
		}
822
	}
823
824
	/**
825
	 * Remove cookie for "remember username"
826
	 */
827
	public function unsetMagicInCookie() {
828
		//TODO: DI for cookies and IRequest
829
		$secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https';
830
831
		unset($_COOKIE['nc_username']); //TODO: DI
832
		unset($_COOKIE['nc_token']);
833
		unset($_COOKIE['nc_session_id']);
834
		setcookie('nc_username', '', $this->timeFacory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true);
835
		setcookie('nc_token', '', $this->timeFacory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true);
836
		setcookie('nc_session_id', '', $this->timeFacory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true);
837
		// old cookies might be stored under /webroot/ instead of /webroot
838
		// and Firefox doesn't like it!
839
		setcookie('nc_username', '', $this->timeFacory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
840
		setcookie('nc_token', '', $this->timeFacory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
841
		setcookie('nc_session_id', '', $this->timeFacory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
842
	}
843
844
	/**
845
	 * Update password of the browser session token if there is one
846
	 *
847
	 * @param string $password
848
	 */
849
	public function updateSessionTokenPassword($password) {
850
		try {
851
			$sessionId = $this->session->getId();
852
			$token = $this->tokenProvider->getToken($sessionId);
853
			$this->tokenProvider->setPassword($token, $sessionId, $password);
854
		} catch (SessionNotAvailableException $ex) {
855
			// Nothing to do
856
		} catch (InvalidTokenException $ex) {
857
			// Nothing to do
858
		}
859
	}
860
861
862
}
863