Completed
Push — master ( 19d552...6b730b )
by Morris
21:07 queued 10:33
created

Session::updateTokens()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017, Sandro Lutz <[email protected]>
4
 * @copyright Copyright (c) 2016, ownCloud, Inc.
5
 *
6
 * @author Arthur Schiwon <[email protected]>
7
 * @author Bernhard Posselt <[email protected]>
8
 * @author Bjoern Schiessle <[email protected]>
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Felix Rupp <[email protected]>
11
 * @author Joas Schilling <[email protected]>
12
 * @author Jörn Friedrich Dreyer <[email protected]>
13
 * @author Lukas Reschke <[email protected]>
14
 * @author Morris Jobke <[email protected]>
15
 * @author Robin Appelman <[email protected]>
16
 * @author Robin McCorkell <[email protected]>
17
 * @author Roeland Jago Douma <[email protected]>
18
 * @author Sandro Lutz <[email protected]>
19
 * @author Thomas Müller <[email protected]>
20
 * @author Vincent Petry <[email protected]>
21
 *
22
 * @license AGPL-3.0
23
 *
24
 * This code is free software: you can redistribute it and/or modify
25
 * it under the terms of the GNU Affero General Public License, version 3,
26
 * as published by the Free Software Foundation.
27
 *
28
 * This program is distributed in the hope that it will be useful,
29
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
30
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31
 * GNU Affero General Public License for more details.
32
 *
33
 * You should have received a copy of the GNU Affero General Public License, version 3,
34
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
35
 *
36
 */
37
38
namespace OC\User;
39
40
use OC;
41
use OC\Authentication\Exceptions\InvalidTokenException;
42
use OC\Authentication\Exceptions\PasswordlessTokenException;
43
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
44
use OC\Authentication\Token\IProvider;
45
use OC\Authentication\Token\IToken;
46
use OC\Hooks\Emitter;
47
use OC\Hooks\PublicEmitter;
48
use OC_User;
49
use OC_Util;
50
use OCA\DAV\Connector\Sabre\Auth;
51
use OCP\AppFramework\Utility\ITimeFactory;
52
use OCP\Files\NotPermittedException;
53
use OCP\IConfig;
54
use OCP\ILogger;
55
use OCP\IRequest;
56
use OCP\ISession;
57
use OCP\IUser;
58
use OCP\IUserSession;
59
use OCP\Lockdown\ILockdownManager;
60
use OCP\Security\ISecureRandom;
61
use OCP\Session\Exceptions\SessionNotAvailableException;
62
use OCP\Util;
63
use Symfony\Component\EventDispatcher\GenericEvent;
64
65
/**
66
 * Class Session
67
 *
68
 * Hooks available in scope \OC\User:
69
 * - preSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
70
 * - postSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
71
 * - preDelete(\OC\User\User $user)
72
 * - postDelete(\OC\User\User $user)
73
 * - preCreateUser(string $uid, string $password)
74
 * - postCreateUser(\OC\User\User $user)
75
 * - assignedUserId(string $uid)
76
 * - preUnassignedUserId(string $uid)
77
 * - postUnassignedUserId(string $uid)
78
 * - preLogin(string $user, string $password)
79
 * - postLogin(\OC\User\User $user, string $password)
80
 * - preRememberedLogin(string $uid)
81
 * - postRememberedLogin(\OC\User\User $user)
82
 * - logout()
83
 * - postLogout()
84
 *
85
 * @package OC\User
86
 */
