Completed
Push — master ( a82218...06e071 )
by Thomas
11:03
created

Session::getAuthModules()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 6
nop 1
dl 0
loc 15
rs 9.2
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\Hooks\PublicEmitter;
44
use OC_User;
45
use OC_Util;
46
use OCA\DAV\Connector\Sabre\Auth;
47
use OCP\App\IServiceLoader;
48
use OCP\AppFramework\Utility\ITimeFactory;
49
use OCP\Authentication\IAuthModule;
50
use OCP\Files\NotPermittedException;
51
use OCP\IConfig;
52
use OCP\IRequest;
53
use OCP\ISession;
54
use OCP\IUser;
55
use OCP\IUserManager;
56
use OCP\IUserSession;
57
use OCP\Session\Exceptions\SessionNotAvailableException;
58
use OCP\Util;
59
use Symfony\Component\EventDispatcher\GenericEvent;
60
61
/**
62
 * Class Session
63
 *
64
 * Hooks available in scope \OC\User:
65
 * - preSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
66
 * - postSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
67
 * - preDelete(\OC\User\User $user)
68
 * - postDelete(\OC\User\User $user)
69
 * - preCreateUser(string $uid, string $password)
70
 * - postCreateUser(\OC\User\User $user)
71
 * - preLogin(string $user, string $password)
72
 * - postLogin(\OC\User\User $user, string $password)
73
 * - failedLogin(string $user)
74
 * - preRememberedLogin(string $uid)
75
 * - postRememberedLogin(\OC\User\User $user)
76
 * - logout()
77
 * - postLogout()
78
 *
79
 * @package OC\User
80
 */
