Completed
Push — master ( 664d98...03ed01 )
by Thomas
11:36
created

Session   F

Complexity

Total Complexity 144

Size/Duplication

Total Lines 959
Duplicated Lines 5.94 %

Coupling/Cohesion

Components 1
Dependencies 30

Importance

Changes 0
Metric Value
wmc 144
lcom 1
cbo 30
dl 57
loc 959
rs 1.0434
c 0
b 0
f 0

37 Methods

Rating   Name   Duplication   Size   Complexity  
A isLoggedIn() 0 8 2
A setLoginName() 0 7 2
A __construct() 0 12 1
A setTokenProvider() 0 3 1
A listen() 0 3 1
A removeListener() 0 3 1
A getSession() 0 3 1
A setSession() 0 7 2
A setUser() 0 8 2
B getUser() 0 18 5
B validateSession() 0 23 5
A login() 0 8 2
A createSessionToken() 0 17 4
A getPassword() 0 16 4
D checkTokenCredentials() 0 41 10
B validateToken() 0 23 5
B tryTokenLogin() 10 21 6
A tryAuthModuleLogin() 0 10 3
B loginUser() 10 28 4
B loginWithCookie() 0 27 3
B logout() 0 28 4
A setMagicInCookie() 0 7 1
A unsetMagicInCookie() 0 16 1
A updateSessionTokenPassword() 0 11 3
A getLoginName() 0 13 3
C logClientIn() 0 28 11
A supportsCookies() 0 7 2
A isTokenAuthEnforced() 0 3 1
A isTwoFactorEnforced() 0 20 4
A isTokenPassword() 0 8 2
B prepareUserLogin() 0 35 5
B tryBasicAuthLogin() 0 22 5
B loginWithPassword() 12 29 4
B loginWithToken() 13 48 6
C loginWithApache() 12 74 14
D verifyAuthHeaders() 0 29 9
B getAuthModules() 0 18 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Session often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Session, and based on these observations, apply Extract Interface, too.

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