87
class Session implements IUserSession, Emitter {
88
89
	/** @var Manager|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 ISecureRandom */
108
	private $random;
109
110
	/** @var ILockdownManager  */
111
	private $lockdownManager;
112
113
	/** @var ILogger */
114
	private $logger;
115
116
	/**
117
	 * @param Manager $manager
118
	 * @param ISession $session
119
	 * @param ITimeFactory $timeFactory
120
	 * @param IProvider $tokenProvider
121
	 * @param IConfig $config
122
	 * @param ISecureRandom $random
123
	 * @param ILockdownManager $lockdownManager
124
	 * @param ILogger $logger
125
	 */
126
	public function __construct(Manager $manager,
127
								ISession $session,
128
								ITimeFactory $timeFactory,
129
								$tokenProvider,
130
								IConfig $config,
131
								ISecureRandom $random,
132
								ILockdownManager $lockdownManager,
133
								ILogger $logger) {
134
		$this->manager = $manager;
135
		$this->session = $session;
136
		$this->timeFactory = $timeFactory;
137
		$this->tokenProvider = $tokenProvider;
138
		$this->config = $config;
139
		$this->random = $random;
140
		$this->lockdownManager = $lockdownManager;
141
		$this->logger = $logger;
142
	}
143
144
	/**
145
	 * @param IProvider $provider
146
	 */
147
	public function setTokenProvider(IProvider $provider) {
148
		$this->tokenProvider = $provider;
149
	}
150
151
	/**
152
	 * @param string $scope
153
	 * @param string $method
154
	 * @param callable $callback
155
	 */
156
	public function listen($scope, $method, callable $callback) {
157
		$this->manager->listen($scope, $method, $callback);
158
	}
159
160
	/**
161
	 * @param string $scope optional
162
	 * @param string $method optional
163
	 * @param callable $callback optional
164
	 */
165
	public function removeListener($scope = null, $method = null, callable $callback = null) {
166
		$this->manager->removeListener($scope, $method, $callback);
167
	}
168
169
	/**
170
	 * get the manager object
171
	 *
172
	 * @return Manager|PublicEmitter
173
	 */
174
	public function getManager() {
175
		return $this->manager;
176
	}
177
178
	/**
179
	 * get the session object
180
	 *
181
	 * @return ISession
182
	 */
183
	public function getSession() {
184
		return $this->session;
185
	}
186
187
	/**
188
	 * set the session object
189
	 *
190
	 * @param ISession $session
191
	 */
192
	public function setSession(ISession $session) {
193
		if ($this->session instanceof ISession) {
194
			$this->session->close();
195
		}
196
		$this->session = $session;
197
		$this->activeUser = null;
198
	}
199
200
	/**
201
	 * set the currently active user
202
	 *
203
	 * @param IUser|null $user
204
	 */
205
	public function setUser($user) {
206
		if (is_null($user)) {
207
			$this->session->remove('user_id');
208
		} else {
209
			$this->session->set('user_id', $user->getUID());
210
		}
211
		$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...
212
	}
213
214
	/**
215
	 * get the current active user
216
	 *
217
	 * @return IUser|null Current user, otherwise null
218
	 */
219
	public function getUser() {
220
		// FIXME: This is a quick'n dirty work-around for the incognito mode as
221
		// described at https://github.com/owncloud/core/pull/12912#issuecomment-67391155
222
		if (OC_User::isIncognitoMode()) {
223
			return null;
224
		}
225
		if (is_null($this->activeUser)) {
226
			$uid = $this->session->get('user_id');
227
			if (is_null($uid)) {
228
				return null;
229
			}
230
			$this->activeUser = $this->manager->get($uid);
0 ignored issues
show
Bug introduced by
The method get does only exist in OC\User\Manager, but not in OC\Hooks\PublicEmitter.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
231
			if (is_null($this->activeUser)) {
232
				return null;
233
			}
234
			$this->validateSession();
235
		}
236
		return $this->activeUser;
237
	}
238
239
	/**
240
	 * Validate whether the current session is valid
241
	 *
242
	 * - For token-authenticated clients, the token validity is checked
243
	 * - For browsers, the session token validity is checked
244
	 */
245
	protected function validateSession() {
246
		$token = null;
247
		$appPassword = $this->session->get('app_password');
248
249
		if (is_null($appPassword)) {
250
			try {
251
				$token = $this->session->getId();
252
			} catch (SessionNotAvailableException $ex) {
253
				return;
254
			}
255
		} else {
256
			$token = $appPassword;
257
		}
258
259
		if (!$this->validateToken($token)) {
260
			// Session was invalidated
261
			$this->logout();
262
		}
263
	}
264
265
	/**
266
	 * Checks whether the user is logged in
267
	 *
268
	 * @return bool if logged in
269
	 */
270
	public function isLoggedIn() {
271
		$user = $this->getUser();
272
		if (is_null($user)) {
273
			return false;
274
		}
275
276
		return $user->isEnabled();
277
	}
278
279
	/**
280
	 * set the login name
281
	 *
282
	 * @param string|null $loginName for the logged in user
283
	 */
284
	public function setLoginName($loginName) {
285
		if (is_null($loginName)) {
286
			$this->session->remove('loginname');
287
		} else {
288
			$this->session->set('loginname', $loginName);
289
		}
290
	}
291
292
	/**
293
	 * get the login name of the current user
294
	 *
295
	 * @return string
296
	 */
297
	public function getLoginName() {
298
		if ($this->activeUser) {
299
			return $this->session->get('loginname');
300
		}
301
302
		$uid = $this->session->get('user_id');
303
		if ($uid) {
304
			$this->activeUser = $this->manager->get($uid);
0 ignored issues
show
Bug introduced by
The method get does only exist in OC\User\Manager, but not in OC\Hooks\PublicEmitter.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
305
			return $this->session->get('loginname');
306
		}
307
308
		return null;
309
	}
310
311
	/**
312
	 * set the token id
313
	 *
314
	 * @param int|null $token that was used to log in
315
	 */
316
	protected function setToken($token) {
317
		if ($token === null) {
318
			$this->session->remove('token-id');
319
		} else {
320
			$this->session->set('token-id', $token);
321
		}
322
	}
323
324
	/**
325
	 * try to log in with the provided credentials
326
	 *
327
	 * @param string $uid
328
	 * @param string $password
329
	 * @return boolean|null
330
	 * @throws LoginException
331
	 */
332
	public function login($uid, $password) {
333
		$this->session->regenerateId();
334
		if ($this->validateToken($password, $uid)) {
335
			return $this->loginWithToken($password);
336
		}
337
		return $this->loginWithPassword($uid, $password);
338
	}
339
340
	/**
341
	 * @param IUser $user
342
	 * @param array $loginDetails
343
	 * @param bool $regenerateSessionId
344
	 * @return true returns true if login successful or an exception otherwise
345
	 * @throws LoginException
346
	 */
347
	public function completeLogin(IUser $user, array $loginDetails, $regenerateSessionId = true) {
348
		if (!$user->isEnabled()) {
349
			// disabled users can not log in
350
			// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
351
			$message = \OC::$server->getL10N('lib')->t('User disabled');
352
			throw new LoginException($message);
353
		}
354
355
		if($regenerateSessionId) {
356
			$this->session->regenerateId();
357
		}
358
359
		$this->setUser($user);
360
		$this->setLoginName($loginDetails['loginName']);
361
362
		if(isset($loginDetails['token']) && $loginDetails['token'] instanceof IToken) {
363
			$this->setToken($loginDetails['token']->getId());
364
			$this->lockdownManager->setToken($loginDetails['token']);
365
			$firstTimeLogin = false;
366
		} else {
367
			$this->setToken(null);
368
			$firstTimeLogin = $user->updateLastLoginTimestamp();
369
		}
370
		$this->manager->emit('\OC\User', 'postLogin', [$user, $loginDetails['password']]);
371
		if($this->isLoggedIn()) {
372
			$this->prepareUserLogin($firstTimeLogin, $regenerateSessionId);
373
			return true;
374
		}
375
376
		$message = \OC::$server->getL10N('lib')->t('Login canceled by app');
377
		throw new LoginException($message);
378
	}
379
380
	/**
381
	 * Tries to log in a client
382
	 *
383
	 * Checks token auth enforced
384
	 * Checks 2FA enabled
385
	 *
386
	 * @param string $user
387
	 * @param string $password
388
	 * @param IRequest $request
389
	 * @param OC\Security\Bruteforce\Throttler $throttler
390
	 * @throws LoginException
391
	 * @throws PasswordLoginForbiddenException
392
	 * @return boolean
393
	 */
394
	public function logClientIn($user,
395
								$password,
396
								IRequest $request,
397
								OC\Security\Bruteforce\Throttler $throttler) {
398
		$currentDelay = $throttler->sleepDelay($request->getRemoteAddress(), 'login');
399
400
		if ($this->manager instanceof PublicEmitter) {
401
			$this->manager->emit('\OC\User', 'preLogin', array($user, $password));
402
		}
403
404
		$isTokenPassword = $this->isTokenPassword($password);
405
		if (!$isTokenPassword && $this->isTokenAuthEnforced()) {
406
			throw new PasswordLoginForbiddenException();
407
		}
408
		if (!$isTokenPassword && $this->isTwoFactorEnforced($user)) {
409
			throw new PasswordLoginForbiddenException();
410
		}
411
412
		// Try to login with this username and password
413
		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...
414
415
			// Failed, maybe the user used their email address
416
			$users = $this->manager->getByEmail($user);
0 ignored issues
show
Bug introduced by
The method getByEmail does only exist in OC\User\Manager, but not in OC\Hooks\PublicEmitter.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
417
			if (!(\count($users) === 1 && $this->login($users[0]->getUID(), $password))) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->login($users[0]->getUID(), $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...
418
419
				$this->logger->warning('Login failed: \'' . $user . '\' (Remote IP: \'' . \OC::$server->getRequest()->getRemoteAddress() . '\')', ['app' => 'core']);
420
421
				$throttler->registerAttempt('login', $request->getRemoteAddress(), ['uid' => $user]);
422
				if ($currentDelay === 0) {
423
					$throttler->sleepDelay($request->getRemoteAddress(), 'login');
424
				}
425
				return false;
426
			}
427
		}
428
429
		if ($isTokenPassword) {
430
			$this->session->set('app_password', $password);
431
		} else if($this->supportsCookies($request)) {
432
			// Password login, but cookies supported -> create (browser) session token
433
			$this->createSessionToken($request, $this->getUser()->getUID(), $user, $password);
434
		}
435
436
		return true;
437
	}