81
class Session implements IUserSession, Emitter {
82
83
	/** @var IUserManager | PublicEmitter $manager */
84
	private $manager;
85
86
	/** @var ISession $session */
87
	private $session;
88
89
	/** @var ITimeFactory */
90
	private $timeFactory;
91
92
	/** @var IProvider */
93
	private $tokenProvider;
94
95
	/** @var IConfig */
96
	private $config;
97
98
	/** @var User $activeUser */
99
	protected $activeUser;
100
101
	/** @var IServiceLoader */
102
	private $serviceLoader;
103
104
	/**
105
	 * @param IUserManager $manager
106
	 * @param ISession $session
107
	 * @param ITimeFactory $timeFactory
108
	 * @param IProvider $tokenProvider
109
	 * @param IConfig $config
110
	 * @param IServiceLoader $serviceLoader
111
	 */
112
	public function __construct(IUserManager $manager, ISession $session,
113
								ITimeFactory $timeFactory, $tokenProvider,
114
								IConfig $config, IServiceLoader $serviceLoader) {
115
		$this->manager = $manager;
116
		$this->session = $session;
117
		$this->timeFactory = $timeFactory;
118
		$this->tokenProvider = $tokenProvider;
119
		$this->config = $config;
120
		$this->serviceLoader = $serviceLoader;
121
	}
122
123
	/**
124
	 * @param IProvider $provider
125
	 */
126
	public function setTokenProvider(IProvider $provider) {
127
		$this->tokenProvider = $provider;
128
	}
129
130
	/**
131
	 * @param string $scope
132
	 * @param string $method
133
	 * @param callable $callback
134
	 */
135
	public function listen($scope, $method, callable $callback) {
136
		$this->manager->listen($scope, $method, $callback);
137
	}
138
139
	/**
140
	 * @param string $scope optional
141
	 * @param string $method optional
142
	 * @param callable $callback optional
143
	 */
144
	public function removeListener($scope = null, $method = null, callable $callback = null) {
145
		$this->manager->removeListener($scope, $method, $callback);
146
	}
147
148
	/**
149
	 * get the session object
150
	 *
151
	 * @return ISession
152
	 */
153
	public function getSession() {
154
		return $this->session;
155
	}
156
157
	/**
158
	 * set the session object
159
	 *
160
	 * @param ISession $session
161
	 */
162
	public function setSession(ISession $session) {
163
		if ($this->session instanceof ISession) {
164
			$this->session->close();
165
		}
166
		$this->session = $session;
167
		$this->activeUser = null;
168
	}
169
170
	/**
171
	 * set the currently active user
172
	 *
173
	 * @param IUser|null $user
174
	 */
175
	public function setUser($user) {
176
		if (is_null($user)) {
177
			$this->session->remove('user_id');
178
		} else {
179
			$this->session->set('user_id', $user->getUID());
180
		}
181
		$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...
182
	}
183
184
	/**
185
	 * get the current active user
186
	 *
187
	 * @return IUser|null Current user, otherwise null
188
	 */
189
	public function getUser() {
190
		// FIXME: This is a quick'n dirty work-around for the incognito mode as
191
		// described at https://github.com/owncloud/core/pull/12912#issuecomment-67391155
192
		if (OC_User::isIncognitoMode()) {
193
			return null;
194
		}
195
		if (is_null($this->activeUser)) {
196
			$uid = $this->session->get('user_id');
197
			if (is_null($uid)) {
198
				return null;
199
			}
200
			$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...
201
			if (is_null($this->activeUser)) {
202
				return null;
203
			}
204
		}
205
		return $this->activeUser;
206
	}
207
208
	/**
209
	 * Validate whether the current session is valid
210
	 *
211
	 * - For token-authenticated clients, the token validity is checked
212
	 * - For browsers, the session token validity is checked
213
	 */
214
	public function validateSession() {
215
		if (!$this->getUser()) {
216
			return;
217
		}
218
219
		$token = null;
220
		$appPassword = $this->session->get('app_password');
221
222
		if (is_null($appPassword)) {
223
			try {
224
				$token = $this->session->getId();
225
			} catch (SessionNotAvailableException $ex) {
226
				return;
227
			}
228
		} else {
229
			$token = $appPassword;
230
		}
231
232
		if (!$this->validateToken($token)) {
233
			// Session was invalidated
234
			$this->logout();
235
		}
236
	}
237
238
	/**
239
	 * Checks whether the user is logged in
240
	 *
241
	 * @return bool if logged in
242
	 */
243
	public function isLoggedIn() {
244
		$user = $this->getUser();
245
		if (is_null($user)) {
246
			return false;
247
		}
248
249
		return $user->isEnabled();
250
	}
251
252
	/**
253
	 * set the login name
254
	 *
255
	 * @param string|null $loginName for the logged in user
256
	 */
257
	public function setLoginName($loginName) {
258
		if (is_null($loginName)) {
259
			$this->session->remove('loginname');
260
		} else {
261
			$this->session->set('loginname', $loginName);
262
		}
263
	}
264
265
	/**
266
	 * get the login name of the current user
267
	 *
268
	 * @return string
269
	 */
270
	public function getLoginName() {
271
		if ($this->activeUser) {
272
			return $this->session->get('loginname');
273
		} else {
274
			$uid = $this->session->get('user_id');
275
			if ($uid) {
276
				$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...
277
				return $this->session->get('loginname');
278
			} else {
279
				return null;
280
			}
281
		}
282
	}
283
284
	/**
285
	 * try to log in with the provided credentials
286
	 *
287
	 * @param string $uid
288
	 * @param string $password
289
	 * @return boolean|null
290
	 * @throws LoginException
291
	 */
292
	public function login($uid, $password) {
293
		$this->session->regenerateId();
294
295
		if ($this->validateToken($password, $uid)) {
296
			return $this->loginWithToken($password);
297
		}
298
		return $this->loginWithPassword($uid, $password);
299
	}
300
301
	/**
302
	 * Tries to log in a client
303
	 *
304
	 * Checks token auth enforced
305
	 * Checks 2FA enabled
306
	 *
307
	 * @param string $user
308
	 * @param string $password
309
	 * @param IRequest $request
310
	 * @throws LoginException
311
	 * @throws PasswordLoginForbiddenException
312
	 * @return boolean
313
	 */
314
	public function logClientIn($user, $password, IRequest $request) {
315
		$isTokenPassword = $this->isTokenPassword($password);
316
		if ($user === null || trim($user) === '') {
317
			throw new \InvalidArgumentException('$user cannot be empty');
318
		}
319
		if (!$isTokenPassword && $this->isTokenAuthEnforced()) {
320
			throw new PasswordLoginForbiddenException();
321
		}
322
		if (!$isTokenPassword && $this->isTwoFactorEnforced($user)) {
323
			throw new PasswordLoginForbiddenException();
324
		}
325
		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...
326
			$users = $this->manager->getByEmail($user);
327
			if (count($users) === 1) {
328
				return $this->login($users[0]->getUID(), $password);
329
			}
330
			return false;
331
		}
332
333
		if ($isTokenPassword) {
334
			$this->session->set('app_password', $password);
335
		} else if($this->supportsCookies($request)) {
336
			// Password login, but cookies supported -> create (browser) session token
337
			$this->createSessionToken($request, $this->getUser()->getUID(), $user, $password);
338
		}
339
340
		return true;
341
	}
