PageRenderTime 39ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Backend/Modules/Authentication/Actions/Index.php

http://github.com/forkcms/forkcms
PHP | 336 lines | 213 code | 55 blank | 68 comment | 29 complexity | fbaa82db137dba94184200f9fcb6005f MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, MIT, AGPL-3.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. namespace Backend\Modules\Authentication\Actions;
  3. use Backend\Core\Engine\Authentication as BackendAuthentication;
  4. use Backend\Core\Engine\Base\ActionIndex as BackendBaseActionIndex;
  5. use Backend\Core\Engine\Form as BackendForm;
  6. use Backend\Core\Language\Language as BL;
  7. use Backend\Core\Engine\Model as BackendModel;
  8. use Backend\Core\Engine\User;
  9. use Backend\Modules\Users\Engine\Model as BackendUsersModel;
  10. use Common\Mailer\Message;
  11. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  12. use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
  13. /**
  14. * This is the index-action (default), it will display the login screen
  15. */
  16. class Index extends BackendBaseActionIndex
  17. {
  18. /**
  19. * @var BackendForm
  20. */
  21. private $form;
  22. /**
  23. * @var BackendForm
  24. */
  25. private $formForgotPassword;
  26. public function execute(): void
  27. {
  28. // check if the user is really logged on
  29. if (BackendAuthentication::getUser()->isAuthenticated()) {
  30. $userEmail = BackendAuthentication::getUser()->getEmail();
  31. $this->getContainer()->get('logger.public')->info(
  32. "User '{$userEmail}' is already authenticated."
  33. );
  34. $this->redirectToAllowedModuleAndAction();
  35. }
  36. parent::execute();
  37. $this->buildForm();
  38. $this->validateForm();
  39. $this->parse();
  40. $this->display();
  41. }
  42. private function buildForm(): void
  43. {
  44. $this->form = new BackendForm(null, null, 'post', true, false);
  45. $this->form
  46. ->addText('backend_email')
  47. ->setAttribute('placeholder', \SpoonFilter::ucfirst(BL::lbl('Email')))
  48. ->setAttribute('type', 'email')
  49. ;
  50. $this->form
  51. ->addPassword('backend_password')
  52. ->setAttribute('placeholder', \SpoonFilter::ucfirst(BL::lbl('Password')))
  53. ;
  54. $this->formForgotPassword = new BackendForm('forgotPassword');
  55. $this->formForgotPassword->addText('backend_email_forgot');
  56. }
  57. public function parse(): void
  58. {
  59. parent::parse();
  60. // assign the interface language ourself, because it won't be assigned automagically
  61. $this->template->assign('INTERFACE_LANGUAGE', BL::getInterfaceLanguage());
  62. $this->form->parse($this->template);
  63. $this->formForgotPassword->parse($this->template);
  64. }
  65. private function validateForm(): void
  66. {
  67. if ($this->form->isSubmitted()) {
  68. $txtEmail = $this->form->getField('backend_email');
  69. $txtPassword = $this->form->getField('backend_password');
  70. // required fields
  71. if (!$txtEmail->isFilled() || !$txtPassword->isFilled()) {
  72. // add error
  73. $this->form->addError('fields required');
  74. // show error
  75. $this->template->assign('hasError', true);
  76. }
  77. $this->getContainer()->get('logger.public')->info(
  78. "Trying to authenticate user '{$txtEmail->getValue()}'."
  79. );
  80. // invalid form-token?
  81. if ($this->form->getToken() != $this->form->getField('form_token')->getValue()) {
  82. // set a correct header, so bots understand they can't mess with us.
  83. throw new BadRequestHttpException();
  84. }
  85. // get the user's id
  86. $userId = BackendUsersModel::getIdByEmail($txtEmail->getValue());
  87. // all fields are ok?
  88. if ($txtEmail->isFilled() && $txtPassword->isFilled() && $this->form->getToken() == $this->form->getField('form_token')->getValue()) {
  89. // try to login the user
  90. if (!BackendAuthentication::loginUser($txtEmail->getValue(), $txtPassword->getValue())) {
  91. $this->getContainer()->get('logger.public')->info(
  92. "Failed authenticating user '{$txtEmail->getValue()}'."
  93. );
  94. // add error
  95. $this->form->addError('invalid login');
  96. // store attempt in session
  97. $current = (int) BackendModel::getSession()->get('backend_login_attempts', 0);
  98. // increment and store
  99. BackendModel::getSession()->set('backend_login_attempts', ++$current);
  100. // save the failed login attempt in the user's settings
  101. if ($userId !== false) {
  102. BackendUsersModel::setSetting($userId, 'last_failed_login_attempt', time());
  103. }
  104. // show error
  105. $this->template->assign('hasError', true);
  106. }
  107. }
  108. // check sessions
  109. if (BackendModel::getSession()->get('backend_login_attempts', 0) >= 5) {
  110. // get previous attempt
  111. $previousAttempt = BackendModel::getSession()->get('backend_last_attempt', time());
  112. // calculate timeout
  113. $timeout = 5 * (BackendModel::getSession()->get('backend_login_attempts') - 4);
  114. // too soon!
  115. if (time() < $previousAttempt + $timeout) {
  116. // sleep until the user can login again
  117. sleep($timeout);
  118. // set a correct header, so bots understand they can't mess with us.
  119. throw new ServiceUnavailableHttpException();
  120. }
  121. // increment and store
  122. BackendModel::getSession()->set('backend_last_attempt', time());
  123. // too many attempts
  124. $this->form->addError('too many attempts');
  125. $this->getContainer()->get('logger.public')->info(
  126. "Too many login attempts for user '{$txtEmail->getValue()}'."
  127. );
  128. // show error
  129. $this->template->assign('hasTooManyAttemps', true);
  130. $this->template->assign('hasError', false);
  131. }
  132. // no errors in the form?
  133. if ($this->form->isCorrect()) {
  134. // cleanup sessions
  135. BackendModel::getSession()->remove('backend_login_attempts');
  136. BackendModel::getSession()->remove('backend_last_attempt');
  137. // save the login timestamp in the user's settings
  138. $lastLogin = BackendUsersModel::getSetting($userId, 'current_login');
  139. BackendUsersModel::setSetting($userId, 'current_login', time());
  140. if ($lastLogin) {
  141. BackendUsersModel::setSetting($userId, 'last_login', $lastLogin);
  142. }
  143. $this->getContainer()->get('logger.public')->info(
  144. "Successfully authenticated user '{$txtEmail->getValue()}'."
  145. );
  146. // redirect to the correct URL (URL the user was looking for or fallback)
  147. $this->redirectToAllowedModuleAndAction();
  148. }
  149. }
  150. // is the form submitted
  151. if ($this->formForgotPassword->isSubmitted()) {
  152. // backend email
  153. $email = $this->formForgotPassword->getField('backend_email_forgot')->getValue();
  154. // required fields
  155. if ($this->formForgotPassword->getField('backend_email_forgot')->isEmail(BL::err('EmailIsInvalid'))) {
  156. // check if there is a user with the given emailaddress
  157. if (!BackendUsersModel::existsEmail($email)) {
  158. $this->formForgotPassword->getField('backend_email_forgot')->addError(BL::err('EmailIsUnknown'));
  159. }
  160. }
  161. // no errors in the form?
  162. if ($this->formForgotPassword->isCorrect()) {
  163. // generate the key for the reset link and fetch the user ID for this email
  164. $key = BackendAuthentication::getEncryptedString($email, uniqid('', true));
  165. // insert the key and the timestamp into the user settings
  166. $userId = BackendUsersModel::getIdByEmail($email);
  167. $user = new User($userId);
  168. $user->setSetting('reset_password_key', $key);
  169. $user->setSetting('reset_password_timestamp', time());
  170. // send e-mail to user
  171. $from = $this->get('fork.settings')->get('Core', 'mailer_from');
  172. $replyTo = $this->get('fork.settings')->get('Core', 'mailer_reply_to');
  173. $message = Message::newInstance(
  174. \SpoonFilter::ucfirst(BL::msg('ResetYourPasswordMailSubject'))
  175. )
  176. ->setFrom([$from['email'] => $from['name']])
  177. ->setTo([$email])
  178. ->setReplyTo([$replyTo['email'] => $replyTo['name']])
  179. ->parseHtml(
  180. '/Authentication/Layout/Templates/Mails/ResetPassword.html.twig',
  181. [
  182. 'resetLink' => SITE_URL . BackendModel::createUrlForAction('ResetPassword')
  183. . '&email=' . $email . '&key=' . $key,
  184. ]
  185. );
  186. $this->get('mailer')->send($message);
  187. // clear post-values
  188. $_POST['backend_email_forgot'] = '';
  189. // show success message
  190. $this->template->assign('isForgotPasswordSuccess', true);
  191. // show form
  192. $this->template->assign('showForm', true);
  193. } else {
  194. // errors?
  195. $this->template->assign('showForm', true);
  196. }
  197. }
  198. }
  199. /**
  200. * Find out which module and action are allowed
  201. * and send the user on his way.
  202. */
  203. private function redirectToAllowedModuleAndAction(): void
  204. {
  205. $allowedModule = $this->getAllowedModule();
  206. $allowedAction = $this->getAllowedAction($allowedModule);
  207. $allowedModuleActionUrl = $allowedModule !== false && $allowedAction !== false ?
  208. BackendModel::createUrlForAction($allowedAction, $allowedModule) :
  209. BackendModel::createUrlForAction('Index', 'Authentication');
  210. $userEmail = BackendAuthentication::getUser()->getEmail();
  211. $this->getContainer()->get('logger.public')->info(
  212. "Redirecting user '{$userEmail}' to {$allowedModuleActionUrl}."
  213. );
  214. $this->redirect(
  215. $this->sanitizeQueryString(
  216. $this->getRequest()->query->get('querystring', $allowedModuleActionUrl),
  217. $allowedModuleActionUrl
  218. )
  219. );
  220. }
  221. /**
  222. * Run through the action of a certain module and find us an action(name) this user is allowed to access.
  223. *
  224. * @param string $module
  225. *
  226. * @return bool|string
  227. */
  228. private function getAllowedAction(string $module)
  229. {
  230. if (BackendAuthentication::isAllowedAction('Index', $module)) {
  231. return 'Index';
  232. }
  233. $allowedAction = false;
  234. $groupsRightsActions = BackendUsersModel::getModuleGroupsRightsActions(
  235. $module
  236. );
  237. foreach ($groupsRightsActions as $groupsRightsAction) {
  238. $isAllowedAction = BackendAuthentication::isAllowedAction(
  239. $groupsRightsAction['action'],
  240. $module
  241. );
  242. if ($isAllowedAction) {
  243. $allowedAction = $groupsRightsAction['action'];
  244. break;
  245. }
  246. }
  247. return $allowedAction;
  248. }
  249. /**
  250. * Run through the modules and find us a module(name) this user is allowed to access.
  251. *
  252. * @return bool|string
  253. */
  254. private function getAllowedModule()
  255. {
  256. // create filter with modules which may not be displayed
  257. $filter = ['Authentication', 'Error', 'Core'];
  258. // get all modules
  259. $modules = array_diff(BackendModel::getModules(), $filter);
  260. $allowedModule = false;
  261. if (BackendAuthentication::isAllowedModule('Dashboard')) {
  262. $allowedModule = 'Dashboard';
  263. } else {
  264. foreach ($modules as $module) {
  265. if (BackendAuthentication::isAllowedModule($module)) {
  266. $allowedModule = $module;
  267. break;
  268. }
  269. }
  270. }
  271. return $allowedModule;
  272. }
  273. private function sanitizeQueryString(string $queryString, string $default): string
  274. {
  275. // only allow internal urls starting with "\"
  276. if (!preg_match('/^\//', $queryString)) {
  277. return $default;
  278. }
  279. return $queryString;
  280. }
  281. }