438
439
	protected function supportsCookies(IRequest $request) {
440
		if (!is_null($request->getCookie('cookie_test'))) {
441
			return true;
442
		}
443
		setcookie('cookie_test', 'test', $this->timeFactory->getTime() + 3600);
444
		return false;
445
	}
446
447
	private function isTokenAuthEnforced() {
448
		return $this->config->getSystemValue('token_auth_enforced', false);
449
	}
450
451
	protected function isTwoFactorEnforced($username) {
452
		Util::emitHook(
453
			'\OCA\Files_Sharing\API\Server2Server',
454
			'preLoginNameUsedAsUserName',
455
			array('uid' => &$username)
456
		);
457
		$user = $this->manager->get($username);
0 ignored issues
show
Bug introduced by
The method get does only exist in OC\User\Manager, but not in OC\Hooks\PublicEmitter.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
458
		if (is_null($user)) {
459
			$users = $this->manager->getByEmail($username);
0 ignored issues
show
Bug introduced by
The method getByEmail does only exist in OC\User\Manager, but not in OC\Hooks\PublicEmitter.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
460
			if (empty($users)) {
461
				return false;
462
			}
463
			if (count($users) !== 1) {
464
				return true;
465
			}
466
			$user = $users[0];
467
		}
468
		// DI not possible due to cyclic dependencies :'-/
469
		return OC::$server->getTwoFactorAuthManager()->isTwoFactorAuthenticated($user);
470
	}
