1<?php 2/** 3 * @copyright Copyright (c) 2017, Sandro Lutz <sandro.lutz@temparus.ch> 4 * @copyright Copyright (c) 2016, ownCloud, Inc. 5 * 6 * @author Arthur Schiwon <blizzz@arthur-schiwon.de> 7 * @author Bernhard Posselt <dev@bernhard-posselt.com> 8 * @author Bjoern Schiessle <bjoern@schiessle.org> 9 * @author Christoph Wurst <christoph@winzerhof-wurst.at> 10 * @author Felix Rupp <github@felixrupp.com> 11 * @author Greta Doci <gretadoci@gmail.com> 12 * @author Joas Schilling <coding@schilljs.com> 13 * @author Jörn Friedrich Dreyer <jfd@butonic.de> 14 * @author Lionel Elie Mamane <lionel@mamane.lu> 15 * @author Lukas Reschke <lukas@statuscode.ch> 16 * @author Morris Jobke <hey@morrisjobke.de> 17 * @author Robin Appelman <robin@icewind.nl> 18 * @author Robin McCorkell <robin@mccorkell.me.uk> 19 * @author Roeland Jago Douma <roeland@famdouma.nl> 20 * @author Sandro Lutz <sandro.lutz@temparus.ch> 21 * @author Thomas Müller <thomas.mueller@tmit.eu> 22 * @author Vincent Petry <vincent@nextcloud.com> 23 * 24 * @license AGPL-3.0 25 * 26 * This code is free software: you can redistribute it and/or modify 27 * it under the terms of the GNU Affero General Public License, version 3, 28 * as published by the Free Software Foundation. 29 * 30 * This program is distributed in the hope that it will be useful, 31 * but WITHOUT ANY WARRANTY; without even the implied warranty of 32 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 33 * GNU Affero General Public License for more details. 34 * 35 * You should have received a copy of the GNU Affero General Public License, version 3, 36 * along with this program. If not, see <http://www.gnu.org/licenses/> 37 * 38 */ 39namespace OC\User; 40 41use OC; 42use OC\Authentication\Exceptions\ExpiredTokenException; 43use OC\Authentication\Exceptions\InvalidTokenException; 44use OC\Authentication\Exceptions\PasswordlessTokenException; 45use OC\Authentication\Exceptions\PasswordLoginForbiddenException; 46use OC\Authentication\Token\IProvider; 47use OC\Authentication\Token\IToken; 48use OC\Hooks\Emitter; 49use OC\Hooks\PublicEmitter; 50use OC_User; 51use OC_Util; 52use OCA\DAV\Connector\Sabre\Auth; 53use OCP\AppFramework\Utility\ITimeFactory; 54use OCP\EventDispatcher\IEventDispatcher; 55use OCP\Files\NotPermittedException; 56use OCP\IConfig; 57use OCP\ILogger; 58use OCP\IRequest; 59use OCP\ISession; 60use OCP\IUser; 61use OCP\IUserSession; 62use OCP\Lockdown\ILockdownManager; 63use OCP\Security\ISecureRandom; 64use OCP\Session\Exceptions\SessionNotAvailableException; 65use OCP\User\Events\PostLoginEvent; 66use OCP\Util; 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 * - assignedUserId(string $uid) 80 * - preUnassignedUserId(string $uid) 81 * - postUnassignedUserId(string $uid) 82 * - preLogin(string $user, string $password) 83 * - postLogin(\OC\User\User $user, string $loginName, string $password, boolean $isTokenLogin) 84 * - preRememberedLogin(string $uid) 85 * - postRememberedLogin(\OC\User\User $user) 86 * - logout() 87 * - postLogout() 88 * 89 * @package OC\User 90 */ 91class Session implements IUserSession, Emitter { 92 93 /** @var Manager|PublicEmitter $manager */ 94 private $manager; 95 96 /** @var ISession $session */ 97 private $session; 98 99 /** @var ITimeFactory */ 100 private $timeFactory; 101 102 /** @var IProvider */ 103 private $tokenProvider; 104 105 /** @var IConfig */ 106 private $config; 107 108 /** @var User $activeUser */ 109 protected $activeUser; 110 111 /** @var ISecureRandom */ 112 private $random; 113 114 /** @var ILockdownManager */ 115 private $lockdownManager; 116 117 /** @var ILogger */ 118 private $logger; 119 /** @var IEventDispatcher */ 120 private $dispatcher; 121 122 /** 123 * @param Manager $manager 124 * @param ISession $session 125 * @param ITimeFactory $timeFactory 126 * @param IProvider $tokenProvider 127 * @param IConfig $config 128 * @param ISecureRandom $random 129 * @param ILockdownManager $lockdownManager 130 * @param ILogger $logger 131 */ 132 public function __construct(Manager $manager, 133 ISession $session, 134 ITimeFactory $timeFactory, 135 $tokenProvider, 136 IConfig $config, 137 ISecureRandom $random, 138 ILockdownManager $lockdownManager, 139 ILogger $logger, 140 IEventDispatcher $dispatcher 141 ) { 142 $this->manager = $manager; 143 $this->session = $session; 144 $this->timeFactory = $timeFactory; 145 $this->tokenProvider = $tokenProvider; 146 $this->config = $config; 147 $this->random = $random; 148 $this->lockdownManager = $lockdownManager; 149 $this->logger = $logger; 150 $this->dispatcher = $dispatcher; 151 } 152 153 /** 154 * @param IProvider $provider 155 */ 156 public function setTokenProvider(IProvider $provider) { 157 $this->tokenProvider = $provider; 158 } 159 160 /** 161 * @param string $scope 162 * @param string $method 163 * @param callable $callback 164 */ 165 public function listen($scope, $method, callable $callback) { 166 $this->manager->listen($scope, $method, $callback); 167 } 168 169 /** 170 * @param string $scope optional 171 * @param string $method optional 172 * @param callable $callback optional 173 */ 174 public function removeListener($scope = null, $method = null, callable $callback = null) { 175 $this->manager->removeListener($scope, $method, $callback); 176 } 177 178 /** 179 * get the manager object 180 * 181 * @return Manager|PublicEmitter 182 */ 183 public function getManager() { 184 return $this->manager; 185 } 186 187 /** 188 * get the session object 189 * 190 * @return ISession 191 */ 192 public function getSession() { 193 return $this->session; 194 } 195 196 /** 197 * set the session object 198 * 199 * @param ISession $session 200 */ 201 public function setSession(ISession $session) { 202 if ($this->session instanceof ISession) { 203 $this->session->close(); 204 } 205 $this->session = $session; 206 $this->activeUser = null; 207 } 208 209 /** 210 * set the currently active user 211 * 212 * @param IUser|null $user 213 */ 214 public function setUser($user) { 215 if (is_null($user)) { 216 $this->session->remove('user_id'); 217 } else { 218 $this->session->set('user_id', $user->getUID()); 219 } 220 $this->activeUser = $user; 221 } 222 223 /** 224 * get the current active user 225 * 226 * @return IUser|null Current user, otherwise null 227 */ 228 public function getUser() { 229 // FIXME: This is a quick'n dirty work-around for the incognito mode as 230 // described at https://github.com/owncloud/core/pull/12912#issuecomment-67391155 231 if (OC_User::isIncognitoMode()) { 232 return null; 233 } 234 if (is_null($this->activeUser)) { 235 $uid = $this->session->get('user_id'); 236 if (is_null($uid)) { 237 return null; 238 } 239 $this->activeUser = $this->manager->get($uid); 240 if (is_null($this->activeUser)) { 241 return null; 242 } 243 $this->validateSession(); 244 } 245 return $this->activeUser; 246 } 247 248 /** 249 * Validate whether the current session is valid 250 * 251 * - For token-authenticated clients, the token validity is checked 252 * - For browsers, the session token validity is checked 253 */ 254 protected function validateSession() { 255 $token = null; 256 $appPassword = $this->session->get('app_password'); 257 258 if (is_null($appPassword)) { 259 try { 260 $token = $this->session->getId(); 261 } catch (SessionNotAvailableException $ex) { 262 return; 263 } 264 } else { 265 $token = $appPassword; 266 } 267 268 if (!$this->validateToken($token)) { 269 // Session was invalidated 270 $this->logout(); 271 } 272 } 273 274 /** 275 * Checks whether the user is logged in 276 * 277 * @return bool if logged in 278 */ 279 public function isLoggedIn() { 280 $user = $this->getUser(); 281 if (is_null($user)) { 282 return false; 283 } 284 285 return $user->isEnabled(); 286 } 287 288 /** 289 * set the login name 290 * 291 * @param string|null $loginName for the logged in user 292 */ 293 public function setLoginName($loginName) { 294 if (is_null($loginName)) { 295 $this->session->remove('loginname'); 296 } else { 297 $this->session->set('loginname', $loginName); 298 } 299 } 300 301 /** 302 * get the login name of the current user 303 * 304 * @return string 305 */ 306 public function getLoginName() { 307 if ($this->activeUser) { 308 return $this->session->get('loginname'); 309 } 310 311 $uid = $this->session->get('user_id'); 312 if ($uid) { 313 $this->activeUser = $this->manager->get($uid); 314 return $this->session->get('loginname'); 315 } 316 317 return null; 318 } 319 320 /** 321 * @return null|string 322 */ 323 public function getImpersonatingUserID(): ?string { 324 return $this->session->get('oldUserId'); 325 } 326 327 public function setImpersonatingUserID(bool $useCurrentUser = true): void { 328 if ($useCurrentUser === false) { 329 $this->session->remove('oldUserId'); 330 return; 331 } 332 333 $currentUser = $this->getUser(); 334 335 if ($currentUser === null) { 336 throw new \OC\User\NoUserException(); 337 } 338 $this->session->set('oldUserId', $currentUser->getUID()); 339 } 340 /** 341 * set the token id 342 * 343 * @param int|null $token that was used to log in 344 */ 345 protected function setToken($token) { 346 if ($token === null) { 347 $this->session->remove('token-id'); 348 } else { 349 $this->session->set('token-id', $token); 350 } 351 } 352 353 /** 354 * try to log in with the provided credentials 355 * 356 * @param string $uid 357 * @param string $password 358 * @return boolean|null 359 * @throws LoginException 360 */ 361 public function login($uid, $password) { 362 $this->session->regenerateId(); 363 if ($this->validateToken($password, $uid)) { 364 return $this->loginWithToken($password); 365 } 366 return $this->loginWithPassword($uid, $password); 367 } 368 369 /** 370 * @param IUser $user 371 * @param array $loginDetails 372 * @param bool $regenerateSessionId 373 * @return true returns true if login successful or an exception otherwise 374 * @throws LoginException 375 */ 376 public function completeLogin(IUser $user, array $loginDetails, $regenerateSessionId = true) { 377 if (!$user->isEnabled()) { 378 // disabled users can not log in 379 // injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory 380 $message = \OC::$server->getL10N('lib')->t('User disabled'); 381 throw new LoginException($message); 382 } 383 384 if ($regenerateSessionId) { 385 $this->session->regenerateId(); 386 } 387 388 $this->setUser($user); 389 $this->setLoginName($loginDetails['loginName']); 390 391 $isToken = isset($loginDetails['token']) && $loginDetails['token'] instanceof IToken; 392 if ($isToken) { 393 $this->setToken($loginDetails['token']->getId()); 394 $this->lockdownManager->setToken($loginDetails['token']); 395 $firstTimeLogin = false; 396 } else { 397 $this->setToken(null); 398 $firstTimeLogin = $user->updateLastLoginTimestamp(); 399 } 400 401 $this->dispatcher->dispatchTyped(new PostLoginEvent( 402 $user, 403 $loginDetails['loginName'], 404 $loginDetails['password'], 405 $isToken 406 )); 407 $this->manager->emit('\OC\User', 'postLogin', [ 408 $user, 409 $loginDetails['loginName'], 410 $loginDetails['password'], 411 $isToken, 412 ]); 413 if ($this->isLoggedIn()) { 414 $this->prepareUserLogin($firstTimeLogin, $regenerateSessionId); 415 return true; 416 } 417 418 $message = \OC::$server->getL10N('lib')->t('Login canceled by app'); 419 throw new LoginException($message); 420 } 421 422 /** 423 * Tries to log in a client 424 * 425 * Checks token auth enforced 426 * Checks 2FA enabled 427 * 428 * @param string $user 429 * @param string $password 430 * @param IRequest $request 431 * @param OC\Security\Bruteforce\Throttler $throttler 432 * @throws LoginException 433 * @throws PasswordLoginForbiddenException 434 * @return boolean 435 */ 436 public function logClientIn($user, 437 $password, 438 IRequest $request, 439 OC\Security\Bruteforce\Throttler $throttler) { 440 $currentDelay = $throttler->sleepDelay($request->getRemoteAddress(), 'login'); 441 442 if ($this->manager instanceof PublicEmitter) { 443 $this->manager->emit('\OC\User', 'preLogin', [$user, $password]); 444 } 445 446 try { 447 $isTokenPassword = $this->isTokenPassword($password); 448 } catch (ExpiredTokenException $e) { 449 // Just return on an expired token no need to check further or record a failed login 450 return false; 451 } 452 453 if (!$isTokenPassword && $this->isTokenAuthEnforced()) { 454 throw new PasswordLoginForbiddenException(); 455 } 456 if (!$isTokenPassword && $this->isTwoFactorEnforced($user)) { 457 throw new PasswordLoginForbiddenException(); 458 } 459 460 // Try to login with this username and password 461 if (!$this->login($user, $password)) { 462 463 // Failed, maybe the user used their email address 464 $users = $this->manager->getByEmail($user); 465 if (!(\count($users) === 1 && $this->login($users[0]->getUID(), $password))) { 466 $this->logger->warning('Login failed: \'' . $user . '\' (Remote IP: \'' . \OC::$server->getRequest()->getRemoteAddress() . '\')', ['app' => 'core']); 467 468 $throttler->registerAttempt('login', $request->getRemoteAddress(), ['user' => $user]); 469 470 $this->dispatcher->dispatchTyped(new OC\Authentication\Events\LoginFailed($user)); 471 472 if ($currentDelay === 0) { 473 $throttler->sleepDelay($request->getRemoteAddress(), 'login'); 474 } 475 return false; 476 } 477 } 478 479 if ($isTokenPassword) { 480 $this->session->set('app_password', $password); 481 } elseif ($this->supportsCookies($request)) { 482 // Password login, but cookies supported -> create (browser) session token 483 $this->createSessionToken($request, $this->getUser()->getUID(), $user, $password); 484 } 485 486 return true; 487 } 488 489 protected function supportsCookies(IRequest $request) { 490 if (!is_null($request->getCookie('cookie_test'))) { 491 return true; 492 } 493 setcookie('cookie_test', 'test', $this->timeFactory->getTime() + 3600); 494 return false; 495 } 496 497 private function isTokenAuthEnforced() { 498 return $this->config->getSystemValue('token_auth_enforced', false); 499 } 500 501 protected function isTwoFactorEnforced($username) { 502 Util::emitHook( 503 '\OCA\Files_Sharing\API\Server2Server', 504 'preLoginNameUsedAsUserName', 505 ['uid' => &$username] 506 ); 507 $user = $this->manager->get($username); 508 if (is_null($user)) { 509 $users = $this->manager->getByEmail($username); 510 if (empty($users)) { 511 return false; 512 } 513 if (count($users) !== 1) { 514 return true; 515 } 516 $user = $users[0]; 517 } 518 // DI not possible due to cyclic dependencies :'-/ 519 return OC::$server->getTwoFactorAuthManager()->isTwoFactorAuthenticated($user); 520 } 521 522 /** 523 * Check if the given 'password' is actually a device token 524 * 525 * @param string $password 526 * @return boolean 527 * @throws ExpiredTokenException 528 */ 529 public function isTokenPassword($password) { 530 try { 531 $this->tokenProvider->getToken($password); 532 return true; 533 } catch (ExpiredTokenException $e) { 534 throw $e; 535 } catch (InvalidTokenException $ex) { 536 $this->logger->logException($ex, [ 537 'level' => ILogger::DEBUG, 538 'message' => 'Token is not valid: ' . $ex->getMessage(), 539 ]); 540 return false; 541 } 542 } 543 544 protected function prepareUserLogin($firstTimeLogin, $refreshCsrfToken = true) { 545 if ($refreshCsrfToken) { 546 // TODO: mock/inject/use non-static 547 // Refresh the token 548 \OC::$server->getCsrfTokenManager()->refreshToken(); 549 } 550 551 //we need to pass the user name, which may differ from login name 552 $user = $this->getUser()->getUID(); 553 OC_Util::setupFS($user); 554 555 if ($firstTimeLogin) { 556 // TODO: lock necessary? 557 //trigger creation of user home and /files folder 558 $userFolder = \OC::$server->getUserFolder($user); 559 560 try { 561 // copy skeleton 562 \OC_Util::copySkeleton($user, $userFolder); 563 } catch (NotPermittedException $ex) { 564 // read only uses 565 } 566 567 // trigger any other initialization 568 \OC::$server->getEventDispatcher()->dispatch(IUser::class . '::firstLogin', new GenericEvent($this->getUser())); 569 } 570 } 571 572 /** 573 * Tries to login the user with HTTP Basic Authentication 574 * 575 * @todo do not allow basic auth if the user is 2FA enforced 576 * @param IRequest $request 577 * @param OC\Security\Bruteforce\Throttler $throttler 578 * @return boolean if the login was successful 579 */ 580 public function tryBasicAuthLogin(IRequest $request, 581 OC\Security\Bruteforce\Throttler $throttler) { 582 if (!empty($request->server['PHP_AUTH_USER']) && !empty($request->server['PHP_AUTH_PW'])) { 583 try { 584 if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request, $throttler)) { 585 /** 586 * Add DAV authenticated. This should in an ideal world not be 587 * necessary but the iOS App reads cookies from anywhere instead 588 * only the DAV endpoint. 589 * This makes sure that the cookies will be valid for the whole scope 590 * @see https://github.com/owncloud/core/issues/22893 591 */ 592 $this->session->set( 593 Auth::DAV_AUTHENTICATED, $this->getUser()->getUID() 594 ); 595 596 // Set the last-password-confirm session to make the sudo mode work 597 $this->session->set('last-password-confirm', $this->timeFactory->getTime()); 598 599 return true; 600 } 601 // If credentials were provided, they need to be valid, otherwise we do boom 602 throw new LoginException(); 603 } catch (PasswordLoginForbiddenException $ex) { 604 // Nothing to do 605 } 606 } 607 return false; 608 } 609 610 /** 611 * Log an user in via login name and password 612 * 613 * @param string $uid 614 * @param string $password 615 * @return boolean 616 * @throws LoginException if an app canceld the login process or the user is not enabled 617 */ 618 private function loginWithPassword($uid, $password) { 619 $user = $this->manager->checkPasswordNoLogging($uid, $password); 620 if ($user === false) { 621 // Password check failed 622 return false; 623 } 624 625 return $this->completeLogin($user, ['loginName' => $uid, 'password' => $password], false); 626 } 627 628 /** 629 * Log an user in with a given token (id) 630 * 631 * @param string $token 632 * @return boolean 633 * @throws LoginException if an app canceled the login process or the user is not enabled 634 */ 635 private function loginWithToken($token) { 636 try { 637 $dbToken = $this->tokenProvider->getToken($token); 638 } catch (InvalidTokenException $ex) { 639 return false; 640 } 641 $uid = $dbToken->getUID(); 642 643 // When logging in with token, the password must be decrypted first before passing to login hook 644 $password = ''; 645 try { 646 $password = $this->tokenProvider->getPassword($dbToken, $token); 647 } catch (PasswordlessTokenException $ex) { 648 // Ignore and use empty string instead 649 } 650 651 $this->manager->emit('\OC\User', 'preLogin', [$dbToken->getLoginName(), $password]); 652 653 $user = $this->manager->get($uid); 654 if (is_null($user)) { 655 // user does not exist 656 return false; 657 } 658 659 return $this->completeLogin( 660 $user, 661 [ 662 'loginName' => $dbToken->getLoginName(), 663 'password' => $password, 664 'token' => $dbToken 665 ], 666 false); 667 } 668 669 /** 670 * Create a new session token for the given user credentials 671 * 672 * @param IRequest $request 673 * @param string $uid user UID 674 * @param string $loginName login name 675 * @param string $password 676 * @param int $remember 677 * @return boolean 678 */ 679 public function createSessionToken(IRequest $request, $uid, $loginName, $password = null, $remember = IToken::DO_NOT_REMEMBER) { 680 if (is_null($this->manager->get($uid))) { 681 // User does not exist 682 return false; 683 } 684 $name = isset($request->server['HTTP_USER_AGENT']) ? $request->server['HTTP_USER_AGENT'] : 'unknown browser'; 685 try { 686 $sessionId = $this->session->getId(); 687 $pwd = $this->getPassword($password); 688 // Make sure the current sessionId has no leftover tokens 689 $this->tokenProvider->invalidateToken($sessionId); 690 $this->tokenProvider->generateToken($sessionId, $uid, $loginName, $pwd, $name, IToken::TEMPORARY_TOKEN, $remember); 691 return true; 692 } catch (SessionNotAvailableException $ex) { 693 // This can happen with OCC, where a memory session is used 694 // if a memory session is used, we shouldn't create a session token anyway 695 return false; 696 } 697 } 698 699 /** 700 * Checks if the given password is a token. 701 * If yes, the password is extracted from the token. 702 * If no, the same password is returned. 703 * 704 * @param string $password either the login password or a device token 705 * @return string|null the password or null if none was set in the token 706 */ 707 private function getPassword($password) { 708 if (is_null($password)) { 709 // This is surely no token ;-) 710 return null; 711 } 712 try { 713 $token = $this->tokenProvider->getToken($password); 714 try { 715 return $this->tokenProvider->getPassword($token, $password); 716 } catch (PasswordlessTokenException $ex) { 717 return null; 718 } 719 } catch (InvalidTokenException $ex) { 720 return $password; 721 } 722 } 723 724 /** 725 * @param IToken $dbToken 726 * @param string $token 727 * @return boolean 728 */ 729 private function checkTokenCredentials(IToken $dbToken, $token) { 730 // Check whether login credentials are still valid and the user was not disabled 731 // This check is performed each 5 minutes 732 $lastCheck = $dbToken->getLastCheck() ? : 0; 733 $now = $this->timeFactory->getTime(); 734 if ($lastCheck > ($now - 60 * 5)) { 735 // Checked performed recently, nothing to do now 736 return true; 737 } 738 739 try { 740 $pwd = $this->tokenProvider->getPassword($dbToken, $token); 741 } catch (InvalidTokenException $ex) { 742 // An invalid token password was used -> log user out 743 return false; 744 } catch (PasswordlessTokenException $ex) { 745 // Token has no password 746 747 if (!is_null($this->activeUser) && !$this->activeUser->isEnabled()) { 748 $this->tokenProvider->invalidateToken($token); 749 return false; 750 } 751 752 $dbToken->setLastCheck($now); 753 $this->tokenProvider->updateToken($dbToken); 754 return true; 755 } 756 757 // Invalidate token if the user is no longer active 758 if (!is_null($this->activeUser) && !$this->activeUser->isEnabled()) { 759 $this->tokenProvider->invalidateToken($token); 760 return false; 761 } 762 763 // If the token password is no longer valid mark it as such 764 if ($this->manager->checkPassword($dbToken->getLoginName(), $pwd) === false) { 765 $this->tokenProvider->markPasswordInvalid($dbToken, $token); 766 // User is logged out 767 return false; 768 } 769 770 $dbToken->setLastCheck($now); 771 $this->tokenProvider->updateToken($dbToken); 772 return true; 773 } 774 775 /** 776 * Check if the given token exists and performs password/user-enabled checks 777 * 778 * Invalidates the token if checks fail 779 * 780 * @param string $token 781 * @param string $user login name 782 * @return boolean 783 */ 784 private function validateToken($token, $user = null) { 785 try { 786 $dbToken = $this->tokenProvider->getToken($token); 787 } catch (InvalidTokenException $ex) { 788 return false; 789 } 790 791 // Check if login names match 792 if (!is_null($user) && $dbToken->getLoginName() !== $user) { 793 // TODO: this makes it impossible to use different login names on browser and client 794 // e.g. login by e-mail 'user@example.com' on browser for generating the token will not 795 // allow to use the client token with the login name 'user'. 796 $this->logger->error('App token login name does not match', [ 797 'tokenLoginName' => $dbToken->getLoginName(), 798 'sessionLoginName' => $user, 799 ]); 800 801 return false; 802 } 803 804 if (!$this->checkTokenCredentials($dbToken, $token)) { 805 return false; 806 } 807 808 // Update token scope 809 $this->lockdownManager->setToken($dbToken); 810 811 $this->tokenProvider->updateTokenActivity($dbToken); 812 813 return true; 814 } 815 816 /** 817 * Tries to login the user with auth token header 818 * 819 * @param IRequest $request 820 * @todo check remember me cookie 821 * @return boolean 822 */ 823 public function tryTokenLogin(IRequest $request) { 824 $authHeader = $request->getHeader('Authorization'); 825 if (strpos($authHeader, 'Bearer ') === 0) { 826 $token = substr($authHeader, 7); 827 } else { 828 // No auth header, let's try session id 829 try { 830 $token = $this->session->getId(); 831 } catch (SessionNotAvailableException $ex) { 832 return false; 833 } 834 } 835 836 if (!$this->loginWithToken($token)) { 837 return false; 838 } 839 if (!$this->validateToken($token)) { 840 return false; 841 } 842 843 try { 844 $dbToken = $this->tokenProvider->getToken($token); 845 } catch (InvalidTokenException $e) { 846 // Can't really happen but better save than sorry 847 return true; 848 } 849 850 // Remember me tokens are not app_passwords 851 if ($dbToken->getRemember() === IToken::DO_NOT_REMEMBER) { 852 // Set the session variable so we know this is an app password 853 $this->session->set('app_password', $token); 854 } 855 856 return true; 857 } 858 859 /** 860 * perform login using the magic cookie (remember login) 861 * 862 * @param string $uid the username 863 * @param string $currentToken 864 * @param string $oldSessionId 865 * @return bool 866 */ 867 public function loginWithCookie($uid, $currentToken, $oldSessionId) { 868 $this->session->regenerateId(); 869 $this->manager->emit('\OC\User', 'preRememberedLogin', [$uid]); 870 $user = $this->manager->get($uid); 871 if (is_null($user)) { 872 // user does not exist 873 return false; 874 } 875 876 // get stored tokens 877 $tokens = $this->config->getUserKeys($uid, 'login_token'); 878 // test cookies token against stored tokens 879 if (!in_array($currentToken, $tokens, true)) { 880 return false; 881 } 882 // replace successfully used token with a new one 883 $this->config->deleteUserValue($uid, 'login_token', $currentToken); 884 $newToken = $this->random->generate(32); 885 $this->config->setUserValue($uid, 'login_token', $newToken, $this->timeFactory->getTime()); 886 887 try { 888 $sessionId = $this->session->getId(); 889 $token = $this->tokenProvider->renewSessionToken($oldSessionId, $sessionId); 890 } catch (SessionNotAvailableException $ex) { 891 return false; 892 } catch (InvalidTokenException $ex) { 893 \OC::$server->getLogger()->warning('Renewing session token failed', ['app' => 'core']); 894 return false; 895 } 896 897 $this->setMagicInCookie($user->getUID(), $newToken); 898 899 //login 900 $this->setUser($user); 901 $this->setLoginName($token->getLoginName()); 902 $this->setToken($token->getId()); 903 $this->lockdownManager->setToken($token); 904 $user->updateLastLoginTimestamp(); 905 $password = null; 906 try { 907 $password = $this->tokenProvider->getPassword($token, $sessionId); 908 } catch (PasswordlessTokenException $ex) { 909 // Ignore 910 } 911 $this->manager->emit('\OC\User', 'postRememberedLogin', [$user, $password]); 912 return true; 913 } 914 915 /** 916 * @param IUser $user 917 */ 918 public function createRememberMeToken(IUser $user) { 919 $token = $this->random->generate(32); 920 $this->config->setUserValue($user->getUID(), 'login_token', $token, $this->timeFactory->getTime()); 921 $this->setMagicInCookie($user->getUID(), $token); 922 } 923 924 /** 925 * logout the user from the session 926 */ 927 public function logout() { 928 $user = $this->getUser(); 929 $this->manager->emit('\OC\User', 'logout', [$user]); 930 if ($user !== null) { 931 try { 932 $this->tokenProvider->invalidateToken($this->session->getId()); 933 } catch (SessionNotAvailableException $ex) { 934 } 935 } 936 $this->setUser(null); 937 $this->setLoginName(null); 938 $this->setToken(null); 939 $this->unsetMagicInCookie(); 940 $this->session->clear(); 941 $this->manager->emit('\OC\User', 'postLogout', [$user]); 942 } 943 944 /** 945 * Set cookie value to use in next page load 946 * 947 * @param string $username username to be set 948 * @param string $token 949 */ 950 public function setMagicInCookie($username, $token) { 951 $secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https'; 952 $webRoot = \OC::$WEBROOT; 953 if ($webRoot === '') { 954 $webRoot = '/'; 955 } 956 957 $maxAge = $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); 958 \OC\Http\CookieHelper::setCookie( 959 'nc_username', 960 $username, 961 $maxAge, 962 $webRoot, 963 '', 964 $secureCookie, 965 true, 966 \OC\Http\CookieHelper::SAMESITE_LAX 967 ); 968 \OC\Http\CookieHelper::setCookie( 969 'nc_token', 970 $token, 971 $maxAge, 972 $webRoot, 973 '', 974 $secureCookie, 975 true, 976 \OC\Http\CookieHelper::SAMESITE_LAX 977 ); 978 try { 979 \OC\Http\CookieHelper::setCookie( 980 'nc_session_id', 981 $this->session->getId(), 982 $maxAge, 983 $webRoot, 984 '', 985 $secureCookie, 986 true, 987 \OC\Http\CookieHelper::SAMESITE_LAX 988 ); 989 } catch (SessionNotAvailableException $ex) { 990 // ignore 991 } 992 } 993 994 /** 995 * Remove cookie for "remember username" 996 */ 997 public function unsetMagicInCookie() { 998 //TODO: DI for cookies and IRequest 999 $secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https'; 1000 1001 unset($_COOKIE['nc_username']); //TODO: DI 1002 unset($_COOKIE['nc_token']); 1003 unset($_COOKIE['nc_session_id']); 1004 setcookie('nc_username', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true); 1005 setcookie('nc_token', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true); 1006 setcookie('nc_session_id', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true); 1007 // old cookies might be stored under /webroot/ instead of /webroot 1008 // and Firefox doesn't like it! 1009 setcookie('nc_username', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true); 1010 setcookie('nc_token', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true); 1011 setcookie('nc_session_id', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true); 1012 } 1013 1014 /** 1015 * Update password of the browser session token if there is one 1016 * 1017 * @param string $password 1018 */ 1019 public function updateSessionTokenPassword($password) { 1020 try { 1021 $sessionId = $this->session->getId(); 1022 $token = $this->tokenProvider->getToken($sessionId); 1023 $this->tokenProvider->setPassword($token, $sessionId, $password); 1024 } catch (SessionNotAvailableException $ex) { 1025 // Nothing to do 1026 } catch (InvalidTokenException $ex) { 1027 // Nothing to do 1028 } 1029 } 1030 1031 public function updateTokens(string $uid, string $password) { 1032 $this->tokenProvider->updatePasswords($uid, $password); 1033 } 1034} 1035