342
343
	protected function supportsCookies(IRequest $request) {
344
		if (!is_null($request->getCookie('cookie_test'))) {
345
			return true;
346
		}
347
		setcookie('cookie_test', 'test', $this->timeFactory->getTime() + 3600);
348
		return false;
349
	}
350
351
	private function isTokenAuthEnforced() {
352
		return $this->config->getSystemValue('token_auth_enforced', false);
353
	}
354
355
	protected function isTwoFactorEnforced($username) {
356
		Util::emitHook(
357
			'\OCA\Files_Sharing\API\Server2Server',
358
			'preLoginNameUsedAsUserName',
359
			['uid' => &$username]
360
		);
361
		$user = $this->manager->get($username);
362
		if (is_null($user)) {
363
			$users = $this->manager->getByEmail($username);
364
			if (empty($users)) {
365
				return false;
366
			}
367
			if (count($users) !== 1) {
368
				return true;
369
			}
370
			$user = $users[0];
371
		}
372
		// DI not possible due to cyclic dependencies :'-/
373
		return OC::$server->getTwoFactorAuthManager()->isTwoFactorAuthenticated($user);
374
	}
375
376
	/**
377
	 * Check if the given 'password' is actually a device token
378
	 *
379
	 * @param string $password
380
	 * @return boolean
381
	 */
382
	public function isTokenPassword($password) {
383
		try {
384
			$this->tokenProvider->getToken($password);
385
			return true;
386
		} catch (InvalidTokenException $ex) {
387
			return false;
388
		}
389
	}
390
391
	/**
392
	 * Unintentional public
393
	 *
394
	 * @param bool $firstTimeLogin
395
	 */
396
	public function prepareUserLogin($firstTimeLogin = false) {
397
		// TODO: mock/inject/use non-static
398
		// Refresh the token
399
		\OC::$server->getCsrfTokenManager()->refreshToken();
400
		//we need to pass the user name, which may differ from login name
401
		$user = $this->getUser()->getUID();
402
		OC_Util::setupFS($user);
403
404
		if ($firstTimeLogin) {
405
			// TODO: lock necessary?
406
			//trigger creation of user home and /files folder
407
			$userFolder = \OC::$server->getUserFolder($user);
408
409
			try {
410
				// copy skeleton
411
				\OC_Util::copySkeleton($user, $userFolder);
0 ignored issues
show
Bug introduced by
It seems like $userFolder defined by \OC::$server->getUserFolder($user) on line 407 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...
412
			} catch (NotPermittedException $ex) {
413
				// possible if files directory is in an readonly jail
414
				\OC::$server->getLogger()->warning(
415
					'Skeleton not created due to missing write permission'
416
				);
417
			} catch(\OC\HintException $hintEx) {
418
				// only if Skeleton no existing Dir
419
				\OC::$server->getLogger()->error($hintEx->getMessage());
420
			}
421
422
			// trigger any other initialization
423
			\OC::$server->getEventDispatcher()->dispatch(IUser::class . '::firstLogin', new GenericEvent($this->getUser()));
424
		}
425
	}
426
427
	/**
428
	 * Tries to login the user with HTTP Basic Authentication
429
	 *
430
	 * @todo do not allow basic auth if the user is 2FA enforced
431
	 * @param IRequest $request
432
	 * @return boolean if the login was successful
433
	 */