471
472
	/**
473
	 * Check if the given 'password' is actually a device token
474
	 *
475
	 * @param string $password
476
	 * @return boolean
477
	 */
478
	public function isTokenPassword($password) {
479
		try {
480
			$this->tokenProvider->getToken($password);
481
			return true;
482
		} catch (InvalidTokenException $ex) {
483
			return false;
484
		}
485
	}
486
487
	protected function prepareUserLogin($firstTimeLogin, $refreshCsrfToken = true) {
488
		if ($refreshCsrfToken) {
489
			// TODO: mock/inject/use non-static
490
			// Refresh the token
491
			\OC::$server->getCsrfTokenManager()->refreshToken();
492
		}
493
494
		//we need to pass the user name, which may differ from login name
495
		$user = $this->getUser()->getUID();
496
		OC_Util::setupFS($user);
497
498
		if ($firstTimeLogin) {
499
			// TODO: lock necessary?
500
			//trigger creation of user home and /files folder
501
			$userFolder = \OC::$server->getUserFolder($user);
502
503
			try {
504
				// copy skeleton
505
				\OC_Util::copySkeleton($user, $userFolder);
0 ignored issues
show
Bug introduced by
It seems like $userFolder defined by \OC::$server->getUserFolder($user) on line 501 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...
506
			} catch (NotPermittedException $ex) {
507
				// read only uses
508
			}
509
510
			// trigger any other initialization
511
			\OC::$server->getEventDispatcher()->dispatch(IUser::class . '::firstLogin', new GenericEvent($this->getUser()));
512
		}
513
	}
