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; |
|
|
|
|
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); |
|
|
|
|
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); |
|
|
|
|
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)) { |
|
|
|
|
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); |
|
|
|
|
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'])) { |
|
|
|
|
480
|
|
|
try { |
481
|
|
|
if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request)) { |
|
|
|
|
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()) { |
|
|
|
|
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()) { |
|
|
|
|
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'; |
|
|
|
|
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) { |
|
|
|
|
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()) { |
|
|
|
|
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()) { |
|
|
|
|
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
|
|
|
|
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 theid
property of an instance of theAccount
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.