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
8 years ago
8 years ago
8 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
10 years ago
10 years ago
10 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  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\HintException;
  34. use \OCP\AppFramework\Controller;
  35. use OCP\AppFramework\Http\JSONResponse;
  36. use \OCP\AppFramework\Http\TemplateResponse;
  37. use OCP\AppFramework\Utility\ITimeFactory;
  38. use OCP\Defaults;
  39. use OCP\Encryption\IEncryptionModule;
  40. use OCP\Encryption\IManager;
  41. use OCP\ILogger;
  42. use \OCP\IURLGenerator;
  43. use \OCP\IRequest;
  44. use \OCP\IL10N;
  45. use \OCP\IConfig;
  46. use OCP\IUser;
  47. use OCP\IUserManager;
  48. use OCP\Mail\IMailer;
  49. use OCP\Security\ICrypto;
  50. use OCP\Security\ISecureRandom;
  51. /**
  52. * Class LostController
  53. *
  54. * Successfully changing a password will emit the post_passwordReset hook.
  55. *
  56. * @package OC\Core\Controller
  57. */
  58. class LostController extends Controller {
  59. /** @var IURLGenerator */
  60. protected $urlGenerator;
  61. /** @var IUserManager */
  62. protected $userManager;
  63. /** @var Defaults */
  64. protected $defaults;
  65. /** @var IL10N */
  66. protected $l10n;
  67. /** @var string */
  68. protected $from;
  69. /** @var IManager */
  70. protected $encryptionManager;
  71. /** @var IConfig */
  72. protected $config;
  73. /** @var ISecureRandom */
  74. protected $secureRandom;
  75. /** @var IMailer */
  76. protected $mailer;
  77. /** @var ITimeFactory */
  78. protected $timeFactory;
  79. /** @var ICrypto */
  80. protected $crypto;
  81. /** @var ILogger */
  82. private $logger;
  83. /** @var Manager */
  84. private $twoFactorManager;
  85. /**
  86. * @param string $appName
  87. * @param IRequest $request
  88. * @param IURLGenerator $urlGenerator
  89. * @param IUserManager $userManager
  90. * @param Defaults $defaults
  91. * @param IL10N $l10n
  92. * @param IConfig $config
  93. * @param ISecureRandom $secureRandom
  94. * @param string $defaultMailAddress
  95. * @param IManager $encryptionManager
  96. * @param IMailer $mailer
  97. * @param ITimeFactory $timeFactory
  98. * @param ICrypto $crypto
  99. */
  100. public function __construct($appName,
  101. IRequest $request,
  102. IURLGenerator $urlGenerator,
  103. IUserManager $userManager,
  104. Defaults $defaults,
  105. IL10N $l10n,
  106. IConfig $config,
  107. ISecureRandom $secureRandom,
  108. $defaultMailAddress,
  109. IManager $encryptionManager,
  110. IMailer $mailer,
  111. ITimeFactory $timeFactory,
  112. ICrypto $crypto,
  113. ILogger $logger,
  114. Manager $twoFactorManager) {
  115. parent::__construct($appName, $request);
  116. $this->urlGenerator = $urlGenerator;
  117. $this->userManager = $userManager;
  118. $this->defaults = $defaults;
  119. $this->l10n = $l10n;
  120. $this->secureRandom = $secureRandom;
  121. $this->from = $defaultMailAddress;
  122. $this->encryptionManager = $encryptionManager;
  123. $this->config = $config;
  124. $this->mailer = $mailer;
  125. $this->timeFactory = $timeFactory;
  126. $this->crypto = $crypto;
  127. $this->logger = $logger;
  128. $this->twoFactorManager = $twoFactorManager;
  129. }
  130. /**
  131. * Someone wants to reset their password:
  132. *
  133. * @PublicPage
  134. * @NoCSRFRequired
  135. *
  136. * @param string $token
  137. * @param string $userId
  138. * @return TemplateResponse
  139. */
  140. public function resetform($token, $userId) {
  141. if ($this->config->getSystemValue('lost_password_link', '') !== '') {
  142. return new TemplateResponse('core', 'error', [
  143. 'errors' => [['error' => $this->l10n->t('Password reset is disabled')]]
  144. ],
  145. 'guest'
  146. );
  147. }
  148. try {
  149. $this->checkPasswordResetToken($token, $userId);
  150. } catch (\Exception $e) {
  151. return new TemplateResponse(
  152. 'core', 'error', [
  153. "errors" => array(array("error" => $e->getMessage()))
  154. ],
  155. 'guest'
  156. );
  157. }
  158. return new TemplateResponse(
  159. 'core',
  160. 'lostpassword/resetpassword',
  161. array(
  162. 'link' => $this->urlGenerator->linkToRouteAbsolute('core.lost.setPassword', array('userId' => $userId, 'token' => $token)),
  163. ),
  164. 'guest'
  165. );
  166. }
  167. /**
  168. * @param string $token
  169. * @param string $userId
  170. * @throws \Exception
  171. */
  172. protected function checkPasswordResetToken($token, $userId) {
  173. $user = $this->userManager->get($userId);
  174. if($user === null || !$user->isEnabled()) {
  175. throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
  176. }
  177. try {
  178. $encryptedToken = $this->config->getUserValue($userId, 'core', 'lostpassword', null);
  179. $mailAddress = !is_null($user->getEMailAddress()) ? $user->getEMailAddress() : '';
  180. $decryptedToken = $this->crypto->decrypt($encryptedToken, $mailAddress.$this->config->getSystemValue('secret'));
  181. } catch (\Exception $e) {
  182. throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
  183. }
  184. $splittedToken = explode(':', $decryptedToken);
  185. if(count($splittedToken) !== 2) {
  186. throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
  187. }
  188. if ($splittedToken[0] < ($this->timeFactory->getTime() - 60*60*24*7) ||
  189. $user->getLastLogin() > $splittedToken[0]) {
  190. throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is expired'));
  191. }
  192. if (!hash_equals($splittedToken[1], $token)) {
  193. throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid'));
  194. }
  195. }
  196. /**
  197. * @param $message
  198. * @param array $additional
  199. * @return array
  200. */
  201. private function error($message, array $additional=array()) {
  202. return array_merge(array('status' => 'error', 'msg' => $message), $additional);
  203. }
  204. /**
  205. * @param array $data
  206. * @return array
  207. */
  208. private function success($data = []) {
  209. return array_merge($data, ['status'=>'success']);
  210. }
  211. /**
  212. * @PublicPage
  213. * @BruteForceProtection(action=passwordResetEmail)
  214. * @AnonRateThrottle(limit=10, period=300)
  215. *
  216. * @param string $user
  217. * @return JSONResponse
  218. */
  219. public function email($user){
  220. if ($this->config->getSystemValue('lost_password_link', '') !== '') {
  221. return new JSONResponse($this->error($this->l10n->t('Password reset is disabled')));
  222. }
  223. \OCP\Util::emitHook(
  224. '\OCA\Files_Sharing\API\Server2Server',
  225. 'preLoginNameUsedAsUserName',
  226. ['uid' => &$user]
  227. );
  228. // FIXME: use HTTP error codes
  229. try {
  230. $this->sendEmail($user);
  231. } catch (\Exception $e) {
  232. // Ignore the error since we do not want to leak this info
  233. $this->logger->logException($e, [
  234. 'level' => ILogger::WARN
  235. ]);
  236. }
  237. $response = new JSONResponse($this->success());
  238. $response->throttle();
  239. return $response;
  240. }
  241. /**
  242. * @PublicPage
  243. * @param string $token
  244. * @param string $userId
  245. * @param string $password
  246. * @param boolean $proceed
  247. * @return array
  248. */
  249. public function setPassword($token, $userId, $password, $proceed) {
  250. if ($this->config->getSystemValue('lost_password_link', '') !== '') {
  251. return $this->error($this->l10n->t('Password reset is disabled'));
  252. }
  253. if ($this->encryptionManager->isEnabled() && !$proceed) {
  254. $encryptionModules = $this->encryptionManager->getEncryptionModules();
  255. foreach ($encryptionModules as $module) {
  256. /** @var IEncryptionModule $instance */
  257. $instance = call_user_func($module['callback']);
  258. // this way we can find out whether per-user keys are used or a system wide encryption key
  259. if ($instance->needDetailedAccessList()) {
  260. return $this->error('', array('encryption' => true));
  261. }
  262. }
  263. }
  264. try {
  265. $this->checkPasswordResetToken($token, $userId);
  266. $user = $this->userManager->get($userId);
  267. \OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'pre_passwordReset', array('uid' => $userId, 'password' => $password));
  268. if (!$user->setPassword($password)) {
  269. throw new \Exception();
  270. }
  271. \OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'post_passwordReset', array('uid' => $userId, 'password' => $password));
  272. $this->twoFactorManager->clearTwoFactorPending($userId);
  273. $this->config->deleteUserValue($userId, 'core', 'lostpassword');
  274. @\OC::$server->getUserSession()->unsetMagicInCookie();
  275. } catch (HintException $e){
  276. return $this->error($e->getHint());
  277. } catch (\Exception $e){
  278. return $this->error($e->getMessage());
  279. }
  280. return $this->success(['user' => $userId]);
  281. }
  282. /**
  283. * @param string $input
  284. * @throws \Exception
  285. */
  286. protected function sendEmail($input) {
  287. $user = $this->findUserByIdOrMail($input);
  288. $email = $user->getEMailAddress();
  289. if (empty($email)) {
  290. throw new \Exception(
  291. $this->l10n->t('Could not send reset email because there is no email address for this username. Please contact your administrator.')
  292. );
  293. }
  294. // Generate the token. It is stored encrypted in the database with the
  295. // secret being the users' email address appended with the system secret.
  296. // This makes the token automatically invalidate once the user changes
  297. // their email address.
  298. $token = $this->secureRandom->generate(
  299. 21,
  300. ISecureRandom::CHAR_DIGITS.
  301. ISecureRandom::CHAR_LOWER.
  302. ISecureRandom::CHAR_UPPER
  303. );
  304. $tokenValue = $this->timeFactory->getTime() .':'. $token;
  305. $encryptedValue = $this->crypto->encrypt($tokenValue, $email . $this->config->getSystemValue('secret'));
  306. $this->config->setUserValue($user->getUID(), 'core', 'lostpassword', $encryptedValue);
  307. $link = $this->urlGenerator->linkToRouteAbsolute('core.lost.resetform', array('userId' => $user->getUID(), 'token' => $token));
  308. $emailTemplate = $this->mailer->createEMailTemplate('core.ResetPassword', [
  309. 'link' => $link,
  310. ]);
  311. $emailTemplate->setSubject($this->l10n->t('%s password reset', [$this->defaults->getName()]));
  312. $emailTemplate->addHeader();
  313. $emailTemplate->addHeading($this->l10n->t('Password reset'));
  314. $emailTemplate->addBodyText(
  315. htmlspecialchars($this->l10n->t('Click the following button to reset your password. If you have not requested the password reset, then ignore this email.')),
  316. $this->l10n->t('Click the following link to reset your password. If you have not requested the password reset, then ignore this email.')
  317. );
  318. $emailTemplate->addBodyButton(
  319. htmlspecialchars($this->l10n->t('Reset your password')),
  320. $link,
  321. false
  322. );
  323. $emailTemplate->addFooter();
  324. try {
  325. $message = $this->mailer->createMessage();
  326. $message->setTo([$email => $user->getUID()]);
  327. $message->setFrom([$this->from => $this->defaults->getName()]);
  328. $message->useTemplate($emailTemplate);
  329. $this->mailer->send($message);
  330. } catch (\Exception $e) {
  331. throw new \Exception($this->l10n->t(
  332. 'Couldn\'t send reset email. Please contact your administrator.'
  333. ));
  334. }
  335. }
  336. /**
  337. * @param string $input
  338. * @return IUser
  339. * @throws \InvalidArgumentException
  340. */
  341. protected function findUserByIdOrMail($input) {
  342. $userNotFound = new \InvalidArgumentException(
  343. $this->l10n->t('Couldn\'t send reset email. Please make sure your username is correct.')
  344. );
  345. $user = $this->userManager->get($input);
  346. if ($user instanceof IUser) {
  347. if (!$user->isEnabled()) {
  348. throw $userNotFound;
  349. }
  350. return $user;
  351. }
  352. $users = \array_filter($this->userManager->getByEmail($input), function (IUser $user) {
  353. return $user->isEnabled();
  354. });
  355. if (\count($users) === 1) {
  356. return $users[0];
  357. }
  358. throw $userNotFound;
  359. }
  360. }