Skip to content

Commit

Permalink
Merge branch 'release/3.3.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
rhukster committed Apr 30, 2020
2 parents 8a56490 + 65db1d3 commit 73c2238
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 125 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,17 @@
# 3.3.0
## 04/30/2020

* [](#new)
* Rate limiter logic was moved to login events and can be turned on with `['rate_limit' => true]` option
* Rate limiter sets `UserLoginEvent::AUTHENTICATION_CANCELLED` and triggers `onUserLoginFailure` event
* Login now triggers extra `onUserLoginAuthorized` event if user is authorized
* 2FA now triggers either `onUserLoginAuthorized` or `onUserLoginFailure` event with `AUTHORIZATION_CHALLENGE` state
1. [](#bugfix)
* Fixed issue with backwards compatibility for `route_after_login` and `route_after_logout`
* Removed duplicate entries in `blueprint.yaml` causing YAML errors
* Fixed logout not removing task if there was no redirect set
* Fixed remember me triggering `onUserLoginFailure`, use `onUserLoginGuest` event instead

# 3.2.0
## 04/27/2020

Expand Down
10 changes: 10 additions & 0 deletions README.md
Expand Up @@ -12,8 +12,16 @@ These are available via GPM, and because the plugin has dependencies you just ne
$ bin/gpm install login
```

# Changes in version 3.2

New events:

* `onUserLoginAuthorized` Allows plugins to include their own logic when user gets authorized (usually after 2FA challenge).

# Changes in version 3.1

New events:

* `onUserActivated` Allows plugins to hook into user activation, when user has clicked on confirmation email.

# Changes in version 2.6
Expand Down Expand Up @@ -41,6 +49,8 @@ They use following events which can be hooked by plugins:
* `onUserLoginRegisterData` Allows plugins to include their own data to be added to the user object during registration.
* `onUserLoginRegistered` Allows plugins to hook into user registration just before the redirect.

All the events use `UserLoginEvent` with some useful methods to see what is going on.

New Plugin options have been added for:

* `dynamic_page_visibility` - Integrate access into page visibility so things can be shown or hidden in the menu
Expand Down
20 changes: 1 addition & 19 deletions blueprints.yaml
@@ -1,7 +1,7 @@
name: Login
slug: login
type: plugin
version: 3.2.0
version: 3.3.0
testing: false
description: Enables user authentication and login screen.
icon: sign-in
Expand Down Expand Up @@ -233,24 +233,6 @@ form:
help: PLUGIN_LOGIN.REDIRECT_AFTER_ACTIVATION_HELP
placeholder: "/page-to-show-after-activation"

route_forgot:
type: text
size: medium
label: PLUGIN_LOGIN.ROUTE_FORGOT
placeholder: '/forgot_password'

route_reset:
type: text
size: medium
label: PLUGIN_LOGIN.ROUTE_RESET
placeholder: '/reset_password'

route_profile:
type: text
size: medium
label: PLUGIN_LOGIN.ROUTE_PROFILE
placeholder: '/user_profile'

route_register:
type: text
size: medium
Expand Down
177 changes: 128 additions & 49 deletions classes/Controller.php
Expand Up @@ -3,19 +3,21 @@
/**
* @package Grav\Plugin\Login
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @copyright Copyright (C) 2014 - 2020 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/

namespace Grav\Plugin\Login;

use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Grav\Common\Language\Language;
use Grav\Common\Uri;
use Grav\Common\User\Interfaces\UserCollectionInterface;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Plugin\Email\Utils as EmailUtils;
use Grav\Plugin\Login\Events\UserLoginEvent;
use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
use Grav\Plugin\LoginPlugin;
use RocketTheme\Toolbox\Session\Message;
Expand Down Expand Up @@ -120,47 +122,22 @@ public function taskLogin()
/** @var Message $messages */
$messages = $this->grav['messages'];

$userKey = (string)($this->post['username'] ?? '');
$ip = Uri::ip();
$isIPv4 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
$ipKey = $isIPv4 ? $ip : Utils::getSubnet($ip, $this->grav['config']->get('plugins.login.ipv6_subnet_size'));
// Remove login nonce from the form.
$form = array_diff_key($this->post, ['login-form-nonce' => true]);

// Is twofa enabled?
$twofa = $this->grav['config']->get('plugins.login.twofa_enabled', false);

// Pseudonymization of the IP
$ipKey = sha1($ipKey . $this->grav['config']->get('security.salt'));

$rateLimiter = $this->login->getRateLimiter('login_attempts');

// 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))) {
$messages->add($t->translate(['PLUGIN_LOGIN.TOO_MANY_LOGIN_ATTEMPTS', $rateLimiter->getInterval()]), 'error');
$this->setRedirect($this->grav['config']->get('plugins.login.route', '/'));

return true;
}

// Remove login nonce from the form.
$form = array_diff_key($this->post, ['login-form-nonce' => true]);

// Fire Login process.
$event = $this->login->login($form, ['remember_me' => true, 'twofa' => $twofa], ['return_event' => true]);
$event = $this->login->login($form, ['rate_limit' => true, 'remember_me' => true, 'twofa' => $twofa], ['return_event' => true]);
$user = $event->getUser();

/* Support old string-based $redirect_after_login + new bool approach */
$redirect_after_login = $this->grav['config']->get('plugins.login.redirect_after_login');
$route_after_login = $this->grav['config']->get('plugins.login.route_after_login');
$login_redirect = is_bool($redirect_after_login) && $redirect_after_login == true ? $route_after_login : $redirect_after_login;


if ($user->authenticated) {
$rateLimiter->resetRateLimit($ipKey, 'ip')->resetRateLimit($userKey);
if ($user->authorized) {
$event->defMessage('PLUGIN_LOGIN.LOGIN_SUCCESSFUL', 'info');

Expand Down Expand Up @@ -199,48 +176,133 @@ public function taskLogin()

public function taskTwoFa()
{
/** @var Config $config */
$config = $this->grav['config'];

/** @var Language $t */
$t = $this->grav['language'];

/** @var Message $messages */
$messages = $this->grav['messages'];
if (!$config->get('plugins.login.twofa_enabled', false)) {
$messages->add($t->translate('PLUGIN_LOGIN.2FA_FAILED'), 'error');

/** @var TwoFactorAuth $twoFa */
$twoFa = $this->grav['login']->twoFactorAuth();
return true;
}

$twoFa = $this->login->twoFactorAuth();
$user = $this->grav['user'];

$code = $this->post['2fa_code'] ?? null;
$secret = $user->twofa_secret ?? null;

$eventOptions = [
'credentials' => ['username' => $user->get('username')],
'options' => ['twofa' => true]
];

// Attempt to authenticate the user.
$event = new UserLoginEvent($eventOptions);
$event->setUser($user);

if (!$code || !$secret || !$twoFa->verifyCode($secret, $code)) {
$messages->add($t->translate('PLUGIN_LOGIN.2FA_FAILED'), 'error');
$event->setStatus(UserLoginEvent::AUTHENTICATION_FAILURE | UserLoginEvent::AUTHORIZATION_CHALLENGE);
$event->setMessage($t->translate('PLUGIN_LOGIN.2FA_FAILED'), 'error');

$this->grav->fireEvent('onUserLoginFailure', $event);

// Make sure that event didn't mess up with the user authorization.
$user = $event->getUser();
$user->authenticated = false;
$user->authorized = false;

$redirect_to_login = $this->grav['config']->get('plugins.login.route_to_login');
$login_route = $this->grav['config']->get('plugins.login.route');
$redirect_route = $redirect_to_login && $login_route ? $login_route : false;
if ($redirect_route) {
$this->setRedirect($redirect_route, 303);
if (!$event->getRedirect()) {
$redirect_to_login = $this->grav['config']->get('plugins.login.route_to_login');
$login_route = $this->grav['config']->get('plugins.login.route');

$event->setRedirect(
$redirect_to_login && $login_route ? $login_route : $this->getCurrentRedirect(),
303
);
}
} else {

$event->setStatus(UserLoginEvent::AUTHENTICATION_SUCCESS | UserLoginEvent::AUTHORIZATION_CHALLENGE);
$event->setMessage($t->translate('PLUGIN_LOGIN.LOGIN_SUCCESSFUL'), 'info');

$this->grav->fireEvent('onUserLoginAuthorized', $event);

// Make sure that event didn't mess up with the user authorization.
$user = $event->getUser();
$user->authenticated = $event->isSuccess();
$user->authorized = !$event->isDelayed();

if (!$event->getRedirect()) {
/* Support old string-based $redirect_after_login + new bool approach */
$redirect_after_login = $this->grav['config']->get('plugins.login.redirect_after_login');
$route_after_login = $this->grav['config']->get('plugins.login.route_after_login');
$login_redirect = is_bool($redirect_after_login) && $redirect_after_login === true ? $route_after_login : $redirect_after_login;

$event->setRedirect(
$this->grav['session']->redirect_after_login ?: $login_redirect ?: $this->grav['uri']->referrer('/'),
303
);
}
}

/** @var Message $messages */
$messages = $this->grav['messages'];
$messages->add($event->getMessage(), $event->getMessageType());

$redirect = $event->getRedirect() ?: $this->getCurrentRedirect();
$this->setRedirect($redirect, $event->getRedirectCode());

return true;
}

public function taskTwofa_cancel()
{
/** @var Config $config */
$config = $this->grav['config'];

/** @var Language $t */
$t = $this->grav['language'];

/** @var Message $messages */
$messages = $this->grav['messages'];
if (!$config->get('plugins.login.twofa_enabled', false)) {
$messages->add($t->translate('PLUGIN_LOGIN.2FA_FAILED'), 'error');

return true;
}

$messages->add($t->translate('PLUGIN_LOGIN.LOGIN_SUCCESSFUL'), 'info');
$user = $this->grav['user'];
$eventOptions = [
'credentials' => ['username' => $user->get('username')],
'options' => ['twofa' => true]
];

$user->authorized = true;
$event = new UserLoginEvent($eventOptions);

/* Support old string-based $redirect_after_login + new bool approach */
$redirect_after_login = $this->grav['config']->get('plugins.login.redirect_after_login');
$route_after_login = $this->grav['config']->get('plugins.login.route_after_login');
$login_redirect = is_bool($redirect_after_login) && $redirect_after_login == true ? $route_after_login : $redirect_after_login;
$event->setStatus(UserLoginEvent::AUTHENTICATION_CANCELLED | UserLoginEvent::AUTHORIZATION_CHALLENGE);
$event->setMessage($t->translate('PLUGIN_LOGIN.2FA_FAILED'), 'error');

$this->grav->fireEvent('onUserLoginFailure', $event);

// Make sure that event didn't mess up with the user authorization.
$user = $event->getUser();
$user->authenticated = false;
$user->authorized = false;

if (!$event->getRedirect()) {
$redirect_to_login = $this->grav['config']->get('plugins.login.route_to_login');
$login_route = $this->grav['config']->get('plugins.login.route');

$this->setRedirect(
$this->grav['session']->redirect_after_login
?: $login_redirect
?: $this->grav['uri']->referrer('/')
);
$event->setRedirect(
$redirect_to_login && $login_route ? $login_route : $this->getCurrentRedirect(),
303
);
}

return true;
}
Expand Down Expand Up @@ -268,7 +330,7 @@ public function taskLogout()
$route_after_logout = $this->grav['config']->get('plugins.login.route_after_logout');
$logout_redirect = is_bool($redirect_after_logout) && $redirect_after_logout == true ? $route_after_logout : $redirect_after_logout;

$redirect = $event->getRedirect() ?: $logout_redirect;
$redirect = $event->getRedirect() ?: $logout_redirect ?: $this->getCurrentRedirect();
if ($redirect) {
$this->setRedirect($redirect, $event->getRedirectCode());
}
Expand Down Expand Up @@ -485,6 +547,23 @@ public function taskRegenerate2FASecret()
exit;
}

/**
* @return string
*/
protected function getCurrentRedirect()
{
/** @var Uri $uri */
$uri = $this->grav['uri'];
$redirect = $uri->route();
foreach ($uri->params(null, true) as $key => $value) {
if (!in_array($key, ['task', 'nonce', 'login-nonce', 'logout-nonce'], true)) {
$redirect .= $uri->params($key);
}
}

return $redirect;
}

/**
* Redirects an action
*/
Expand Down

0 comments on commit 73c2238

Please sign in to comment.