514
515
	/**
516
	 * Tries to login the user with HTTP Basic Authentication
517
	 *
518
	 * @todo do not allow basic auth if the user is 2FA enforced
519
	 * @param IRequest $request
520
	 * @param OC\Security\Bruteforce\Throttler $throttler
521
	 * @return boolean if the login was successful
522
	 */
523
	public function tryBasicAuthLogin(IRequest $request,
524
									  OC\Security\Bruteforce\Throttler $throttler) {
525
		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...
526
			try {
527
				if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request, $throttler)) {
0 ignored issues
show
Bug introduced by
Accessing server on the interface OCP\IRequest suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
528
					/**
529
					 * Add DAV authenticated. This should in an ideal world not be
530
					 * necessary but the iOS App reads cookies from anywhere instead
531
					 * only the DAV endpoint.
532
					 * This makes sure that the cookies will be valid for the whole scope
533
					 * @see https://github.com/owncloud/core/issues/22893
534
					 */
535
					$this->session->set(
536
						Auth::DAV_AUTHENTICATED, $this->getUser()->getUID()
537
					);
538
539
					// Set the last-password-confirm session to make the sudo mode work
540
					 $this->session->set('last-password-confirm', $this->timeFactory->getTime());
541
542
					return true;
543
				}
544
			} catch (PasswordLoginForbiddenException $ex) {
545
				// Nothing to do
546
			}
547
		}
548
		return false;
549
	}
550
551
	/**
552
	 * Log an user in via login name and password
553
	 *
554
	 * @param string $uid
555
	 * @param string $password
556
	 * @return boolean
557
	 * @throws LoginException if an app canceld the login process or the user is not enabled
558
	 */
559
	private function loginWithPassword($uid, $password) {
560
		$user = $this->manager->checkPasswordNoLogging($uid, $password);
0 ignored issues
show
Bug introduced by
The method checkPasswordNoLogging does only exist in OC\User\Manager, but not in OC\Hooks\PublicEmitter.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
561
		if ($user === false) {
562
			// Password check failed
563
			return false;
564
		}
565
566
		return $this->completeLogin($user, ['loginName' => $uid, 'password' => $password], false);
567
	}
568
569
	/**
570
	 * Log an user in with a given token (id)
571
	 *
572
	 * @param string $token
573
	 * @return boolean
574
	 * @throws LoginException if an app canceled the login process or the user is not enabled
575
	 */
576
	private function loginWithToken($token) {
577
		try {
578
			$dbToken = $this->tokenProvider->getToken($token);
579
		} catch (InvalidTokenException $ex) {
580
			return false;
581
		}
582
		$uid = $dbToken->getUID();
583
584
		// When logging in with token, the password must be decrypted first before passing to login hook
585
		$password = '';
586
		try {
587
			$password = $this->tokenProvider->getPassword($dbToken, $token);
588
		} catch (PasswordlessTokenException $ex) {
589
			// Ignore and use empty string instead
590
		}
591
592
		$this->manager->emit('\OC\User', 'preLogin', array($uid, $password));
593
594
		$user = $this->manager->get($uid);
0 ignored issues
show
Bug introduced by
The method get does only exist in OC\User\Manager, but not in OC\Hooks\PublicEmitter.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
595
		if (is_null($user)) {
596
			// user does not exist
597
			return false;
598
		}
599
600
		return $this->completeLogin(
601
			$user,
602
			[
603
				'loginName' => $dbToken->getLoginName(),
604
				'password' => $password,
605
				'token' => $dbToken
606
			],
607
			false);
608
	}