434
	public function tryBasicAuthLogin(IRequest $request) {
435
		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...
436
			try {
437
				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...
438
					/**
439
					 * Add DAV authenticated. This should in an ideal world not be
440
					 * necessary but the iOS App reads cookies from anywhere instead
441
					 * only the DAV endpoint.
442
					 * This makes sure that the cookies will be valid for the whole scope
443
					 * @see https://github.com/owncloud/core/issues/22893
444
					 */
445
					$this->session->set(
446
						Auth::DAV_AUTHENTICATED, $this->getUser()->getUID()
447
					);
448
					return true;
449
				}
450
			} catch (PasswordLoginForbiddenException $ex) {
451
				// Nothing to do
452
			}
453
		}
454
		return false;
455
	}
456
457
	/**
458
	 * Log an user in via login name and password
459
	 *
460
	 * @param string $uid
461
	 * @param string $password
462
	 * @return boolean
463
	 * @throws LoginException if an app canceld the login process or the user is not enabled
464
	 */
465
	private function loginWithPassword($uid, $password) {
466
		$this->manager->emit('\OC\User', 'preLogin', [$uid, $password]);
467
		$user = $this->manager->checkPassword($uid, $password);
468
		if ($user === false) {
469
			$this->manager->emit('\OC\User', 'failedLogin', [$uid]);
470
			return false;
471
		}
472
473
		if ($user->isEnabled()) {
474
			$this->setUser($user);
475
			$this->setLoginName($uid);
476
			$firstTimeLogin = $user->updateLastLoginTimestamp();
477
			$this->manager->emit('\OC\User', 'postLogin', [$user, $password]);
478 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...
479
				$this->prepareUserLogin($firstTimeLogin);
480
				return true;
481
			} else {
482
				// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
483
				$message = \OC::$server->getL10N('lib')->t('Login canceled by app');
484
				throw new LoginException($message);
485
			}
486
		} else {
487
			// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
488
			$message = \OC::$server->getL10N('lib')->t('User disabled');
489
			throw new LoginException($message);
490
		}
491
	}
492
493
	/**
494
	 * Log an user in with a given token (id)
495
	 *
496
	 * @param string $token
497
	 * @return boolean
498
	 * @throws LoginException if an app canceld the login process or the user is not enabled
499
	 */
500
	private function loginWithToken($token) {
501
		try {
502
			$dbToken = $this->tokenProvider->getToken($token);
503
		} catch (InvalidTokenException $ex) {
504
			return false;
505
		}
506
		$uid = $dbToken->getUID();
507
508
		// When logging in with token, the password must be decrypted first before passing to login hook
509
		$password = '';
510
		try {
511
			$password = $this->tokenProvider->getPassword($dbToken, $token);
512
		} catch (PasswordlessTokenException $ex) {
513
			// Ignore and use empty string instead
514
		}
515
516
		$this->manager->emit('\OC\User', 'preLogin', [$uid, $password]);
517
518
		$user = $this->manager->get($uid);
519
		if (is_null($user)) {
520
			// user does not exist
521
			return false;
522
		}
523 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...
524
			// disabled users can not log in
525
			// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
526
			$message = \OC::$server->getL10N('lib')->t('User disabled');
527
			throw new LoginException($message);
528
		}
529
530
		//login
531
		$this->setUser($user);
532
		$this->setLoginName($dbToken->getLoginName());
533
		$this->manager->emit('\OC\User', 'postLogin', [$user, $password]);
534
535 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...
536
			$this->prepareUserLogin();
537
		} else {
538
			// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
539
			$message = \OC::$server->getL10N('lib')->t('Login canceled by app');
540
			throw new LoginException($message);
541
		}
542
543
		// set the app password
544
		$this->session->set('app_password', $token);
545
546
		return true;
547
	}
548
549
	/**
550
	 * Create a new session token for the given user credentials
551
	 *
552
	 * @param IRequest $request
553
	 * @param string $uid user UID
554
	 * @param string $loginName login name
555
	 * @param string $password
556
	 * @return boolean
557
	 */
