diff options
author | Thomas Steur <tsteur@users.noreply.github.com> | 2018-12-03 06:27:29 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-12-03 06:27:29 +0300 |
commit | 284bdc0816dd2eff4010e4be42812ff3cc7e25e1 (patch) | |
tree | 88c60d0e72bae97b5467c5ad7693a64dd477bf3e /plugins/Login | |
parent | e679e0383496383b00f95fd5fd0e42eed4ca49fe (diff) |
Implement Two Factor Authentication (#13670)
Diffstat (limited to 'plugins/Login')
-rw-r--r-- | plugins/Login/Controller.php | 60 | ||||
-rw-r--r-- | plugins/Login/Login.php | 5 | ||||
-rw-r--r-- | plugins/Login/PasswordVerifier.php | 166 | ||||
-rw-r--r-- | plugins/Login/lang/en.json | 2 | ||||
-rw-r--r-- | plugins/Login/templates/confirmPassword.twig | 43 | ||||
-rw-r--r-- | plugins/Login/templates/login.twig | 258 | ||||
-rw-r--r-- | plugins/Login/templates/loginLayout.twig | 48 | ||||
-rw-r--r-- | plugins/Login/tests/Integration/PasswordVerifierTest.php | 132 |
8 files changed, 562 insertions, 152 deletions
diff --git a/plugins/Login/Controller.php b/plugins/Login/Controller.php index a69a7b9ce8..939a160e96 100644 --- a/plugins/Login/Controller.php +++ b/plugins/Login/Controller.php @@ -12,7 +12,7 @@ use Exception; use Piwik\Common; use Piwik\Config; use Piwik\Container\StaticContainer; -use Piwik\Cookie; +use Piwik\Date; use Piwik\Log; use Piwik\Nonce; use Piwik\Piwik; @@ -44,13 +44,19 @@ class Controller extends \Piwik\Plugin\Controller protected $sessionInitializer; /** + * @var PasswordVerifier + */ + protected $passwordVerify; + + /** * Constructor. * * @param PasswordResetter $passwordResetter * @param AuthInterface $auth * @param SessionInitializer $authenticatedSessionFactory + * @param PasswordVerifier $passwordVerify */ - public function __construct($passwordResetter = null, $auth = null, $sessionInitializer = null) + public function __construct($passwordResetter = null, $auth = null, $sessionInitializer = null, $passwordVerify = null) { parent::__construct(); @@ -64,6 +70,11 @@ class Controller extends \Piwik\Plugin\Controller } $this->auth = $auth; + if (empty($passwordVerify)) { + $passwordVerify = StaticContainer::get('Piwik\Plugins\Login\PasswordVerifier'); + } + $this->passwordVerify = $passwordVerify; + if (empty($sessionInitializer)) { $sessionInitializer = new \Piwik\Session\SessionInitializer(); } @@ -148,6 +159,51 @@ class Controller extends \Piwik\Plugin\Controller $view->nonce = Nonce::getNonce('Login.login'); } + public function confirmPassword() + { + Piwik::checkUserIsNotAnonymous(); + Piwik::checkUserHasSomeViewAccess(); + + if (!$this->passwordVerify->hasPasswordVerifyBeenRequested()) { + throw new Exception('Not available'); + } + + if (!Url::isValidHost()) { + throw new Exception("Cannot confirm password with untrusted hostname!"); + } + + $nonceKey = 'confirmPassword'; + $messageNoAccess = ''; + if (!empty($_POST)) { + $nonce = Common::getRequestVar('nonce', null, 'string', $_POST); + if (!Nonce::verifyNonce($nonceKey, $nonce)) { + $messageNoAccess = $this->getMessageExceptionNoAccess(); + } elseif ($this->verifyPasswordCorrect()) { + $this->passwordVerify->setPasswordVerifiedCorrectly(); + return; + } else { + $messageNoAccess = Piwik::translate('Login_WrongPasswordEntered'); + } + } + + return $this->renderTemplate('confirmPassword', array( + 'nonce' => Nonce::getNonce($nonceKey), + 'AccessErrorString' => $messageNoAccess + )); + } + + private function verifyPasswordCorrect() + { + /** @var \Piwik\Auth $authAdapter */ + $authAdapter = StaticContainer::get('Piwik\Auth'); + $authAdapter->setLogin(Piwik::getCurrentUserLogin()); + $authAdapter->setPasswordHash(null);// ensure authentication happens on password + $authAdapter->setPassword(Common::getRequestVar('password', null, 'string', $_POST)); + $authAdapter->setTokenAuth(null);// ensure authentication happens on password + $authResult = $authAdapter->authenticate(); + return $authResult->wasAuthenticationSuccessful(); + } + /** * Form-less login * @see how to use it on http://piwik.org/faq/how-to/#faq_30 diff --git a/plugins/Login/Login.php b/plugins/Login/Login.php index bd0d9ae253..bc1f0bd302 100644 --- a/plugins/Login/Login.php +++ b/plugins/Login/Login.php @@ -9,13 +9,16 @@ namespace Piwik\Plugins\Login; use Exception; +use Piwik\API\Request; use Piwik\Common; use Piwik\Config; use Piwik\Container\StaticContainer; use Piwik\Cookie; +use Piwik\Date; use Piwik\FrontController; use Piwik\Piwik; use Piwik\Session; +use Piwik\Url; /** * @@ -23,7 +26,7 @@ use Piwik\Session; class Login extends \Piwik\Plugin { /** - * @see Piwik\Plugin::registerEvents + * @see \Piwik\Plugin::registerEvents */ public function registerEvents() { diff --git a/plugins/Login/PasswordVerifier.php b/plugins/Login/PasswordVerifier.php new file mode 100644 index 0000000000..b3909eb71c --- /dev/null +++ b/plugins/Login/PasswordVerifier.php @@ -0,0 +1,166 @@ +<?php +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +namespace Piwik\Plugins\Login; + +use Piwik\Date; +use Piwik\Piwik; +use Piwik\Session\SessionNamespace; +use Piwik\Url; + +class PasswordVerifier +{ + const VERIFY_VALID_FOR_MINUTES = 30; + const VERIFY_REVALIDATE_X_MINUTES_LEFT = 15; + + /** + * @var Date|null + */ + private $now; + private $enableRedirect = true; + + /** + * @ignore + * tests only + */ + public function setDisableRedirect() + { + $this->enableRedirect = false; + } + + private function getLoginSession() + { + return new SessionNamespace('Login'); + } + + public function hasPasswordVerifyBeenRequested() + { + $sessionNamespace = $this->getLoginSession(); + return !empty($sessionNamespace->redirectParams); + } + + public function forgetVerifiedPassword() + { + // call this method if you want the user to enter the password again after some action was finished which needed + // the password + $sessionNamespace = $this->getLoginSession(); + unset($sessionNamespace->lastPasswordAuth); + unset($sessionNamespace->redirectParams); + } + + /** + * @param Date $now + * @ignore + * tests only + */ + public function setNow(Date $now) + { + $this->now = $now; + } + + private function getNow() + { + if ($this->now) { + return $this->now; + } + return Date::now(); + } + + public function setPasswordVerifiedCorrectly() + { + $sessionNamespace = $this->getLoginSession(); + $sessionNamespace->lastPasswordAuth = $this->getNow()->getDatetime(); + $sessionNamespace->setExpirationSeconds(self::VERIFY_VALID_FOR_MINUTES * 60, 'lastPasswordAuth'); + $sessionNamespace->setExpirationSeconds(self::VERIFY_VALID_FOR_MINUTES * 60, 'redirectParams'); + + if ($this->enableRedirect) { + Url::redirectToUrl('index.php' . Url::getCurrentQueryStringWithParametersModified( + $sessionNamespace->redirectParams + )); + } + } + + public function hasBeenVerified() + { + $lastAuthValidTo = $this->getPasswordVerifyValidUpToDateIfVerified(); + $now = $this->getNow(); + + if ($lastAuthValidTo && $now->isEarlier($lastAuthValidTo)) { + return true; + } + return false; + } + + private function getPasswordVerifyValidUpToDateIfVerified() + { + $sessionNamespace = $this->getLoginSession(); + if (!empty($sessionNamespace->lastPasswordAuth) && !empty($sessionNamespace->redirectParams)) { + $lastAuthValidTo = Date::factory($sessionNamespace->lastPasswordAuth)->addPeriod(self::VERIFY_VALID_FOR_MINUTES, 'minute'); + return $lastAuthValidTo; + } + } + + protected function hasBeenVerifiedAndHalfTimeValid() + { + $lastAuthValidTo = $this->getPasswordVerifyValidUpToDateIfVerified(); + $now = $this->getNow()->addPeriod(self::VERIFY_REVALIDATE_X_MINUTES_LEFT, 'minute'); + + if ($lastAuthValidTo && $now->isEarlier($lastAuthValidTo)) { + return true; + } + return false; + } + + /** + * Checks if the user has verified the password within the last 15 minutes. If not, the user will be redirected. + * The password verify will be valid for at least another 15 minutes giving the user some time to perform an action. + * See {@link requirePasswordVerified} + * + * @param $redirectParams + * @return true if password has been verified recently, will redirect if not + * @throws \Zend_Session_Exception + */ + public function requirePasswordVerifiedRecently($redirectParams) + { + if ($this->hasBeenVerifiedAndHalfTimeValid()) { + return true; + } + + $this->initiatePasswordVerifyRedirect($redirectParams); + } + + /** + * Checks if the user has verified the password within the last 30 minutes. If not, the user will be redirected. + * Please note that if the user performs an action afterwards, the password verify could be valid for only few more + * seconds or minutes and by the time the user confirms a certain action, the password verify may no longer be valid. + * If you want to ensure the password will be still valid for eg 15 minutes before the user performs some action, + * consider using {@link requirePasswordVerifiedRecently}. + * + * @param $redirectParams + * @return true if password has been verified, will redirect if not + * @throws \Zend_Session_Exception + */ + public function requirePasswordVerified($redirectParams) + { + if ($this->hasBeenVerified()) { + return true; + } + + $this->initiatePasswordVerifyRedirect($redirectParams); + } + + private function initiatePasswordVerifyRedirect($redirectParams) + { + $sessionNamespace = $this->getLoginSession(); + $sessionNamespace->redirectParams = $redirectParams; + $sessionNamespace->setExpirationSeconds(self::VERIFY_VALID_FOR_MINUTES * 60 * 5, 'redirectParams'); + + if ($this->enableRedirect) { + Piwik::redirectToModule('Login', 'confirmPassword'); + } + } +} diff --git a/plugins/Login/lang/en.json b/plugins/Login/lang/en.json index 48f06f20b0..314bb38293 100644 --- a/plugins/Login/lang/en.json +++ b/plugins/Login/lang/en.json @@ -20,6 +20,8 @@ "PasswordChanged": "Your password has been changed.", "PasswordRepeat": "Password (repeat)", "PasswordsDoNotMatch": "Passwords do not match.", + "WrongPasswordEntered": "Please enter your correct password.", + "ConfirmPasswordToContinue": "Confirm your password to continue", "PluginDescription": "Provides authentication via username and password as well as password reset functionality. Authentication method can be changed by using another Login plugin such as LoginLdap available on the Marketplace.", "RememberMe": "Remember Me" } diff --git a/plugins/Login/templates/confirmPassword.twig b/plugins/Login/templates/confirmPassword.twig new file mode 100644 index 0000000000..14db8f1113 --- /dev/null +++ b/plugins/Login/templates/confirmPassword.twig @@ -0,0 +1,43 @@ +{% extends '@Login/loginLayout.twig' %} + +{% set title %}{{ 'Login_ConfirmPasswordToContinue'|translate }}{% endset %} + +{% block loginContent %} + <div class="contentForm loginForm confirmPasswordForm"> + {% embed 'contentBlock.twig' with {'title': ('Login_ConfirmPasswordToContinue'|translate)} %} + {% block content %} + + <div class="message_container"> + {% if AccessErrorString %} + <div piwik-notification + noclear="true" + context="error"> + <strong>{{ 'General_Error'|translate }}</strong>: {{ AccessErrorString|raw }}<br/> + </div> + {% endif %} + </div> + + <form action="{{ linkTo({'module': 'Login', 'action': 'confirmPassword'}) }}" ng-non-bindable method="post"> + <div class="row"> + <div class="col s12 input-field"> + <input type="hidden" name="nonce" id="login_form_nonce" value="{{ nonce }}"/> + <input type="password" placeholder="" name="password" id="login_form_password" class="input" value="" size="20" + autocorrect="off" autocapitalize="none" + tabindex="20" /> + <label for="login_form_password"><i class="icon-locked icon"></i> {{ 'General_Password'|translate }}</label> + </div> + </div> + + <div class="row actions"> + <div class="col s12"> + <input class="submit btn" id='login_form_submit' type="submit" value="{{ 'General_Confirm'|translate }}" + tabindex="100"/> + </div> + </div> + + </form> + {% endblock %} + {% endembed %} + </div> + +{% endblock %}
\ No newline at end of file diff --git a/plugins/Login/templates/login.twig b/plugins/Login/templates/login.twig index abe2e2ae46..875d5e6818 100644 --- a/plugins/Login/templates/login.twig +++ b/plugins/Login/templates/login.twig @@ -1,170 +1,130 @@ -{% extends '@Morpheus/layout.twig' %} -{% block meta %} - <meta name="robots" content="index,follow"> -{% endblock %} +{% extends '@Login/loginLayout.twig' %} -{% block head %} - {{ parent() }} +{% block loginContent %} + <div class="contentForm loginForm"> + {% embed 'contentBlock.twig' with {'title': 'Login_LogIn'|translate} %} + {% block content %} + <div class="message_container"> - <script type="text/javascript" src="libs/bower_components/jquery-placeholder/jquery.placeholder.js"></script> -{% endblock %} + {{ include('@Login/_formErrors.twig', {formErrors: form_data.errors } ) }} -{% set title %}{{ 'Login_LogIn'|translate }}{% endset %} - -{% block pageDescription %}{{ 'General_OpenSourceWebAnalytics'|translate }}{% endblock %} - -{% set bodyId = 'loginPage' %} - -{% block body %} - - {{ postEvent("Template.beforeTopBar", "login") }} - {{ postEvent("Template.beforeContent", "login") }} - - {% include "_iframeBuster.twig" %} - - <div id="notificationContainer"> - </div> - <nav> - <div class="nav-wrapper"> - {% include "@CoreHome/_logo.twig" with { 'logoLink': 'https://matomo.org', 'centeredLogo': true, 'useLargeLogo': false } %} - </div> - </nav> - - <section class="loginSection row"> - <div class="col s12 m6 push-m3 l4 push-l4"> - - {# untrusted host warning #} - {% if (isValidHost is defined and invalidHostMessage is defined and isValidHost == false) %} - {% include '@CoreHome/_warningInvalidHost.twig' %} - {% else %} - <div class="contentForm loginForm"> - {% embed 'contentBlock.twig' with {'title': 'Login_LogIn'|translate} %} - {% block content %} - - <div class="message_container"> - - {{ include('@Login/_formErrors.twig', {formErrors: form_data.errors } ) }} - - {% if AccessErrorString %} - <div piwik-notification - noclear="true" - context="error"> - <strong>{{ 'General_Error'|translate }}</strong>: {{ AccessErrorString|raw }}<br/> - </div> - {% endif %} - - {% if infoMessage %} - <p class="message">{{ infoMessage|raw }}</p> - {% endif %} - </div> + {% if AccessErrorString %} + <div piwik-notification + noclear="true" + context="error"> + <strong>{{ 'General_Error'|translate }}</strong>: {{ AccessErrorString|raw }}<br/> + </div> + {% endif %} - <form {{ form_data.attributes|raw }} ng-non-bindable> - <div class="row"> - <div class="col s12 input-field"> - <input type="text" name="form_login" placeholder="" id="login_form_login" class="input" value="" size="20" - autocorrect="off" autocapitalize="none" - tabindex="10" autofocus="autofocus"/> - <label for="login_form_login"><i class="icon-user icon"></i> {{ 'Login_LoginOrEmail'|translate }}</label> - </div> + {% if infoMessage %} + <p class="message">{{ infoMessage|raw }}</p> + {% endif %} + </div> + + <form {{ form_data.attributes|raw }} ng-non-bindable> + <div class="row"> + <div class="col s12 input-field"> + <input type="text" name="form_login" placeholder="" id="login_form_login" class="input" value="" size="20" + autocorrect="off" autocapitalize="none" + tabindex="10" autofocus="autofocus"/> + <label for="login_form_login"><i class="icon-user icon"></i> {{ 'Login_LoginOrEmail'|translate }}</label> </div> + </div> - <div class="row"> - <div class="col s12 input-field"> - <input type="hidden" name="form_nonce" id="login_form_nonce" value="{{ nonce }}"/> - <input type="password" placeholder="" name="form_password" id="login_form_password" class="input" value="" size="20" - autocorrect="off" autocapitalize="none" - tabindex="20" /> - <label for="login_form_password"><i class="icon-locked icon"></i> {{ 'General_Password'|translate }}</label> - </div> + <div class="row"> + <div class="col s12 input-field"> + <input type="hidden" name="form_nonce" id="login_form_nonce" value="{{ nonce }}"/> + <input type="password" placeholder="" name="form_password" id="login_form_password" class="input" value="" size="20" + autocorrect="off" autocapitalize="none" + tabindex="20" /> + <label for="login_form_password"><i class="icon-locked icon"></i> {{ 'General_Password'|translate }}</label> </div> + </div> - <div class="row actions"> - <div class="col s12"> - <input name="form_rememberme" type="checkbox" id="login_form_rememberme" value="1" tabindex="90" - {% if form_data.form_rememberme.value %}checked="checked" {% endif %}/> - <label for="login_form_rememberme">{{ 'Login_RememberMe'|translate }}</label> - <input class="submit btn" id='login_form_submit' type="submit" value="{{ 'Login_LogIn'|translate }}" - tabindex="100"/> - </div> + <div class="row actions"> + <div class="col s12"> + <input name="form_rememberme" type="checkbox" id="login_form_rememberme" value="1" tabindex="90" + {% if form_data.form_rememberme.value %}checked="checked" {% endif %}/> + <label for="login_form_rememberme">{{ 'Login_RememberMe'|translate }}</label> + <input class="submit btn" id='login_form_submit' type="submit" value="{{ 'Login_LogIn'|translate }}" + tabindex="100"/> </div> + </div> - </form> - <p id="nav"> - {{ postEvent("Template.loginNav", "top") }} - <a id="login_form_nav" href="#" - title="{{ 'Login_LostYourPassword'|translate }}">{{ 'Login_LostYourPassword'|translate }}</a> - {{ postEvent("Template.loginNav", "bottom") }} + </form> + <p id="nav"> + {{ postEvent("Template.loginNav", "top") }} + <a id="login_form_nav" href="#" + title="{{ 'Login_LostYourPassword'|translate }}">{{ 'Login_LostYourPassword'|translate }}</a> + {{ postEvent("Template.loginNav", "bottom") }} + </p> + + {% if isCustomLogo %} + <p id="piwik"> + <i><a href="https://matomo.org/" rel="noreferrer noopener" target="_blank">{{ linkTitle }}</a></i> </p> + {% endif %} - {% if isCustomLogo %} - <p id="piwik"> - <i><a href="https://matomo.org/" rel="noreferrer noopener" target="_blank">{{ linkTitle }}</a></i> - </p> - {% endif %} - - {% endblock %} - {% endembed %} - </div> - <div class="contentForm resetForm" style="display:none;"> - {% embed 'contentBlock.twig' with {'title': 'Login_ChangeYourPassword'|translate} %} - {% block content %} - - <div class="message_container"> - </div> - - <form id="reset_form" method="post" ng-non-bindable> - <div class="row"> - <div class="col s12 input-field"> - <input type="hidden" name="form_nonce" id="reset_form_nonce" value="{{ nonce }}"/> - <input type="text" placeholder="" name="form_login" id="reset_form_login" class="input" value="" size="20" - autocorrect="off" autocapitalize="none" - tabindex="10"/> - <label for="reset_form_login"><i class="icon-user icon"></i> {{ 'Login_LoginOrEmail'|translate }}</label> - </div> + {% endblock %} + {% endembed %} + </div> + <div class="contentForm resetForm" style="display:none;"> + {% embed 'contentBlock.twig' with {'title': 'Login_ChangeYourPassword'|translate} %} + {% block content %} + + <div class="message_container"> + </div> + + <form id="reset_form" method="post" ng-non-bindable> + <div class="row"> + <div class="col s12 input-field"> + <input type="hidden" name="form_nonce" id="reset_form_nonce" value="{{ nonce }}"/> + <input type="text" placeholder="" name="form_login" id="reset_form_login" class="input" value="" size="20" + autocorrect="off" autocapitalize="none" + tabindex="10"/> + <label for="reset_form_login"><i class="icon-user icon"></i> {{ 'Login_LoginOrEmail'|translate }}</label> </div> - <div class="row"> - <div class="col s12 input-field"> - <input type="password" placeholder="" name="form_password" id="reset_form_password" class="input" value="" size="20" - autocorrect="off" autocapitalize="none" - tabindex="20" autocomplete="off"/> - <label for="reset_form_password"><i class="icon-locked icon"></i> {{ 'Login_NewPassword'|translate }}</label> - </div> + </div> + <div class="row"> + <div class="col s12 input-field"> + <input type="password" placeholder="" name="form_password" id="reset_form_password" class="input" value="" size="20" + autocorrect="off" autocapitalize="none" + tabindex="20" autocomplete="off"/> + <label for="reset_form_password"><i class="icon-locked icon"></i> {{ 'Login_NewPassword'|translate }}</label> </div> - <div class="row"> - <div class="col s12 input-field"> - <input type="password" placeholder="" name="form_password_bis" id="reset_form_password_bis" class="input" value="" - autocorrect="off" autocapitalize="none" - size="20" tabindex="30" autocomplete="off"/> - <label for="reset_form_password_bis"><i class="icon-locked icon"></i> {{ 'Login_NewPasswordRepeat'|translate }}</label> - </div> + </div> + <div class="row"> + <div class="col s12 input-field"> + <input type="password" placeholder="" name="form_password_bis" id="reset_form_password_bis" class="input" value="" + autocorrect="off" autocapitalize="none" + size="20" tabindex="30" autocomplete="off"/> + <label for="reset_form_password_bis"><i class="icon-locked icon"></i> {{ 'Login_NewPasswordRepeat'|translate }}</label> </div> + </div> - <div class="row actions"> - <div class="col s12"> - <input class="submit btn" id='reset_form_submit' type="submit" - value="{{ 'General_ChangePassword'|translate }}" tabindex="100"/> + <div class="row actions"> + <div class="col s12"> + <input class="submit btn" id='reset_form_submit' type="submit" + value="{{ 'General_ChangePassword'|translate }}" tabindex="100"/> - <span class="loadingPiwik" style="display:none;"> - <img alt="Loading" src="plugins/Morpheus/images/loading-blue.gif"/> - </span> - </div> + <span class="loadingPiwik" style="display:none;"> + <img alt="Loading" src="plugins/Morpheus/images/loading-blue.gif"/> + </span> </div> + </div> - <input type="hidden" name="module" value="{{ loginModule }}"/> - <input type="hidden" name="action" value="resetPassword"/> - </form> - <p id="nav"> - <a id="reset_form_nav" href="#" - title="{{ 'Mobile_NavigationBack'|translate }}">{{ 'General_Cancel'|translate }}</a> - <a id="alternate_reset_nav" href="#" style="display:none;" - title="{{'Login_LogIn'|translate}}">{{ 'Login_LogIn'|translate }}</a> - </p> - {% endblock %} - {% endembed %} - </div> - {% endif %} - - </section> + <input type="hidden" name="module" value="{{ loginModule }}"/> + <input type="hidden" name="action" value="resetPassword"/> + </form> + <p id="nav"> + <a id="reset_form_nav" href="#" + title="{{ 'Mobile_NavigationBack'|translate }}">{{ 'General_Cancel'|translate }}</a> + <a id="alternate_reset_nav" href="#" style="display:none;" + title="{{'Login_LogIn'|translate}}">{{ 'Login_LogIn'|translate }}</a> + </p> + {% endblock %} + {% endembed %} + </div> -{% endblock %} +{% endblock %}
\ No newline at end of file diff --git a/plugins/Login/templates/loginLayout.twig b/plugins/Login/templates/loginLayout.twig new file mode 100644 index 0000000000..837d3d3fd5 --- /dev/null +++ b/plugins/Login/templates/loginLayout.twig @@ -0,0 +1,48 @@ +{% extends '@Morpheus/layout.twig' %} + +{% block meta %} + <meta name="robots" content="index,follow"> +{% endblock %} + +{% block head %} + {{ parent() }} + + <script type="text/javascript" src="libs/bower_components/jquery-placeholder/jquery.placeholder.js"></script> +{% endblock %} + +{% set title %}{{ 'Login_LogIn'|translate }}{% endset %} + +{% block pageDescription %}{{ 'General_OpenSourceWebAnalytics'|translate }}{% endblock %} + +{% set bodyId = 'loginPage' %} + +{% block body %} + + {{ postEvent("Template.beforeTopBar", "login") }} + {{ postEvent("Template.beforeContent", "login") }} + + {% include "_iframeBuster.twig" %} + + <div id="notificationContainer"> + </div> + <nav> + <div class="nav-wrapper"> + {% include "@CoreHome/_logo.twig" with { 'logoLink': 'https://matomo.org', 'centeredLogo': true, 'useLargeLogo': false } %} + </div> + </nav> + + <section class="loginSection row"> + <div class="col s12 m6 push-m3 l4 push-l4"> + + {# untrusted host warning #} + {% if (isValidHost is defined and invalidHostMessage is defined and isValidHost == false) %} + {% include '@CoreHome/_warningInvalidHost.twig' %} + {% else %} + {% block loginContent %} + {% endblock %} + {% endif %} + + </div> + </section> + +{% endblock %} diff --git a/plugins/Login/tests/Integration/PasswordVerifierTest.php b/plugins/Login/tests/Integration/PasswordVerifierTest.php new file mode 100644 index 0000000000..a169e5e723 --- /dev/null +++ b/plugins/Login/tests/Integration/PasswordVerifierTest.php @@ -0,0 +1,132 @@ +<?php +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Login\tests\Integration; + +use Piwik\Date; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; +use Piwik\Plugins\Login\PasswordVerifier; + +class CustomPasswordVerifier extends PasswordVerifier { + public function hasBeenVerifiedAndHalfTimeValid() + { + return parent::hasBeenVerifiedAndHalfTimeValid(); + } +} + +class PasswordVerifierTest extends IntegrationTestCase +{ + + /** + * @var CustomPasswordVerifier + */ + private $verifier; + + public function setUp() + { + parent::setUp(); + + $this->verifier = new CustomPasswordVerifier(); + $this->verifier->setDisableRedirect(); + } + + public function test_hasBeenVerified_byDefaultNotVerified() + { + $this->assertFalse($this->verifier->hasBeenVerified()); + } + + public function test_hasBeenVerifiedAndHalfTimeValid_byDefaultNotVerified() + { + $this->assertFalse($this->verifier->hasBeenVerifiedAndHalfTimeValid()); + } + + public function test_hasPasswordVerifyBeenRequested_byDefaultNotRequested() + { + $this->assertFalse($this->verifier->hasPasswordVerifyBeenRequested()); + } + + public function test_requirePasswordVerifiedRecently() + { + $this->assertNull($this->requirePasswordVerify()); + $this->assertTrue($this->verifier->hasPasswordVerifyBeenRequested()); + $this->assertFalse($this->verifier->hasBeenVerified()); + $this->assertFalse($this->verifier->hasBeenVerifiedAndHalfTimeValid()); + } + + public function test_setPasswordVerifiedCorrectly() + { + $this->assertNull($this->requirePasswordVerify()); + $this->assertFalse($this->verifier->hasBeenVerified()); + $this->assertFalse($this->verifier->hasBeenVerifiedAndHalfTimeValid()); + + $this->verifier->setPasswordVerifiedCorrectly(); + + $this->assertTrue($this->verifier->hasBeenVerified()); + $this->assertTrue($this->verifier->hasBeenVerifiedAndHalfTimeValid()); + $this->assertTrue($this->requirePasswordVerify()); // no need to redirect + } + + public function test_setPasswordVerifiedCorrectly_requiresAPasswordToBeRequestedToBeValid() + { + $this->verifier->setPasswordVerifiedCorrectly(); + + $this->assertFalse($this->verifier->hasBeenVerified()); + $this->assertFalse($this->verifier->hasBeenVerifiedAndHalfTimeValid()); + $this->assertNull($this->requirePasswordVerify()); + } + + public function test_setPasswordVerifiedCorrectly_expiresAfter15Min() + { + $this->assertNull($this->requirePasswordVerify()); + $this->assertFalse($this->verifier->hasBeenVerified()); + $this->assertFalse($this->verifier->hasBeenVerifiedAndHalfTimeValid()); + + $this->verifier->setPasswordVerifiedCorrectly(); + + $this->assertTrue($this->verifier->hasBeenVerified()); + $this->assertTrue($this->verifier->hasBeenVerifiedAndHalfTimeValid()); + $this->assertTrue($this->requirePasswordVerify()); // no need to redirect + + $this->verifier->setNow(Date::now()->addPeriod(PasswordVerifier::VERIFY_REVALIDATE_X_MINUTES_LEFT - 1, 'minutes')); + + $this->assertTrue($this->verifier->hasBeenVerified()); + $this->assertTrue($this->verifier->hasBeenVerifiedAndHalfTimeValid()); + $this->assertTrue($this->requirePasswordVerify()); // no need to redirect + + $this->verifier->setNow(Date::now()->addPeriod(PasswordVerifier::VERIFY_REVALIDATE_X_MINUTES_LEFT + 1, 'minutes')); + + $this->assertTrue($this->verifier->hasBeenVerified()); // it was verified recently + $this->assertFalse($this->verifier->hasBeenVerifiedAndHalfTimeValid()); + $this->assertNull($this->requirePasswordVerify()); // no need to redirect + + $this->verifier->setNow(Date::now()->addPeriod(PasswordVerifier::VERIFY_VALID_FOR_MINUTES + 1, 'minutes')); + + $this->assertFalse($this->verifier->hasBeenVerified()); // it was verified recently + $this->assertFalse($this->verifier->hasBeenVerifiedAndHalfTimeValid()); + $this->assertNull($this->requirePasswordVerify()); // no need to redirect + } + + public function test_forgetVerifiedPassword() + { + $this->requirePasswordVerify(); + $this->verifier->setPasswordVerifiedCorrectly(); + $this->assertTrue($this->verifier->hasBeenVerified()); + $this->assertTrue($this->requirePasswordVerify()); // no need to redirect + + $this->verifier->forgetVerifiedPassword(); + + $this->assertNull($this->requirePasswordVerify()); + $this->assertFalse($this->verifier->hasBeenVerified()); + } + + private function requirePasswordVerify() + { + return $this->verifier->requirePasswordVerifiedRecently(array('module' => 'Login', 'action' => 'test')); + } + +}
\ No newline at end of file |