609
610
	/**
611
	 * Create a new session token for the given user credentials
612
	 *
613
	 * @param IRequest $request
614
	 * @param string $uid user UID
615
	 * @param string $loginName login name
616
	 * @param string $password
617
	 * @param int $remember
618
	 * @return boolean
619
	 */
620
	public function createSessionToken(IRequest $request, $uid, $loginName, $password = null, $remember = IToken::DO_NOT_REMEMBER) {
621
		if (is_null($this->manager->get($uid))) {
0 ignored issues
show
Bug introduced by
The method get does only exist in OC\User\Manager, but not in OC\Hooks\PublicEmitter.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
622
			// User does not exist
623
			return false;
624
		}
625
		$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...
626
		try {
627
			$sessionId = $this->session->getId();
628
			$pwd = $this->getPassword($password);
629
			// Make sure the current sessionId has no leftover tokens
630
			$this->tokenProvider->invalidateToken($sessionId);
631
			$this->tokenProvider->generateToken($sessionId, $uid, $loginName, $pwd, $name, IToken::TEMPORARY_TOKEN, $remember);
632
			return true;
633
		} catch (SessionNotAvailableException $ex) {
634
			// This can happen with OCC, where a memory session is used
635
			// if a memory session is used, we shouldn't create a session token anyway
636
			return false;
637
		}
638
	}
639
640
	/**
641
	 * Checks if the given password is a token.
642
	 * If yes, the password is extracted from the token.
643
	 * If no, the same password is returned.
644
	 *
645
	 * @param string $password either the login password or a device token
646
	 * @return string|null the password or null if none was set in the token
647
	 */
648
	private function getPassword($password) {
649
		if (is_null($password)) {
650
			// This is surely no token ;-)
651
			return null;
652
		}
653
		try {
654
			$token = $this->tokenProvider->getToken($password);
655
			try {
656
				return $this->tokenProvider->getPassword($token, $password);
657
			} catch (PasswordlessTokenException $ex) {
658
				return null;
659
			}
660
		} catch (InvalidTokenException $ex) {
661
			return $password;
662
		}
663
	}
664
665
	/**
666
	 * @param IToken $dbToken
667
	 * @param string $token
668
	 * @return boolean
669
	 */
670
	private function checkTokenCredentials(IToken $dbToken, $token) {
671
		// Check whether login credentials are still valid and the user was not disabled
672
		// This check is performed each 5 minutes
673
		$lastCheck = $dbToken->getLastCheck() ? : 0;
674
		$now = $this->timeFactory->getTime();
675
		if ($lastCheck > ($now - 60 * 5)) {
676
			// Checked performed recently, nothing to do now
677
			return true;
678
		}
679
680
		try {
681
			$pwd = $this->tokenProvider->getPassword($dbToken, $token);
682
		} catch (InvalidTokenException $ex) {
683
			// An invalid token password was used -> log user out
684
			return false;
685
		} catch (PasswordlessTokenException $ex) {
686
			// Token has no password
687
688 View Code Duplication
			if (!is_null($this->activeUser) && !$this->activeUser->isEnabled()) {
689
				$this->tokenProvider->invalidateToken($token);
690
				return false;
691
			}
692
693
			$dbToken->setLastCheck($now);
694
			return true;
695
		}
696
697
		// Invalidate token if the user is no longer active
698 View Code Duplication
		if (!is_null($this->activeUser) && !$this->activeUser->isEnabled()) {
699
			$this->tokenProvider->invalidateToken($token);
700
			return false;
701
		}
702
703
		// If the token password is no longer valid mark it as such
704
		if ($this->manager->checkPassword($dbToken->getLoginName(), $pwd) === false) {
0 ignored issues
show
Bug introduced by
The method checkPassword does only exist in OC\User\Manager, but not in OC\Hooks\PublicEmitter.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
705
			$this->tokenProvider->markPasswordInvalid($dbToken, $token);
706
			// User is logged out
707
			return false;
708
		}
709
710
		$dbToken->setLastCheck($now);
711
		return true;
712
	}
