diff --git a/CHANGELOG.md b/CHANGELOG.md index 4006d875..0ac5650c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,10 @@ ## mm/dd/2018 1. [](#new) - * Updated plugin dependencies (Form >=2.13.0, Login >=2.7.0, Email >=2.7.0) + * Updated plugin dependencies (Grav >= 1.4.5, Form >=2.13.0, Login >=2.7.0, Email >=2.7.0) * Updated `pagemedia` form field so it can be used with non-Page objects * Moved 2FA authentication to login plugin + * Admin login now uses login plugin events (with option `admin: true`) 1. [](#bugfix) * Fixed Firefox issue with the Regenerate button for 2FA. Forcing the page to reload * Fixed jumpiness behavior for Regenerate button when on active state. diff --git a/admin.php b/admin.php index 25a4072f..5fcf2bb8 100644 --- a/admin.php +++ b/admin.php @@ -16,6 +16,7 @@ use Grav\Plugin\Admin\Popularity; use Grav\Plugin\Admin\Themes; use Grav\Plugin\Admin\AdminController; use Grav\Plugin\Admin\Twig\AdminTwigExtension; +use Grav\Plugin\Form\Form; use Grav\Plugin\Login\Login; use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\Session\Session; @@ -177,7 +178,6 @@ class AdminPlugin extends Plugin * Process the admin registration form. * * @param Event $event - * @FIXME: login */ public function onFormProcessed(Event $event) { @@ -266,19 +266,8 @@ class AdminPlugin extends Plugin { // Only activate admin if we're inside the admin path. if ($this->active) { - // Store this version and prefer newer method - if (method_exists($this, 'getBlueprint')) { - $this->version = $this->getBlueprint()->version; - } else { - $this->version = $this->grav['plugins']->get('admin')->blueprints()->version; - } - - // Test for correct Grav 1.1 version - if (version_compare(GRAV_VERSION, '1.1.0-beta.1', '<')) { - $messages = $this->grav['messages']; - $messages->add($this->grav['language']->translate(['PLUGIN_ADMIN.NEEDS_GRAV_1_1', GRAV_VERSION]), - 'error'); - } + // Store this version. + $this->version = $this->getBlueprint()->version; // Have a unique Admin-only Cache key if (method_exists($this->grav['cache'], 'setKey')) { @@ -487,10 +476,19 @@ class AdminPlugin extends Plugin // add form if it exists in the page $header = $page->header(); - if (isset($header->form)) { - // preserve form validation - if (!isset($twig->twig_vars['form'])) { + + $forms = []; + if (isset($header->forms)) foreach ($header->forms as $key => $form) { + $forms[$key] = new Form($page, null, $form); + } + $twig->twig_vars['forms'] = $forms; + + // preserve form validation + if (!isset($twig->twig_vars['form'])) { + if (isset($header->form)) { $twig->twig_vars['form'] = new Form($page); + } elseif (isset($header->forms)) { + $twig->twig_vars['form'] = new Form($page, null, reset($header->forms)); } } diff --git a/blueprints.yaml b/blueprints.yaml index 75312afb..aa53738f 100644 --- a/blueprints.yaml +++ b/blueprints.yaml @@ -13,7 +13,7 @@ docs: https://github.com/getgrav/grav-plugin-admin/blob/develop/README.md license: MIT dependencies: - - { name: grav, version: '>=1.4.0' } + - { name: grav, version: '>=1.4.5' } - { name: form, version: '>=2.13.0' } - { name: login, version: '>=2.7.0' } - { name: email, version: '>=2.7.0' } diff --git a/classes/admin.php b/classes/admin.php index 4a0e9b27..b3951ca9 100644 --- a/classes/admin.php +++ b/classes/admin.php @@ -18,7 +18,8 @@ use Grav\Common\Uri; use Grav\Common\User\User; use Grav\Common\Utils; use Grav\Plugin\Admin\Twig\AdminTwigExtension; -use Grav\Plugin\Admin\Utils as AdminUtils; +use Grav\Plugin\Login\Login; +use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth; use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\File\File; use RocketTheme\Toolbox\File\JsonFile; @@ -349,83 +350,126 @@ class Admin /** * Authenticate user. * - * @param array $data Form data. - * @param array $post Additional form fields. - * - * @return bool - * @TODO LOGIN + * @param array $credentials User credentials. */ - public function authenticate($data, $post) + public function authenticate($credentials, $post) { - $count = $this->grav['config']->get('plugins.login.max_login_count', 5); - $interval = $this->grav['config']->get('plugins.login.max_login_interval', 10); + /** @var Login $login */ $login = $this->grav['login']; - if ($login->isUserRateLimited($this->user, 'login_attempts', $count, $interval)) { - $this->setMessage($this->translate(['PLUGIN_LOGIN.TOO_MANY_LOGIN_ATTEMPTS', $interval]), 'error'); - $this->grav->redirect($post['redirect']); - return true; + // Remove login nonce from the form. + $credentials = array_diff_key($credentials, ['admin-nonce' => true]); + $twofa = $this->grav['config']->get('plugins.admin.twofa_enabled', false); + + $rateLimiter = $login->getRateLimiter('login_attempts'); + + $userKey = isset($credentials['username']) ? (string)$credentials['username'] : ''; + $ipKey = Uri::ip(); + $redirect = isset($post['redirect']) ? $post['redirect'] : $this->uri->route(); + + // Check if the current IP has been used in failed login attempts. + $attempts = count($rateLimiter->getAttempts($ipKey, 'ip')); + + $rateLimiter->registerRateLimitedAction($ipKey, 'ip')->registerRateLimitedAction($userKey); + + // Check rate limit for both IP and user, but allow each IP a single try even if user is already rate limited. + if ($rateLimiter->isRateLimited($ipKey, 'ip') || ($attempts && $rateLimiter->isRateLimited($userKey))) { + $this->setMessage($this->translate(['PLUGIN_LOGIN.TOO_MANY_LOGIN_ATTEMPTS', $rateLimiter->getInterval()]), 'error'); + + $this->grav->redirect('/'); } + + // Fire Login process. + $event = $login->login( + $credentials, + ['admin' => true, 'twofa' => $twofa], + ['authorize' => 'admin.login', 'return_event' => true] + ); + $user = $event->getUser(); + if ($user->authenticated) { + $rateLimiter->resetRateLimit($ipKey, 'ip')->resetRateLimit($userKey); + if ($user->authorized) { + $event->defMessage('PLUGIN_ADMIN.LOGIN_LOGGED_IN', 'info'); - if (!$this->user->authenticated && isset($data['username'], $data['password'])) { - // Perform RegEX check on submitted username to check for emails - if (filter_var($data['username'], FILTER_VALIDATE_EMAIL)) { - $user = AdminUtils::findUserByEmail($data['username']); + $event->defRedirect($redirect); } else { - $user = User::load($data['username']); + $this->session->redirect = $redirect; + + $event->defRedirect($this->uri->route()); } - - //default to english if language not set - if (empty($user->language)) { - $user->set('language', 'en'); - } - - if ($user->exists()) { - - // Authenticate user. - $result = $user->authenticate($data['password']); - - if (!$result) { - return false; - } + } else { + if ($user->authorized) { + $event->defMessage('PLUGIN_LOGIN.ACCESS_DENIED', 'error'); + } else { + $event->defMessage('PLUGIN_LOGIN.LOGIN_FAILED', 'error'); } } + $event->defRedirect($this->uri->route()); - $twofa_admin_enabled = $this->grav['config']->get('plugins.admin.twofa_enabled', false); - if ($twofa_admin_enabled && isset($user->twofa_enabled) && - $user->twofa_enabled == true && !$user->authenticated) { - $this->session->redirect = $post['redirect']; - $this->session->user = $user; - - $this->grav->redirect($this->base . '/twofa'); + $message = $event->getMessage(); + if ($message) { + $this->setMessage($this->translate($message), $event->getMessageType()); } - $user->authenticated = true; - $login->resetRateLimit($user,'login_attempts'); + $redirect = $event->getRedirect(); - if ($user->authorize('admin.login')) { - $this->user = $this->session->user = $user; + $this->grav->redirect($redirect, $event->getRedirectCode()); + } - /** @var Grav $grav */ - $grav = $this->grav; + /** + * Check Two-Factor Authentication. + */ + public function twoFa($data, $post) + { + /** @var Login $login */ + $login = $this->grav['login']; - unset($this->grav['user']); - $this->grav['user'] = $user; + /** @var TwoFactorAuth $twoFa */ + $twoFa = $login->twoFactorAuth(); + $user = $this->grav['user']; - $this->setMessage($this->translate('PLUGIN_ADMIN.LOGIN_LOGGED_IN'), 'info'); - $grav->redirect($post['redirect']); + $code = isset($data['2fa_code']) ? $data['2fa_code'] : null; - return true; //never reached + $secret = isset($user->twofa_secret) ? $user->twofa_secret : null; + + if (!$code || !$secret || !$twoFa->verifyCode($secret, $code)) { + $login->logout(['admin' => true]); + + $this->grav['session']->setFlashCookieObject(Admin::TMP_COOKIE_NAME, ['message' => $this->translate('PLUGIN_ADMIN.2FA_FAILED'), 'status' => 'error']); + + $this->grav->redirect($this->uri->route(), 303); } - return false; + $this->setMessage($this->translate('PLUGIN_ADMIN.LOGIN_LOGGED_IN'), 'info'); + + $user->authorized = true; + + $this->grav->redirect($post['redirect']); + } + + /** + * Logout from admin. + */ + public function Logout($data, $post) + { + /** @var Login $login */ + $login = $this->grav['login']; + + $event = $login->logout(['admin' => true], ['return_event' => true]); + + $event->defMessage('PLUGIN_ADMIN.LOGGED_OUT', 'info'); + $message = $event->getMessage(); + if ($message) { + $this->grav['session']->setFlashCookieObject(Admin::TMP_COOKIE_NAME, ['message' => $this->translate($message), 'status' => $event->getMessageType()]); + } + + $this->grav->redirect($this->base); } /** * @return bool - * @todo LOGIN */ public static function doAnyUsersExist() { diff --git a/classes/admincontroller.php b/classes/admincontroller.php index 4577c27d..e3b1be85 100644 --- a/classes/admincontroller.php +++ b/classes/admincontroller.php @@ -124,41 +124,32 @@ class AdminController extends AdminBaseController * Handle login. * * @return bool True if the action was performed. - * @todo LOGIN */ protected function taskLogin() { - $this->data['username'] = strip_tags(strtolower($this->data['username'])); - if ($this->admin->authenticate($this->data, $this->post)) { - // should never reach here, redirects first - } else { - $this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.LOGIN_FAILED'), 'error'); - } + $this->admin->authenticate($this->data, $this->post); return true; } /** - * @return bool - * @todo LOGIN + * @return bool True if the action was performed. */ - protected function task2faverify() + protected function taskTwofa() { - /** @var TwoFactorAuth $twoFa */ - $twoFa = $this->grav['login']->twoFactorAuth(); - $user = $this->grav['user']; + $this->admin->twoFa($this->data, $this->post); - $secret = isset($user->twofa_secret) ? $user->twofa_secret : null; + return true; + } - if (!(isset($this->data['2fa_code']) && $secret && $twoFa->verifyCode($secret, $this->data['2fa_code']))) { - $this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.2FA_FAILED'), 'error'); - return true; - } - - $this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.LOGIN_LOGGED_IN'), 'info'); - - $user->authenticated = true; - $this->grav->redirect($this->post['redirect']); + /** + * Handle logout. + * + * @return bool True if the action was performed. + */ + protected function taskLogout() + { + $this->admin->logout($this->data, $this->post); return true; } @@ -166,7 +157,6 @@ class AdminController extends AdminBaseController /** * @param null $secret * @return bool - * @todo LOGIN */ public function taskRegenerate2FASecret() { @@ -204,29 +194,10 @@ class AdminController extends AdminBaseController return true; } - /** - * Handle logout. - * - * @return bool True if the action was performed. - * @todo LOGIN - */ - protected function taskLogout() - { - $message = $this->admin->translate('PLUGIN_ADMIN.LOGGED_OUT'); - - $this->admin->session()->invalidate()->start(); - $this->grav['session']->setFlashCookieObject(Admin::TMP_COOKIE_NAME, ['message' => $message, 'status' => 'info']); - - $this->setRedirect('/'); - - return true; - } - /** * Handle the reset password action. * * @return bool True if the action was performed. - * @todo LOGIN */ public function taskReset() { diff --git a/languages/en.yaml b/languages/en.yaml index eed3f167..9071a170 100644 --- a/languages/en.yaml +++ b/languages/en.yaml @@ -546,7 +546,6 @@ PLUGIN_ADMIN: FRONTMATTER_IGNORE_FIELDS: "Ignore frontmatter fields" FRONTMATTER_IGNORE_FIELDS_HELP: "Certain frontmatter fields may contain Twig but should not be processed, such as 'forms'" PACKAGE_X_INSTALLED_SUCCESSFULLY: "Package %s installed successfully" - NEEDS_GRAV_1_1: " You are running Grav v%s. You must update to the latest Grav v1.1.x release in order to ensure compatibility. This may require switching to Testing GPM releases in the System configuration." ORDERING_DISABLED_BECAUSE_PARENT_SETTING_ORDER: "Parent setting order, ordering disabled" ORDERING_DISABLED_BECAUSE_PAGE_NOT_VISIBLE: "Page is not visible, ordering disabled" ORDERING_DISABLED_BECAUSE_TOO_MANY_SIBLINGS: "Ordering via the admin is unsupported because there are more than 200 siblings" diff --git a/pages/admin/login.md b/pages/admin/login.md index 0df8cd28..f1363eed 100644 --- a/pages/admin/login.md +++ b/pages/admin/login.md @@ -1,22 +1,37 @@ --- title: Admin Login -form: - name: login +forms: + login: action: method: post fields: - username: - type: text - placeholder: PLUGIN_ADMIN.USERNAME_EMAIL - autofocus: true - validate: - required: true + username: + type: text + placeholder: PLUGIN_ADMIN.USERNAME_EMAIL + autofocus: true + validate: + required: true + + password: + type: password + placeholder: PLUGIN_ADMIN.PASSWORD + validate: + required: true - password: - type: password - placeholder: PLUGIN_ADMIN.PASSWORD - validate: - required: true + login-twofa: + action: + method: post + + fields: + 2fa_instructions: + type: display + markdown: true + content: PLUGIN_ADMIN.2FA_INSTRUCTIONS + 2fa_code: + type: text + id: twofa-code + autofocus: true + placeholder: PLUGIN_ADMIN.2FA_CODE_INPUT --- diff --git a/pages/admin/twofa.md b/pages/admin/twofa.md deleted file mode 100644 index 250ab112..00000000 --- a/pages/admin/twofa.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: 2-Factor Authentication - -form: - fields: - 2fa_instructions: - type: display - markdown: true - content: PLUGIN_ADMIN.2FA_INSTRUCTIONS - 2fa_code: - type: text - autofocus: true - placeholder: PLUGIN_ADMIN.2FA_CODE_INPUT ---- diff --git a/themes/grav/templates/login.html.twig b/themes/grav/templates/login.html.twig index 86dba798..08683480 100644 --- a/themes/grav/templates/login.html.twig +++ b/themes/grav/templates/login.html.twig @@ -1,44 +1,11 @@ -{% embed 'partials/login.html.twig' with {title:'Grav Admin Login'} %} +{% set user = grav.user %} - {% block form %} - - {% if grav.user.username and grav.user.authenticated %} - -