558
	public function createSessionToken(IRequest $request, $uid, $loginName, $password = null) {
559
		if (is_null($this->manager->get($uid))) {
560
			// User does not exist
561
			return false;
562
		}
563
		$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...
564
		try {
565
			$sessionId = $this->session->getId();
566
			$pwd = $this->getPassword($password);
567
			$this->tokenProvider->generateToken($sessionId, $uid, $loginName, $pwd, $name);
568
			return true;
569
		} catch (SessionNotAvailableException $ex) {
570
			// This can happen with OCC, where a memory session is used
571
			// if a memory session is used, we shouldn't create a session token anyway
572
			return false;
573
		}
574
	}
575
576
	/**
577
	 * Checks if the given password is a token.
578
	 * If yes, the password is extracted from the token.
579
	 * If no, the same password is returned.
580
	 *
581
	 * @param string $password either the login password or a device token
582
	 * @return string|null the password or null if none was set in the token
583
	 */
584
	private function getPassword($password) {
585
		if (is_null($password)) {
586
			// This is surely no token ;-)
587
			return null;
588
		}
589
		try {
590
			$token = $this->tokenProvider->getToken($password);
591
			try {
592
				return $this->tokenProvider->getPassword($token, $password);
593
			} catch (PasswordlessTokenException $ex) {
594
				return null;
595
			}
596
		} catch (InvalidTokenException $ex) {
597
			return $password;
598
		}
599
	}
600
601
	/**
602
	 * @param IToken $dbToken
603
	 * @param string $token
604
	 * @return boolean
605
	 */
606
	private function checkTokenCredentials(IToken $dbToken, $token) {
607
		// Check whether login credentials are still valid and the user was not disabled
608
		// This check is performed each 5 minutes per default
609
		// However, we try to read last_check_timeout from the appconfig table so the
610
		// administrator could change this 5 minutes timeout
611
		$lastCheck = $dbToken->getLastCheck() ? : 0;
612
		$now = $this->timeFactory->getTime();
613
		$last_check_timeout = intval($this->config->getAppValue('core', 'last_check_timeout', 5));
614
		if ($lastCheck > ($now - 60 * $last_check_timeout)) {
615
			// Checked performed recently, nothing to do now
616
			return true;
617
		}
618
619
		try {
620
			$pwd = $this->tokenProvider->getPassword($dbToken, $token);
621
		} catch (InvalidTokenException $ex) {
622
			// An invalid token password was used -> log user out
623
			return false;
624
		} catch (PasswordlessTokenException $ex) {
625
			// Token has no password
626
627
			if (!is_null($this->activeUser) && !$this->activeUser->isEnabled()) {
628
				$this->tokenProvider->invalidateToken($token);
629
				return false;
630
			}
631
632
			$dbToken->setLastCheck($now);
633
			$this->tokenProvider->updateToken($dbToken);
634
			return true;
635
		}
636
637
		if ($this->manager->checkPassword($dbToken->getLoginName(), $pwd) === false
638
			|| (!is_null($this->activeUser) && !$this->activeUser->isEnabled())) {
639
			$this->tokenProvider->invalidateToken($token);
640
			// Password has changed or user was disabled -> log user out
641
			return false;
642
		}
643
		$dbToken->setLastCheck($now);
644
		$this->tokenProvider->updateToken($dbToken);
645
		return true;
646
	}
647
648
	/**
649
	 * Check if the given token exists and performs password/user-enabled checks
650
	 *
651
	 * Invalidates the token if checks fail
652
	 *
653
	 * @param string $token
654
	 * @param string $user login name
655
	 * @return boolean
656
	 */
657
	private function validateToken($token, $user = null) {
658
		try {
659
			$dbToken = $this->tokenProvider->getToken($token);
660
		} catch (InvalidTokenException $ex) {
661
			return false;
662
		}
663
664
		// Check if login names match
665
		if (!is_null($user) && $dbToken->getLoginName() !== $user) {
666
			// TODO: this makes it imposssible to use different login names on browser and client
667
			// e.g. login by e-mail '[email protected]' on browser for generating the token will not
668
			//      allow to use the client token with the login name 'user'.
669
			return false;
670
		}
671
672
		if (!$this->checkTokenCredentials($dbToken, $token)) {
673
			return false;
674
		}
675
676
		$this->tokenProvider->updateTokenActivity($dbToken);
677
678
		return true;
679
	}
