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