Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Steur <tsteur@users.noreply.github.com>2018-12-03 06:27:29 +0300
committerGitHub <noreply@github.com>2018-12-03 06:27:29 +0300
commit284bdc0816dd2eff4010e4be42812ff3cc7e25e1 (patch)
tree88c60d0e72bae97b5467c5ad7693a64dd477bf3e /plugins/Login
parente679e0383496383b00f95fd5fd0e42eed4ca49fe (diff)
Implement Two Factor Authentication (#13670)
Diffstat (limited to 'plugins/Login')
-rw-r--r--plugins/Login/Controller.php60
-rw-r--r--plugins/Login/Login.php5
-rw-r--r--plugins/Login/PasswordVerifier.php166
-rw-r--r--plugins/Login/lang/en.json2
-rw-r--r--plugins/Login/templates/confirmPassword.twig43
-rw-r--r--plugins/Login/templates/login.twig258
-rw-r--r--plugins/Login/templates/loginLayout.twig48
-rw-r--r--plugins/Login/tests/Integration/PasswordVerifierTest.php132
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