You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LostController.php 12KB

10 years ago
7 years ago
7 years ago
7 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Bernhard Posselt <dev@bernhard-posselt.com>
  6. * @author Bjoern Schiessle <bjoern@schiessle.org>
  7. * @author Björn Schießle <bjoern@schiessle.org>
  8. * @author Joas Schilling <coding@schilljs.com>
  9. * @author Julius Haertl <jus@bitgrid.net>
  10. * @author Lukas Reschke <lukas@statuscode.ch>
  11. * @author Morris Jobke <hey@morrisjobke.de>
  12. * @author Roeland Jago Douma <roeland@famdouma.nl>
  13. * @author Thomas Müller <thomas.mueller@tmit.eu>
  14. * @author Victor Dubiniuk <dubiniuk@owncloud.com>
  15. *
  16. * @license AGPL-3.0
  17. *
  18. * This code is free software: you can redistribute it and/or modify
  19. * it under the terms of the GNU Affero General Public License, version 3,
  20. * as published by the Free Software Foundation.
  21. *
  22. * This program is distributed in the hope that it will be useful,
  23. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  24. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  25. * GNU Affero General Public License for more details.
  26. *
  27. * You should have received a copy of the GNU Affero General Public License, version 3,
  28. * along with this program. If not, see <http://www.gnu.org/licenses/>
  29. *
  30. */
  31. namespace OC\Core\Controller;
  32. use OC\Authentication\TwoFactorAuth\Manager;
  33. use OC\Core\Exception\ResetPasswordException;
  34. use OC\HintException;
  35. use \OCP\AppFramework\Controller;
  36. use OCP\AppFramework\Http\JSONResponse;
  37. use \OCP\AppFramework\Http\TemplateResponse;
  38. use OCP\AppFramework\Utility\ITimeFactory;
  39. use OCP\Defaults;
  40. use OCP\Encryption\IEncryptionModule;
  41. use OCP\Encryption\IManager;
  42. use OCP\IInitialStateService;
  43. use OCP\ILogger;
  44. use \OCP\IURLGenerator;
  45. use \OCP\IRequest;
  46. use \OCP\IL10N;
  47. use \OCP\IConfig;
  48. use OCP\IUser;
  49. use OCP\IUserManager;
  50. use OCP\Mail\IMailer;
  51. use OCP\Security\ICrypto;
  52. use OCP\Security\ISecureRandom;
  53. use function array_filter;
  54. use function count;
  55. use function reset;
  56. /**
  57. * Class LostController
  58. *
  59. * Successfully changing a password will emit the post_passwordReset hook.
  60. *
  61. * @package OC\Core\Controller
  62. */
  63. class LostController extends Controller {
  64. /** @var IURLGenerator */
  65. protected $urlGenerator;
  66. /** @var IUserManager */
  67. protected $userManager;
  68. /** @var Defaults */
  69. protected $defaults;
  70. /** @var IL10N */
  71. protected $l10n;
  72. /** @var string */
  73. protected $from;
  74. /** @var IManager */
  75. protected $encryptionManager;
  76. /** @var IConfig */
  77. protected $config;
  78. /** @var ISecureRandom */
  79. protected $secureRandom;
  80. /** @var IMailer */
  81. protected $mailer;
  82. /** @var ITimeFactory */
  83. protected $timeFactory;
  84. /** @var ICrypto */
  85. protected $crypto;
  86. /** @var ILogger */
  87. private $logger;
  88. /** @var Manager */
  89. private $twoFactorManager;
  90. /** @var IInitialStateService */
  91. private $initialStateService;
  92. /**
  93. * @param string $appName
  94. * @param IRequest $request
  95. * @param IURLGenerator $urlGenerator
  96. * @param IUserManager $userManager
  97. * @param Defaults $defaults
  98. * @param IL10N $l10n
  99. * @param IConfig $config
  100. * @param ISecureRandom $secureRandom
  101. * @param string $defaultMailAddress
  102. * @param IManager $encryptionManager
  103. * @param IMailer $mailer
  104. * @param ITimeFactory $timeFactory
  105. * @param ICrypto $crypto
  106. */
  107. public function __construct($appName,
  108. IRequest $request,
  109. IURLGenerator $urlGenerator,
  110. IUserManager $userManager,
  111. Defaults $defaults,
  112. IL10N $l10n,
  113. IConfig $config,
  114. ISecureRandom $secureRandom,
  115. $defaultMailAddress,
  116. IManager $encryptionManager,
  117. IMailer $mailer,
  118. ITimeFactory $timeFactory,
  119. ICrypto $crypto,
  120. ILogger $logger,
  121. Manager $twoFactorManager,
  122. IInitialStateService $initialStateService) {
  123. parent::__construct($appName, $request);
  124. $this->urlGenerator = $urlGenerator;
  125. $this->userManager = $userManager;
  126. $this->defaults = $defaults;
  127. $this->l10n = $l10n;
  128. $this->secureRandom = $secureRandom;
  129. $this->from = $defaultMailAddress;
  130. $this->encryptionManager = $encryptionManager;
  131. $this->config = $config;
  132. $this->mailer = $mailer;
  133. $this->timeFactory = $timeFactory;
  134. $this->crypto = $crypto;
  135. $this->logger = $logger;
  136. $this->twoFactorManager = $twoFactorManager;
  137. $this->initialStateService = $initialStateService;
  138. }
  139. /**
  140. * Someone wants to reset their password:
  141. *
  142. * @PublicPage
  143. * @NoCSRFRequired
  144. *
  145. * @param string $token
  146. * @param string $userId
  147. * @return TemplateResponse
  148. */
  149. public function resetform($token, $userId) {
  150. if ($this->config->getSystemValue('lost_password_link', '') !== '') {
  151. return new TemplateResponse('core', 'error', [
  152. 'errors' => [['error' => $this->l10n->t('Password reset is disabled')]]
  153. ],
  154. 'guest'
  155. );
  156. }
  157. try {
  158. $this->checkPasswordResetToken($token, $userId);
  159. } catch (\Exception $e) {
  160. return new TemplateResponse(
  161. 'core', 'error', [
  162. "errors" => array(array("error" => $e->getMessage()))
  163. ],
  164. 'guest'
  165. );
  166. }
  167. $this->initialStateService->provideInitialState('core', 'resetPasswordUser', $userId);
  168. $this->initialStateService->provideInitialState('core', 'resetPasswordTarget',
  169. $this->urlGenerator->linkToRouteAbsolute('core.lost.setPassword', ['userId' => $userId, 'token' => $token])
  170. );
  171. return new TemplateResponse(
  172. 'core',
  173. 'login',
  174. [],
  175. 'guest'
  176. );
  177. }
  178. /**
  179. * @param string $token
  180. * @param string $userId
  181. * @throws \Exception
  182. */
  183. protected function checkPasswordResetToken($token, $userId) {
  184. $user = $this->userManager->get($userId);
  185. if($user === null || !$user->isEnabled()) {
  186. throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
  187. }
  188. $encryptedToken = $this->config->getUserValue($userId, 'core', 'lostpassword', null);
  189. if ($encryptedToken === null) {
  190. throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
  191. }
  192. try {
  193. $mailAddress = !is_null($user->getEMailAddress()) ? $user->getEMailAddress() : '';
  194. $decryptedToken = $this->crypto->decrypt($encryptedToken, $mailAddress.$this->config->getSystemValue('secret'));
  195. } catch (\Exception $e) {
  196. throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
  197. }
  198. $splittedToken = explode(':', $decryptedToken);
  199. if(count($splittedToken) !== 2) {
  200. throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
  201. }
  202. if ($splittedToken[0] < ($this->timeFactory->getTime() - 60*60*24*7) ||
  203. $user->getLastLogin() > $splittedToken[0]) {
  204. throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is expired'));
  205. }
  206. if (!hash_equals($splittedToken[1], $token)) {
  207. throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
  208. }
  209. }
  210. /**
  211. * @param $message
  212. * @param array $additional
  213. * @return array
  214. */
  215. private function error($message, array $additional=array()) {
  216. return array_merge(array('status' => 'error', 'msg' => $message), $additional);
  217. }
  218. /**
  219. * @param array $data
  220. * @return array
  221. */
  222. private function success($data = []) {
  223. return array_merge($data, ['status'=>'success']);
  224. }
  225. /**
  226. * @PublicPage
  227. * @BruteForceProtection(action=passwordResetEmail)
  228. * @AnonRateThrottle(limit=10, period=300)
  229. *
  230. * @param string $user
  231. * @return JSONResponse
  232. */
  233. public function email($user){
  234. if ($this->config->getSystemValue('lost_password_link', '') !== '') {
  235. return new JSONResponse($this->error($this->l10n->t('Password reset is disabled')));
  236. }
  237. \OCP\Util::emitHook(
  238. '\OCA\Files_Sharing\API\Server2Server',
  239. 'preLoginNameUsedAsUserName',
  240. ['uid' => &$user]
  241. );
  242. // FIXME: use HTTP error codes
  243. try {
  244. $this->sendEmail($user);
  245. } catch (ResetPasswordException $e) {
  246. // Ignore the error since we do not want to leak this info
  247. $this->logger->warning('Could not send password reset email: ' . $e->getMessage());
  248. } catch (\Exception $e) {
  249. $this->logger->logException($e);
  250. }
  251. $response = new JSONResponse($this->success());
  252. $response->throttle();
  253. return $response;
  254. }
  255. /**
  256. * @PublicPage
  257. * @param string $token
  258. * @param string $userId
  259. * @param string $password
  260. * @param boolean $proceed
  261. * @return array
  262. */
  263. public function setPassword($token, $userId, $password, $proceed) {
  264. if ($this->config->getSystemValue('lost_password_link', '') !== '') {
  265. return $this->error($this->l10n->t('Password reset is disabled'));
  266. }
  267. if ($this->encryptionManager->isEnabled() && !$proceed) {
  268. $encryptionModules = $this->encryptionManager->getEncryptionModules();
  269. foreach ($encryptionModules as $module) {
  270. /** @var IEncryptionModule $instance */
  271. $instance = call_user_func($module['callback']);
  272. // this way we can find out whether per-user keys are used or a system wide encryption key
  273. if ($instance->needDetailedAccessList()) {
  274. return $this->error('', array('encryption' => true));
  275. }
  276. }
  277. }
  278. try {
  279. $this->checkPasswordResetToken($token, $userId);
  280. $user = $this->userManager->get($userId);
  281. \OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'pre_passwordReset', array('uid' => $userId, 'password' => $password));
  282. if (!$user->setPassword($password)) {
  283. throw new \Exception();
  284. }
  285. \OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'post_passwordReset', array('uid' => $userId, 'password' => $password));
  286. $this->twoFactorManager->clearTwoFactorPending($userId);
  287. $this->config->deleteUserValue($userId, 'core', 'lostpassword');
  288. @\OC::$server->getUserSession()->unsetMagicInCookie();
  289. } catch (HintException $e){
  290. return $this->error($e->getHint());
  291. } catch (\Exception $e){
  292. return $this->error($e->getMessage());
  293. }
  294. return $this->success(['user' => $userId]);
  295. }
  296. /**
  297. * @param string $input
  298. * @throws ResetPasswordException
  299. * @throws \OCP\PreConditionNotMetException
  300. */
  301. protected function sendEmail($input) {
  302. $user = $this->findUserByIdOrMail($input);
  303. $email = $user->getEMailAddress();
  304. if (empty($email)) {
  305. throw new ResetPasswordException('Could not send reset e-mail since there is no email for username ' . $input);
  306. }
  307. // Generate the token. It is stored encrypted in the database with the
  308. // secret being the users' email address appended with the system secret.
  309. // This makes the token automatically invalidate once the user changes
  310. // their email address.
  311. $token = $this->secureRandom->generate(
  312. 21,
  313. ISecureRandom::CHAR_DIGITS.
  314. ISecureRandom::CHAR_LOWER.
  315. ISecureRandom::CHAR_UPPER
  316. );
  317. $tokenValue = $this->timeFactory->getTime() .':'. $token;
  318. $encryptedValue = $this->crypto->encrypt($tokenValue, $email . $this->config->getSystemValue('secret'));
  319. $this->config->setUserValue($user->getUID(), 'core', 'lostpassword', $encryptedValue);
  320. $link = $this->urlGenerator->linkToRouteAbsolute('core.lost.resetform', array('userId' => $user->getUID(), 'token' => $token));
  321. $emailTemplate = $this->mailer->createEMailTemplate('core.ResetPassword', [
  322. 'link' => $link,
  323. ]);
  324. $emailTemplate->setSubject($this->l10n->t('%s password reset', [$this->defaults->getName()]));
  325. $emailTemplate->addHeader();
  326. $emailTemplate->addHeading($this->l10n->t('Password reset'));
  327. $emailTemplate->addBodyText(
  328. htmlspecialchars($this->l10n->t('Click the following button to reset your password. If you have not requested the password reset, then ignore this email.')),
  329. $this->l10n->t('Click the following link to reset your password. If you have not requested the password reset, then ignore this email.')
  330. );
  331. $emailTemplate->addBodyButton(
  332. htmlspecialchars($this->l10n->t('Reset your password')),
  333. $link,
  334. false
  335. );
  336. $emailTemplate->addFooter();
  337. try {
  338. $message = $this->mailer->createMessage();
  339. $message->setTo([$email => $user->getUID()]);
  340. $message->setFrom([$this->from => $this->defaults->getName()]);
  341. $message->useTemplate($emailTemplate);
  342. $this->mailer->send($message);
  343. } catch (\Exception $e) {
  344. // Log the exception and continue
  345. $this->logger->logException($e);
  346. }
  347. }
  348. /**
  349. * @param string $input
  350. * @return IUser
  351. * @throws ResetPasswordException
  352. */
  353. protected function findUserByIdOrMail($input) {
  354. $user = $this->userManager->get($input);
  355. if ($user instanceof IUser) {
  356. if (!$user->isEnabled()) {
  357. throw new ResetPasswordException('User is disabled');
  358. }
  359. return $user;
  360. }
  361. $users = array_filter($this->userManager->getByEmail($input), function (IUser $user) {
  362. return $user->isEnabled();
  363. });
  364. if (count($users) === 1) {
  365. return reset($users);
  366. }
  367. throw new ResetPasswordException('Could not find user');
  368. }
  369. }