680
681
	/**
682
	 * Tries to login the user with auth token header
683
	 *
684
	 * @param IRequest $request
685
	 * @todo check remember me cookie
686
	 * @return boolean
687
	 */
688
	public function tryTokenLogin(IRequest $request) {
689
		$authHeader = $request->getHeader('Authorization');
690 View Code Duplication
		if ($authHeader === null || strpos($authHeader, 'token ') === false) {
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...
691
			// No auth header, let's try session id
692
			try {
693
				$token = $this->session->getId();
694
			} catch (SessionNotAvailableException $ex) {
695
				return false;
696
			}
697
		} else {
698
			$token = substr($authHeader, 6);
699
		}
700
701
		if (!$this->loginWithToken($token)) {
702
			return false;
703
		}
704
		if(!$this->validateToken($token)) {
705
			return false;
706
		}
707
		return true;
708
	}
709
710
	/**
711
	 * Tries to login with an AuthModule provided by an app
712
	 *
713
	 * @param IRequest $request The request
714
	 * @return bool True if request can be authenticated, false otherwise
715
	 * @throws Exception If the auth module could not be loaded
716
	 */
717
	public function tryAuthModuleLogin(IRequest $request) {
718
		foreach ($this->getAuthModules(false) as $authModule) {
719
			$user = $authModule->auth($request);
720
			if ($user !== null) {
721
				return $this->loginUser($authModule->auth($request), $authModule->getUserPassword($request));
722
			}
723
		}
724
725
		return false;
726
	}
727
728
	/**
729
	 * Log an user in
730
	 *
731
	 * @param IUser $user The user
732
	 * @param String $password The user's password
733
	 * @return boolean True if the user can be authenticated, false otherwise
734
	 * @throws LoginException if an app canceld the login process or the user is not enabled
735
	 */
736
	private function loginUser($user, $password) {
737
		if (is_null($user)) {
738
			return false;
739
		}
740
741
		$this->manager->emit('\OC\User', 'preLogin', [$user, $password]);
742
743 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...
744
			$message = \OC::$server->getL10N('lib')->t('User disabled');
745
			throw new LoginException($message);
746
		}
747
748
		$this->setUser($user);
749
		$this->setLoginName($user->getDisplayName());
750
751
		$this->manager->emit('\OC\User', 'postLogin', [$user, $password]);
752
753 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...
754
			$this->prepareUserLogin(false);
755
		} else {
756
			$message = \OC::$server->getL10N('lib')->t('Login canceled by app');
757
			throw new LoginException($message);
758
		}
759
760
		return true;
761
	}
762
763
	/**
764
	 * perform login using the magic cookie (remember login)
765
	 *
766
	 * @param string $uid the username
767
	 * @param string $currentToken
768
	 * @return bool
769
	 */
770
	public function loginWithCookie($uid, $currentToken) {
771
		$this->session->regenerateId();
772
		$this->manager->emit('\OC\User', 'preRememberedLogin', [$uid]);
773
		$user = $this->manager->get($uid);
774
		if (is_null($user)) {
775
			// user does not exist
776
			return false;
777
		}
778
779
		// get stored tokens
780
		$tokens = OC::$server->getConfig()->getUserKeys($uid, 'login_token');
781
		// test cookies token against stored tokens
782
		if (!in_array($currentToken, $tokens, true)) {
783
			return false;
784
		}
785
		// replace successfully used token with a new one
786
		OC::$server->getConfig()->deleteUserValue($uid, 'login_token', $currentToken);
787
		$newToken = OC::$server->getSecureRandom()->generate(32);
788
		OC::$server->getConfig()->setUserValue($uid, 'login_token', $newToken, time());
789
		$this->setMagicInCookie($user->getUID(), $newToken);
790
791
		//login
792
		$this->setUser($user);
793
		$user->updateLastLoginTimestamp();
794
		$this->manager->emit('\OC\User', 'postRememberedLogin', [$user]);
795
		return true;
796
	}
797
798
	/**
799
	 * logout the user from the session
800
	 *
801
	 * @return bool
802
	 */
