Session.php 29 KB

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