Session.php 29 KB

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