713
714
	/**
715
	 * Check if the given token exists and performs password/user-enabled checks
716
	 *
717
	 * Invalidates the token if checks fail
718
	 *
719
	 * @param string $token
720
	 * @param string $user login name
721
	 * @return boolean
722
	 */
723
	private function validateToken($token, $user = null) {
724
		try {
725
			$dbToken = $this->tokenProvider->getToken($token);
726
		} catch (InvalidTokenException $ex) {
727
			return false;
728
		}
729
730
		// Check if login names match
731
		if (!is_null($user) && $dbToken->getLoginName() !== $user) {
732
			// TODO: this makes it imposssible to use different login names on browser and client
733
			// e.g. login by e-mail '[email protected]' on browser for generating the token will not
734
			//      allow to use the client token with the login name 'user'.
735
			return false;
736
		}
737
738
		if (!$this->checkTokenCredentials($dbToken, $token)) {
739
			return false;
740
		}
741
742
		// Update token scope
743
		$this->lockdownManager->setToken($dbToken);
744
745
		$this->tokenProvider->updateTokenActivity($dbToken);
746
747
		return true;
748
	}
749
750
	/**
751
	 * Tries to login the user with auth token header
752
	 *
753
	 * @param IRequest $request
754
	 * @todo check remember me cookie
755
	 * @return boolean
756
	 */
757
	public function tryTokenLogin(IRequest $request) {
758
		$authHeader = $request->getHeader('Authorization');
759
		if (strpos($authHeader, 'Bearer ') === false) {
760
			// No auth header, let's try session id
761
			try {
762
				$token = $this->session->getId();
763
			} catch (SessionNotAvailableException $ex) {
764
				return false;
765
			}
766
		} else {
767
			$token = substr($authHeader, 7);
768
		}
769
770
		if (!$this->loginWithToken($token)) {
771
			return false;
772
		}
773
		if(!$this->validateToken($token)) {
774
			return false;
775
		}
776
		return true;
777
	}
778
779
	/**
780
	 * perform login using the magic cookie (remember login)
781
	 *
782
	 * @param string $uid the username
783
	 * @param string $currentToken
784
	 * @param string $oldSessionId
785
	 * @return bool
786
	 */
787
	public function loginWithCookie($uid, $currentToken, $oldSessionId) {
788
		$this->session->regenerateId();
789
		$this->manager->emit('\OC\User', 'preRememberedLogin', array($uid));
790
		$user = $this->manager->get($uid);
0 ignored issues
show
Bug introduced by
The method get does only exist in OC\User\Manager, but not in OC\Hooks\PublicEmitter.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
791
		if (is_null($user)) {
792
			// user does not exist
793
			return false;
794
		}
795
796
		// get stored tokens
797
		$tokens = $this->config->getUserKeys($uid, 'login_token');
798
		// test cookies token against stored tokens
799
		if (!in_array($currentToken, $tokens, true)) {
800
			return false;
801
		}
802
		// replace successfully used token with a new one
803
		$this->config->deleteUserValue($uid, 'login_token', $currentToken);
804
		$newToken = $this->random->generate(32);
805
		$this->config->setUserValue($uid, 'login_token', $newToken, $this->timeFactory->getTime());
806
807
		try {
808
			$sessionId = $this->session->getId();
809
			$this->tokenProvider->renewSessionToken($oldSessionId, $sessionId);
810
		} catch (SessionNotAvailableException $ex) {
811
			return false;
812
		} catch (InvalidTokenException $ex) {
813
			\OC::$server->getLogger()->warning('Renewing session token failed', ['app' => 'core']);
814
			return false;
815
		}
816
817
		$this->setMagicInCookie($user->getUID(), $newToken);
818
		$token = $this->tokenProvider->getToken($sessionId);
819
820
		//login
821
		$this->setUser($user);
822
		$this->setLoginName($token->getLoginName());
823
		$this->setToken($token->getId());
824
		$this->lockdownManager->setToken($token);
825
		$user->updateLastLoginTimestamp();
826
		$password = null;
827
		try {
828
			$password = $this->tokenProvider->getPassword($token, $sessionId);
829
		} catch (PasswordlessTokenException $ex) {
830
			// Ignore
831
		}
832
		$this->manager->emit('\OC\User', 'postRememberedLogin', [$user, $password]);
833
		return true;
834
	}
