Login.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. <?php
  2. /**
  3. * @package Grav\Plugin\Login
  4. *
  5. * @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Plugin\Login;
  9. use Birke\Rememberme\Cookie;
  10. use Grav\Common\Config\Config;
  11. use Grav\Common\Data\Data;
  12. use Grav\Common\Grav;
  13. use Grav\Common\Language\Language;
  14. use Grav\Common\Page\Interfaces\PageInterface;
  15. use Grav\Common\Session;
  16. use Grav\Common\User\Interfaces\UserCollectionInterface;
  17. use Grav\Common\User\Interfaces\UserInterface;
  18. use Grav\Common\Uri;
  19. use Grav\Common\Utils;
  20. use Grav\Plugin\Email\Utils as EmailUtils;
  21. use Grav\Plugin\Login\Events\UserLoginEvent;
  22. use Grav\Plugin\Login\RememberMe\RememberMe;
  23. use Grav\Plugin\Login\RememberMe\TokenStorage;
  24. use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
  25. /**
  26. * Class Login
  27. * @package Grav\Plugin
  28. */
  29. class Login
  30. {
  31. /** @var Grav */
  32. protected $grav;
  33. /** @var Config */
  34. protected $config;
  35. /** @var Language $language */
  36. protected $language;
  37. /** @var Session */
  38. protected $session;
  39. /** @var Uri */
  40. protected $uri;
  41. /** @var RememberMe */
  42. protected $rememberMe;
  43. /** @var TwoFactorAuth */
  44. protected $twoFa;
  45. /** @var RateLimiter[] */
  46. protected $rateLimiters = [];
  47. /** @var array */
  48. protected $provider_login_templates = [];
  49. /**
  50. * Login constructor.
  51. *
  52. * @param Grav $grav
  53. */
  54. public function __construct(Grav $grav)
  55. {
  56. $this->grav = $grav;
  57. $this->config = $this->grav['config'];
  58. $this->language = $this->grav['language'];
  59. $this->session = $this->grav['session'];
  60. $this->uri = $this->grav['uri'];
  61. }
  62. /**
  63. * Login user.
  64. *
  65. * @param array $credentials Login credentials, eg: ['username' => '', 'password' => '']
  66. * @param array $options Login options, eg: ['remember_me' => true]
  67. * @param array $extra Example: ['authorize' => 'site.login', 'user' => null], undefined variables get set.
  68. * @return UserInterface|UserLoginEvent Returns event if $extra['return_event'] is true.
  69. */
  70. public function login(array $credentials, array $options = [], array $extra = [])
  71. {
  72. $grav = Grav::instance();
  73. $eventOptions = [
  74. 'credentials' => $credentials,
  75. 'options' => $options
  76. ] + $extra;
  77. // Attempt to authenticate the user.
  78. $event = new UserLoginEvent($eventOptions);
  79. $grav->fireEvent('onUserLoginAuthenticate', $event);
  80. if ($event->isSuccess()) {
  81. // Make sure that event didn't mess up with the user authorization.
  82. $user = $event->getUser();
  83. $user->authenticated = true;
  84. $user->authorized = false;
  85. // Allow plugins to prevent login after successful authentication.
  86. $event = new UserLoginEvent($event->toArray());
  87. $grav->fireEvent('onUserLoginAuthorize', $event);
  88. }
  89. if ($event->isSuccess()) {
  90. // User has been logged in, let plugins know.
  91. $event = new UserLoginEvent($event->toArray());
  92. $grav->fireEvent('onUserLogin', $event);
  93. // Make sure that event didn't mess up with the user authorization.
  94. $user = $event->getUser();
  95. $user->authenticated = true;
  96. $user->authorized = !$event->isDelayed();
  97. } else {
  98. // Allow plugins to log errors or do other tasks on failure.
  99. $event = new UserLoginEvent($event->toArray());
  100. $grav->fireEvent('onUserLoginFailure', $event);
  101. // Make sure that event didn't mess up with the user authorization.
  102. $user = $event->getUser();
  103. $user->authenticated = false;
  104. $user->authorized = false;
  105. }
  106. $user = $event->getUser();
  107. $user->def('language', 'en');
  108. return !empty($event['return_event']) ? $event : $user;
  109. }
  110. /**
  111. * Logout user.
  112. *
  113. * @param array $options
  114. * @param array|UserInterface $extra Array of: ['user' => $user, ...] or UserInterface object (deprecated).
  115. * @return UserInterface|UserLoginEvent Returns event if $extra['return_event'] is true.
  116. */
  117. public function logout(array $options = [], $extra = [])
  118. {
  119. $grav = Grav::instance();
  120. if ($extra instanceof UserInterface) {
  121. $extra = ['user' => $extra];
  122. } elseif (isset($extra['user'])) {
  123. $extra['user'] = $grav['user'];
  124. }
  125. $eventOptions = [
  126. 'options' => $options
  127. ] + $extra;
  128. $event = new UserLoginEvent($eventOptions);
  129. // Logout the user.
  130. $grav->fireEvent('onUserLogout', $event);
  131. $user = $event->getUser();
  132. $user->authenticated = false;
  133. $user->authorized = false;
  134. return !empty($event['return_event']) ? $event : $user;
  135. }
  136. /**
  137. * Authenticate user.
  138. *
  139. * @param array $credentials Form fields.
  140. * @param array $options
  141. *
  142. * @return bool
  143. * @deprecated Uses the Controller::taskLogin() event
  144. */
  145. public function authenticate($credentials, $options = ['remember_me' => true])
  146. {
  147. $event = $this->login($credentials, $options, ['return_event' => true]);
  148. $user = $event['user'];
  149. $redirect = $event->getRedirect();
  150. $message = $event->getMessage();
  151. $messageType = $event->getMessageType();
  152. if ($user->authenticated && $user->authorized) {
  153. if (!$message) {
  154. $message = 'PLUGIN_LOGIN.LOGIN_SUCCESSFUL';
  155. $messageType = 'info';
  156. }
  157. if (!$redirect) {
  158. $redirect = $this->uri->route();
  159. }
  160. }
  161. if ($message) {
  162. $this->grav['messages']->add($this->language->translate($message, [$user->language]), $messageType);
  163. }
  164. if ($redirect) {
  165. $this->grav->redirectLangSafe($redirect, $event->getRedirectCode());
  166. }
  167. return $user->authenticated && $user->authorized;
  168. }
  169. /**
  170. * Create a new user file
  171. *
  172. * @param array $data
  173. * @param array $files
  174. *
  175. * @return UserInterface
  176. */
  177. public function register(array $data, array $files = [])
  178. {
  179. if (!isset($data['groups'])) {
  180. //Add new user ACL settings
  181. $groups = (array) $this->config->get('plugins.login.user_registration.groups', []);
  182. if (\count($groups) > 0) {
  183. $data['groups'] = $groups;
  184. }
  185. }
  186. if (!isset($data['access'])) {
  187. $access = (array) $this->config->get('plugins.login.user_registration.access.site', []);
  188. if (\count($access) > 0) {
  189. $data['access']['site'] = $access;
  190. }
  191. }
  192. $username = $this->validateField('username', $data['username']);
  193. /** @var UserCollectionInterface $users */
  194. $users = $this->grav['accounts'];
  195. // Create user object and save it
  196. $user = $users->load($username);
  197. if ($user->exists()) {
  198. throw new \RuntimeException('User ' . $username . ' cannot be registered: user already exists!');
  199. }
  200. $user->update($data, $files);
  201. if (isset($data['groups'])) {
  202. $user->groups = $data['groups'];
  203. }
  204. if (isset($data['access'])) {
  205. $user->access = $data['access'];
  206. }
  207. $user->save();
  208. return $user;
  209. }
  210. /**
  211. * @param string $type
  212. * @param mixed $value
  213. * @param string $extra
  214. *
  215. * @return string
  216. */
  217. public function validateField($type, $value, $extra = '')
  218. {
  219. switch ($type) {
  220. case 'user':
  221. case 'username':
  222. /** @var Config $config */
  223. $config = Grav::instance()['config'];
  224. $username_regex = '/' . $config->get('system.username_regex') . '/';
  225. if (!\is_string($value) || !preg_match($username_regex, $value)) {
  226. throw new \RuntimeException('Username does not pass the minimum requirements');
  227. }
  228. break;
  229. case 'password1':
  230. /** @var Config $config */
  231. $config = Grav::instance()['config'];
  232. $pwd_regex = '/' . $config->get('system.pwd_regex') . '/';
  233. if (!\is_string($value) || !preg_match($pwd_regex, $value)) {
  234. throw new \RuntimeException('Password does not pass them minimum requirements');
  235. }
  236. break;
  237. case 'password2':
  238. if (!\is_string($value) || strcmp($value, $extra)) {
  239. throw new \RuntimeException('Passwords did not match.');
  240. }
  241. break;
  242. case 'email':
  243. if (!\is_string($value) || !filter_var($value, FILTER_VALIDATE_EMAIL)) {
  244. throw new \RuntimeException('Not a valid email address');
  245. }
  246. break;
  247. case 'permissions':
  248. if (!\is_string($value) || !\in_array($value, ['a', 's', 'b'], true)) {
  249. throw new \RuntimeException('Permissions ' . $value . ' are invalid.');
  250. }
  251. break;
  252. case 'fullname':
  253. if (!\is_string($value) || trim($value) === '') {
  254. throw new \RuntimeException('Fullname cannot be empty');
  255. }
  256. break;
  257. case 'state':
  258. if ($value !== 'enabled' && $value !== 'disabled') {
  259. throw new \RuntimeException('State is not valid');
  260. }
  261. break;
  262. }
  263. return $value;
  264. }
  265. /**
  266. * Handle the email to notify the user account creation to the site admin.
  267. *
  268. * @param UserInterface $user
  269. *
  270. * @return bool True if the action was performed.
  271. * @throws \RuntimeException
  272. */
  273. public function sendNotificationEmail(UserInterface $user)
  274. {
  275. if (empty($user->email)) {
  276. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD'));
  277. }
  278. $site_name = $this->config->get('site.title', 'Website');
  279. $subject = $this->language->translate(['PLUGIN_LOGIN.NOTIFICATION_EMAIL_SUBJECT', $site_name]);
  280. $content = $this->language->translate([
  281. 'PLUGIN_LOGIN.NOTIFICATION_EMAIL_BODY',
  282. $site_name,
  283. $user->username,
  284. $user->email,
  285. $this->grav['base_url_absolute'],
  286. ]);
  287. $to = $this->config->get('plugins.email.to');
  288. if (empty($to)) {
  289. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_NOT_CONFIGURED'));
  290. }
  291. $sent = EmailUtils::sendEmail($subject, $content, $to);
  292. if ($sent < 1) {
  293. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
  294. }
  295. return true;
  296. }
  297. /**
  298. * Handle the email to welcome the new user
  299. *
  300. * @param UserInterface $user
  301. *
  302. * @return bool True if the action was performed.
  303. * @throws \RuntimeException
  304. */
  305. public function sendWelcomeEmail(UserInterface $user)
  306. {
  307. if (empty($user->email)) {
  308. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD'));
  309. }
  310. $site_name = $this->config->get('site.title', 'Website');
  311. $author = $this->grav['config']->get('site.author.name', '');
  312. $fullname = $user->fullname ?: $user->username;
  313. $subject = $this->language->translate(['PLUGIN_LOGIN.WELCOME_EMAIL_SUBJECT', $site_name]);
  314. $content = $this->language->translate(['PLUGIN_LOGIN.WELCOME_EMAIL_BODY',
  315. $fullname,
  316. $this->grav['base_url_absolute'],
  317. $site_name,
  318. $author
  319. ]);
  320. $to = $user->email;
  321. $sent = EmailUtils::sendEmail($subject, $content, $to);
  322. if ($sent < 1) {
  323. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
  324. }
  325. return true;
  326. }
  327. /**
  328. * Handle the email to activate the user account.
  329. *
  330. * @param UserInterface $user
  331. *
  332. * @return bool True if the action was performed.
  333. * @throws \RuntimeException
  334. */
  335. public function sendActivationEmail(UserInterface $user)
  336. {
  337. if (empty($user->email)) {
  338. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD'));
  339. }
  340. $token = md5(uniqid(mt_rand(), true));
  341. $expire = time() + 604800; // next week
  342. $user->activation_token = $token . '::' . $expire;
  343. $user->save();
  344. $param_sep = $this->config->get('system.param_sep', ':');
  345. $activation_link = $this->grav['base_url_absolute'] . $this->config->get('plugins.login.route_activate') . '/token' . $param_sep . $token . '/username' . $param_sep . $user->username;
  346. $site_name = $this->config->get('site.title', 'Website');
  347. $author = $this->grav['config']->get('site.author.name', '');
  348. $fullname = $user->fullname ?: $user->username;
  349. $subject = $this->language->translate(['PLUGIN_LOGIN.ACTIVATION_EMAIL_SUBJECT', $site_name]);
  350. $content = $this->language->translate(['PLUGIN_LOGIN.ACTIVATION_EMAIL_BODY',
  351. $fullname,
  352. $activation_link,
  353. $site_name,
  354. $author
  355. ]);
  356. $to = $user->email;
  357. $sent = EmailUtils::sendEmail($subject, $content, $to);
  358. if ($sent < 1) {
  359. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
  360. }
  361. return true;
  362. }
  363. /**
  364. * Gets and sets the RememberMe class
  365. *
  366. * @param mixed $var A rememberMe instance to set
  367. *
  368. * @return RememberMe Returns the current rememberMe instance
  369. * @throws \InvalidArgumentException
  370. */
  371. public function rememberMe($var = null)
  372. {
  373. if ($var !== null) {
  374. $this->rememberMe = $var;
  375. }
  376. if (!$this->rememberMe) {
  377. /** @var Config $config */
  378. $config = $this->grav['config'];
  379. $cookieName = $config->get('plugins.login.rememberme.name');
  380. $timeout = $config->get('plugins.login.rememberme.timeout');
  381. // Setup storage for RememberMe cookies
  382. $storage = new TokenStorage('user-data://rememberme', $timeout);
  383. $this->rememberMe = new RememberMe($storage);
  384. $this->rememberMe->setCookieName($cookieName);
  385. $this->rememberMe->setExpireTime($timeout);
  386. // Hardening cookies with user-agent and random salt or
  387. // fallback to use system based cache key
  388. $server_agent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
  389. $data = $server_agent . $config->get('security.salt', $this->grav['cache']->getKey());
  390. $this->rememberMe->setSalt(hash('sha512', $data));
  391. // Set cookie with correct base path of Grav install
  392. $cookie = new Cookie;
  393. $cookie->setPath($this->grav['base_url_relative'] ?: '/');
  394. $this->rememberMe->setCookie($cookie);
  395. }
  396. return $this->rememberMe;
  397. }
  398. /**
  399. * Gets and sets the TwoFactorAuth object
  400. *
  401. * @param TwoFactorAuth $var
  402. * @return TwoFactorAuth
  403. * @throws \RobThree\Auth\TwoFactorAuthException
  404. */
  405. public function twoFactorAuth($var = null)
  406. {
  407. if ($var !== null) {
  408. $this->twoFa = $var;
  409. }
  410. if (!$this->twoFa) {
  411. $this->twoFa = new TwoFactorAuth;
  412. }
  413. return $this->twoFa;
  414. }
  415. /**
  416. * @param string $context
  417. * @param int $maxCount
  418. * @param int $interval
  419. * @return RateLimiter
  420. */
  421. public function getRateLimiter($context, $maxCount = null, $interval = null)
  422. {
  423. if (!isset($this->rateLimiters[$context])) {
  424. switch ($context) {
  425. case 'login_attempts':
  426. $maxCount = $this->grav['config']->get('plugins.login.max_login_count', 5);
  427. $interval = $this->grav['config']->get('plugins.login.max_login_interval', 10);
  428. break;
  429. case 'pw_resets':
  430. $maxCount = $this->grav['config']->get('plugins.login.max_pw_resets_count', 2);
  431. $interval = $this->grav['config']->get('plugins.login.max_pw_resets_interval', 60);
  432. break;
  433. }
  434. $this->rateLimiters[$context] = new RateLimiter($context, $maxCount, $interval);
  435. }
  436. return $this->rateLimiters[$context];
  437. }
  438. /**
  439. * @param UserInterface $user
  440. * @param PageInterface $page
  441. * @param Data|null $config
  442. * @return bool
  443. */
  444. public function isUserAuthorizedForPage(UserInterface $user, PageInterface $page, $config = null)
  445. {
  446. $header = $page->header();
  447. $rules = isset($header->access) ? (array)$header->access : [];
  448. if (!$rules && $config !== null && $config->get('parent_acl')) {
  449. // If page has no ACL rules, use its parent's rules
  450. $parent = $page->parent();
  451. while (!$rules and $parent) {
  452. $header = $parent->header();
  453. $rules = isset($header->access) ? (array)$header->access : [];
  454. $parent = $parent->parent();
  455. }
  456. }
  457. // Continue to the page if it has no ACL rules.
  458. if (!$rules) {
  459. return true;
  460. }
  461. if (!$user->authorized) {
  462. return false;
  463. }
  464. // Continue to the page if user is authorized to access the page.
  465. foreach ($rules as $rule => $value) {
  466. if (\is_array($value)) {
  467. foreach ($value as $nested_rule => $nested_value) {
  468. if ($user->authorize($rule . '.' . $nested_rule) === Utils::isPositive($nested_value)) {
  469. return true;
  470. }
  471. }
  472. } elseif ($user->authorize($rule) === Utils::isPositive($value)) {
  473. return true;
  474. }
  475. }
  476. return false;
  477. }
  478. /**
  479. * Check if user may use password reset functionality.
  480. *
  481. * @param UserInterface $user
  482. * @param string $field
  483. * @param int $count
  484. * @param int $interval
  485. * @return bool
  486. * @deprecated 2.5.0 Use $grav['login']->getRateLimiter($context) instead. See Grav\Plugin\Login\RateLimiter class.
  487. */
  488. public function isUserRateLimited(UserInterface $user, $field, $count, $interval)
  489. {
  490. if ($count > 0) {
  491. if (!isset($user->{$field})) {
  492. $user->{$field} = [];
  493. }
  494. //remove older than $interval x minute attempts
  495. $actual_resets = [];
  496. foreach ((array)$user->{$field} as $reset) {
  497. if ($reset > (time() - $interval * 60)) {
  498. $actual_resets[] = $reset;
  499. }
  500. }
  501. if (\count($actual_resets) >= $count) {
  502. return true;
  503. }
  504. $actual_resets[] = time(); // current reset
  505. $user->{$field} = $actual_resets;
  506. }
  507. return false;
  508. }
  509. /**
  510. * Reset the rate limit counter.
  511. *
  512. * @param UserInterface $user
  513. * @param string $field
  514. * @deprecated 2.5.0 Use $grav['login']->getRateLimiter($context) instead. See Grav\Plugin\Login\RateLimiter class.
  515. */
  516. public function resetRateLimit(UserInterface $user, $field)
  517. {
  518. $user->{$field} = [];
  519. }
  520. /**
  521. * Get Current logged in user
  522. *
  523. * @return UserInterface
  524. * @deprecated 2.5.0 Use $grav['user'] instead.
  525. */
  526. public function getUser()
  527. {
  528. /** @var UserInterface $user */
  529. return $this->grav['user'];
  530. }
  531. public function addProviderLoginTemplate($template)
  532. {
  533. $this->provider_login_templates[] = $template;
  534. }
  535. public function getProviderLoginTemplates()
  536. {
  537. return $this->provider_login_templates;
  538. }
  539. }