Session   F
last analyzed

Complexity

Total Complexity 150

Size/Duplication

Total Lines 1125
Duplicated Lines 3.02 %

Coupling/Cohesion

Components 1
Dependencies 30

Importance

Changes 0
Metric Value
dl 34
loc 1125
rs 1.1
c 0
b 0
f 0
wmc 150
lcom 1
cbo 30

39 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 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
A getUser() 0 18 5
A validateSession() 0 24 5
A isLoggedIn() 0 8 2
A setLoginName() 0 7 2
A getLoginName() 0 13 3
A login() 0 16 3
B logClientIn() 0 30 11
A supportsCookies() 0 7 2
A isTokenAuthEnforced() 0 3 1
A isTwoFactorEnforced() 0 20 4
A isTokenPassword() 0 8 2
A prepareUserLogin() 0 36 5
A tryBasicAuthLogin() 0 23 5
A loginWithPassword() 0 34 4
B loginWithToken() 13 53 6
C loginWithApache() 0 84 14
A createSessionToken() 0 27 5
A getPassword() 0 16 4
C checkTokenCredentials() 0 86 11
B validateToken() 0 50 5
B tryTokenLogin() 10 21 6
A tryAuthModuleLogin() 0 13 3
A loginUser() 11 34 5
A loginWithCookie() 0 32 3
A logout() 0 28 4
A setMagicInCookie() 0 7 1
A unsetMagicInCookie() 0 15 1
A updateSessionTokenPassword() 0 11 3
B verifyAuthHeaders() 0 28 9
A getAuthModules() 0 18 5
A emitFailedLogin() 0 6 1
A hashToken() 0 4 1

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