835
836
	/**
837
	 * @param IUser $user
838
	 */
839
	public function createRememberMeToken(IUser $user) {
840
		$token = $this->random->generate(32);
841
		$this->config->setUserValue($user->getUID(), 'login_token', $token, $this->timeFactory->getTime());
842
		$this->setMagicInCookie($user->getUID(), $token);
843
	}
844
845
	/**
846
	 * logout the user from the session
847
	 */
848
	public function logout() {
849
		$this->manager->emit('\OC\User', 'logout');
850
		$user = $this->getUser();
851
		if (!is_null($user)) {
852
			try {
853
				$this->tokenProvider->invalidateToken($this->session->getId());
854
			} catch (SessionNotAvailableException $ex) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
855
856
			}
857
		}
858
		$this->setUser(null);
859
		$this->setLoginName(null);
860
		$this->setToken(null);
861
		$this->unsetMagicInCookie();
862
		$this->session->clear();
863
		$this->manager->emit('\OC\User', 'postLogout');
864
	}
865
866
	/**
867
	 * Set cookie value to use in next page load
868
	 *
869
	 * @param string $username username to be set
870
	 * @param string $token
871
	 */
872
	public function setMagicInCookie($username, $token) {
873
		$secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https';
874
		$webRoot = \OC::$WEBROOT;
875
		if ($webRoot === '') {
876
			$webRoot = '/';
877
		}
878
879
		$maxAge = $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
880
		\OC\Http\CookieHelper::setCookie(
881
			'nc_username',
882
			$username,
883
			$maxAge,
884
			$webRoot,
885
			'',
886
			$secureCookie,
887
			true,
888
			\OC\Http\CookieHelper::SAMESITE_LAX
889
		);
890
		\OC\Http\CookieHelper::setCookie(
891
			'nc_token',
892
			$token,
893
			$maxAge,
894
			$webRoot,
895
			'',
896
			$secureCookie,
897
			true,
898
			\OC\Http\CookieHelper::SAMESITE_LAX
899
		);
900
		try {
901
			\OC\Http\CookieHelper::setCookie(
902
				'nc_session_id',
903
				$this->session->getId(),
904
				$maxAge,
905
				$webRoot,
906
				'',
907
				$secureCookie,
908
				true,
909
				\OC\Http\CookieHelper::SAMESITE_LAX
910
			);
911
		} catch (SessionNotAvailableException $ex) {
912
			// ignore
913
		}
914
	}
915
916
	/**
917
	 * Remove cookie for "remember username"
918
	 */
919
	public function unsetMagicInCookie() {
920
		//TODO: DI for cookies and IRequest
921
		$secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https';
922
923
		unset($_COOKIE['nc_username']); //TODO: DI
924
		unset($_COOKIE['nc_token']);
925
		unset($_COOKIE['nc_session_id']);
926
		setcookie('nc_username', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true);
927
		setcookie('nc_token', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true);
928
		setcookie('nc_session_id', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true);
929
		// old cookies might be stored under /webroot/ instead of /webroot
930
		// and Firefox doesn't like it!
931
		setcookie('nc_username', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
932
		setcookie('nc_token', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
933
		setcookie('nc_session_id', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
934
	}
935
936
	/**
937
	 * Update password of the browser session token if there is one
938
	 *
939
	 * @param string $password
940
	 */
941
	public function updateSessionTokenPassword($password) {
942
		try {
943
			$sessionId = $this->session->getId();
944
			$token = $this->tokenProvider->getToken($sessionId);
945
			$this->tokenProvider->setPassword($token, $sessionId, $password);
946
		} catch (SessionNotAvailableException $ex) {
947
			// Nothing to do
948
		} catch (InvalidTokenException $ex) {
949
			// Nothing to do
950
		}
951
	}
952
953
	public function updateTokens(string $uid, string $password) {
954
		$this->tokenProvider->updatePasswords($uid, $password);
955
	}
956
957
958
}
959