803
	public function logout() {
804
805
		$event = new GenericEvent(null, ['cancel' => false]);
806
		$eventDispatcher = \OC::$server->getEventDispatcher();
807
		$eventDispatcher->dispatch('\OC\User\Session::pre_logout', $event);
808
809
		$this->manager->emit('\OC\User', 'preLogout');
810
811
		if ($event['cancel'] === true) {
812
			return true;
813
		}
814
815
		$this->manager->emit('\OC\User', 'logout');
816
		$user = $this->getUser();
817
		if (!is_null($user)) {
818
			try {
819
				$this->tokenProvider->invalidateToken($this->session->getId());
820
			} catch (SessionNotAvailableException $ex) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
821
822
			}
823
		}
824
		$this->setUser(null);
825
		$this->setLoginName(null);
826
		$this->unsetMagicInCookie();
827
		$this->session->clear();
828
		$this->manager->emit('\OC\User', 'postLogout');
829
	}
830
831
	/**
832
	 * Set cookie value to use in next page load
833
	 *
834
	 * @param string $username username to be set
835
	 * @param string $token
836
	 */
837
	public function setMagicInCookie($username, $token) {
838
		$secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https';
839
		$expires = time() + OC::$server->getConfig()->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
840
		setcookie('oc_username', $username, $expires, OC::$WEBROOT, '', $secureCookie, true);
841
		setcookie('oc_token', $token, $expires, OC::$WEBROOT, '', $secureCookie, true);
842
		setcookie('oc_remember_login', '1', $expires, OC::$WEBROOT, '', $secureCookie, true);
843
	}
844
845
	/**
846
	 * Remove cookie for "remember username"
847
	 */
848
	public function unsetMagicInCookie() {
849
		//TODO: DI for cookies and IRequest
850
		$secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https';
851
852
		unset($_COOKIE['oc_username']); //TODO: DI
853
		unset($_COOKIE['oc_token']);
854
		unset($_COOKIE['oc_remember_login']);
855
		setcookie('oc_username', '', time() - 3600, OC::$WEBROOT, '', $secureCookie, true);
856
		setcookie('oc_token', '', time() - 3600, OC::$WEBROOT, '', $secureCookie, true);
857
		setcookie('oc_remember_login', '', time() - 3600, OC::$WEBROOT, '', $secureCookie, true);
858
		// old cookies might be stored under /webroot/ instead of /webroot
859
		// and Firefox doesn't like it!
860
		setcookie('oc_username', '', time() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
861
		setcookie('oc_token', '', time() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
862
		setcookie('oc_remember_login', '', time() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
863
	}
864
865
	/**
866
	 * Update password of the browser session token if there is one
867
	 *
868
	 * @param string $password
869
	 */
870
	public function updateSessionTokenPassword($password) {
871
		try {
872
			$sessionId = $this->session->getId();
873
			$token = $this->tokenProvider->getToken($sessionId);
874
			$this->tokenProvider->setPassword($token, $sessionId, $password);
875
		} catch (SessionNotAvailableException $ex) {
876
			// Nothing to do
877
		} catch (InvalidTokenException $ex) {
878
			// Nothing to do
879
		}
880
	}
881
882
	public function verifyAuthHeaders($request) {
883
		foreach ($this->getAuthModules(true) as $module) {
884
			$user = $module->auth($request);
885
			if ($user !== null) {
886
				if ($this->isLoggedIn() && $this->getUser()->getUID() !== $user->getUID()) {
887
					// the session is bad -> kill it
888
					$this->logout();
889
					return false;
890
				}
891
				return true;
892
			}
893
		}
894
895
		// the session is bad -> kill it
896
		$this->logout();
897
		return false;
898
	}
899
900
	/**
901
	 * @param $includeBuiltIn
902
	 * @return \Generator | IAuthModule[]
903
	 * @throws Exception
904
	 */
905
	private function getAuthModules($includeBuiltIn) {
906
		if ($includeBuiltIn) {
907
			yield new BasicAuthModule($this->manager);
908
			yield new TokenAuthModule($this->session, $this->tokenProvider, $this->manager);
909
		}
910
911
		$modules = $this->serviceLoader->load(['auth-modules']);
912
		foreach ($modules as $module) {
913
			if ($module instanceof IAuthModule) {
914
				yield $module;
915
			} else {
916
				continue;
917
			}
918
		}
919
	}
920
}
921