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>2020-03-18 06:04:12 +0300
committerGitHub <noreply@github.com>2020-03-18 06:04:12 +0300
commitf0c246cb3a4db3021da7552f6779d56613799414 (patch)
tree42ddf7a9c2e086df78ffc40dfc23af74f8dd3a39 /plugins
parente493fee87c983e02001a7d9438cefe58141a38af (diff)
App specific token_auths (#15410)
* some initial work * add security page * backing up some code * more functionality * adjust more UI parts * adjust more code * more tweaks * add todo note * few tweaks * make sure date is in right format * fix not existing column * few fixes * available hashes * use different hash algo so tests run on php 5 * fix name of aglorithm * trying to fix some tests * another try to fix some tests * more fixes * more fixes * few fixes * update template * fix some tests * fix test * fixing some tests * various test fixes * more fixes * few more tests * more tests * various tweaks * add translations * add some ui tests * fix selector * tweaks * trying to fix some ui tests * fallback to regular authentication if needed * fix call authenticate on null * fix user settings * fix some tests * few fixes * fix more ui tests * update schema * Update plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js Co-Authored-By: Stefan Giehl <stefan@matomo.org> * fix maps are not showing data * trying to fix some tests * set correct token * trying to fix tracking failure * minor tweaks and fixes * fix more tests * fix screenshot test * trigger event so brute force logic is executed * test no fallback to actual authentication * allow fallback * apply review feedback * fix some tests * fix tests * make sure location values from query params are limited properly before attempting a db insert * make sure plugin uninstall migration reloads plugins, make sure 4.0.0-b1 migration removes unique index that is no longer used, use defaults extra file in SqlDump to get test to run on travis * Fix UI tests. * update expected screenshot Co-authored-by: Stefan Giehl <stefan@matomo.org> Co-authored-by: diosmosis <diosmosis@users.noreply.github.com>
Diffstat (limited to 'plugins')
-rw-r--r--plugins/API/Controller.php4
-rw-r--r--plugins/API/css/styles.css29
-rw-r--r--plugins/API/lang/en.json2
-rw-r--r--plugins/API/templates/listAllAPI.twig11
-rw-r--r--plugins/BulkTracking/tests/Integration/RequestsTest.php4
-rw-r--r--plugins/CoreHome/angularjs/common/services/piwik-api.js3
-rw-r--r--plugins/CoreHome/angularjs/common/services/piwik-api.spec.js2
-rw-r--r--plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js4
-rw-r--r--plugins/Dashboard/tests/UI/Dashboard_spec.js2
-rw-r--r--plugins/Feedback/tests/Integration/FeedbackTest.php1
-rw-r--r--plugins/Login/Auth.php21
-rw-r--r--plugins/Login/Login.php2
-rw-r--r--plugins/Login/SessionInitializer.php2
-rw-r--r--plugins/Login/tests/Integration/LoginTest.php26
-rw-r--r--plugins/Login/tests/Integration/SessionInitializerTest.php4
-rw-r--r--plugins/Login/tests/UI/expected-screenshots/Login_bruteforcelog_noentries.png4
-rw-r--r--plugins/Login/tests/UI/expected-screenshots/Login_bruteforcelog_withentries.png4
-rw-r--r--plugins/Morpheus/javascripts/ajaxHelper.js3
-rw-r--r--plugins/Overlay/javascripts/Overlay_Helper.js2
-rw-r--r--plugins/Overlay/templates/index.twig2
-rw-r--r--plugins/Overlay/templates/index_noframe.twig2
m---------plugins/Provider0
-rw-r--r--plugins/TwoFactorAuth/Controller.php2
-rw-r--r--plugins/TwoFactorAuth/TwoFactorAuth.php6
-rw-r--r--plugins/TwoFactorAuth/templates/setupFinished.twig2
-rw-r--r--plugins/TwoFactorAuth/tests/Fixtures/TwoFactorFixture.php2
-rw-r--r--plugins/TwoFactorAuth/tests/Integration/TwoFactorAuthTest.php36
-rw-r--r--plugins/TwoFactorAuth/tests/UI/TwoFactorAuth_spec.js2
-rw-r--r--plugins/UserCountry/Columns/City.php2
-rw-r--r--plugins/UserCountry/Columns/Country.php2
-rw-r--r--plugins/UserCountry/Columns/Region.php2
-rw-r--r--plugins/UserCountryMap/javascripts/realtime-map.js2
-rw-r--r--plugins/UserCountryMap/javascripts/visitor-map.js2
-rw-r--r--plugins/UsersManager/API.php63
-rw-r--r--plugins/UsersManager/Controller.php223
-rw-r--r--plugins/UsersManager/Menu.php1
-rw-r--r--plugins/UsersManager/Model.php214
-rw-r--r--plugins/UsersManager/Tasks.php6
-rw-r--r--plugins/UsersManager/UsersManager.php32
-rw-r--r--plugins/UsersManager/angularjs/personal-settings/personal-settings.controller.js29
-rw-r--r--plugins/UsersManager/lang/en.json17
-rw-r--r--plugins/UsersManager/stylesheets/usersManager.less7
-rw-r--r--plugins/UsersManager/templates/addNewToken.twig37
-rw-r--r--plugins/UsersManager/templates/addNewTokenSuccess.twig17
-rw-r--r--plugins/UsersManager/templates/userSecurity.twig121
-rw-r--r--plugins/UsersManager/templates/userSettings.twig80
-rw-r--r--plugins/UsersManager/tests/Fixtures/ManyUsers.php7
-rw-r--r--plugins/UsersManager/tests/Integration/ModelTest.php260
-rw-r--r--plugins/UsersManager/tests/Integration/UserAccessFilterTest.php18
-rw-r--r--plugins/UsersManager/tests/Integration/UsersManagerTest.php11
-rw-r--r--plugins/UsersManager/tests/System/ApiTest.php96
-rw-r--r--plugins/UsersManager/tests/UI/UserSettings_spec.js34
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_add_token.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_add_token_check_password.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_add_token_success.png3
-rw-r--r--plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_load_security.png3
-rw-r--r--plugins/Widgetize/templates/index.twig4
57 files changed, 1178 insertions, 305 deletions
diff --git a/plugins/API/Controller.php b/plugins/API/Controller.php
index 326b048926..d46bdf158a 100644
--- a/plugins/API/Controller.php
+++ b/plugins/API/Controller.php
@@ -26,7 +26,9 @@ class Controller extends \Piwik\Plugin\Controller
{
function index()
{
- $token = 'token_auth=' . Common::getRequestVar('token_auth', 'anonymous', 'string');
+ $tokenAuth = Common::getRequestVar('token_auth', 'anonymous', 'string');
+
+ $token = 'token_auth=' . $tokenAuth;
// when calling the API through http, we limit the number of returned results
if (!isset($_GET['filter_limit'])) {
diff --git a/plugins/API/css/styles.css b/plugins/API/css/styles.css
new file mode 100644
index 0000000000..3b52ba995a
--- /dev/null
+++ b/plugins/API/css/styles.css
@@ -0,0 +1,29 @@
+#token_auth {
+ background-color:#E8FFE9;
+ border-color:#00CC3A;
+ border-style: solid;
+ border-width: 1px;
+ margin: 0pt 0pt 16px 8px;
+ padding: 12px;
+ line-height:4em;
+}
+.example, .example A {
+ color:#9E9E9E;
+}
+
+.page_api{
+ padding:0px 15px 0 15px;
+ font-size:13px;
+}
+
+.page_api h2 {
+ border-bottom:1px solid #DADADA;
+ margin:10px -15px 15px 0px;
+ padding:0pt 0px 5px 0pt;
+ font-size:24px;
+}
+
+.page_api p {
+ line-height:140%;
+ padding-bottom:20px;
+}
diff --git a/plugins/API/lang/en.json b/plugins/API/lang/en.json
index 72dd837fae..cc43b92313 100644
--- a/plugins/API/lang/en.json
+++ b/plugins/API/lang/en.json
@@ -9,7 +9,7 @@
"ReportingApiReference": "Reporting API Reference",
"TopLinkTooltip": "Access your Web Analytics data programmatically through a simple API in json, xml, etc.",
"UserAuthentication": "User authentication",
- "UsingTokenAuth": "If you want to %1$s request data within a script, a crontab, etc. %2$s you need to add the parameter %3$s to the API calls URLs that require authentication.",
+ "UsingTokenAuth": "If you want to %1$s request data within a script, a crontab, etc. %2$s you need to add the URL parameter %3$s to the API calls URLs that require authentication.",
"Glossary": "Glossary",
"LearnAboutCommonlyUsedTerms2": "Learn about the commonly used terms to make the most of Matomo Analytics.",
"EvolutionMetricName": "%s Evolution"
diff --git a/plugins/API/templates/listAllAPI.twig b/plugins/API/templates/listAllAPI.twig
index 17da68de1d..9c9c2ea70f 100644
--- a/plugins/API/templates/listAllAPI.twig
+++ b/plugins/API/templates/listAllAPI.twig
@@ -19,13 +19,12 @@
</div>
<div piwik-content-block content-title="{{ 'API_UserAuthentication'|translate|e('html_attr') }}">
<p>
- {{ 'API_UsingTokenAuth'|translate('','',"")|raw }}<br/>
- <pre piwik-select-on-focus id='token_auth'>&amp;token_auth=<strong piwik-show-sensitive-data="{{ token_auth }}" data-click-element-selector="#token_auth"></strong></pre><br/>
- {{ 'API_KeepTokenSecret'|translate('<b>','</b>')|raw }}<br />
- {{ 'API_ChangeTokenHint'|translate('<a href="' ~ linkTo({
+ {{ 'API_UsingTokenAuth'|translate('','',"<code>token_auth</code>")|raw }} <a target="_blank" rel="noreferrer noopener" href="https://developer.matomo.org/api-reference/reporting-api#authenticate-to-the-api-via-token_auth-parameter">{{ 'CoreAdminHome_LearnMore'|translate }}</a><br/>
+ <br/>
+ <a href="{{ linkTo({
'module': 'UsersManager',
- 'action': 'userSettings',
- }) ~ '">', '</a>')|raw }}
+ 'action': 'userSecurity',
+ }) }}#/#authtokens">You can manage your authentication tokens on your security page.</a>
</p>
</div>
{{ list_api_methods_with_links|raw }}
diff --git a/plugins/BulkTracking/tests/Integration/RequestsTest.php b/plugins/BulkTracking/tests/Integration/RequestsTest.php
index 3e4e0fbc7e..6b67b88ae2 100644
--- a/plugins/BulkTracking/tests/Integration/RequestsTest.php
+++ b/plugins/BulkTracking/tests/Integration/RequestsTest.php
@@ -8,8 +8,10 @@
namespace Piwik\Plugins\BulkTracking\tests\Integration;
+use Piwik\Container\StaticContainer;
use Piwik\Plugins\BulkTracking\Tracker\Requests;
use Piwik\Plugins\UsersManager\API;
+use Piwik\Plugins\UsersManager\Model;
use Piwik\Plugins\UsersManager\UsersManager;
use Piwik\Tests\Framework\Fixture;
use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
@@ -88,7 +90,7 @@ class RequestsTest extends IntegrationTestCase
$this->expectException(\Exception::class);
$this->expectExceptionMessage('token_auth specified does not have Admin permission for idsite=1');
- $dummyToken = API::getInstance()->createTokenAuth('test');
+ $dummyToken = StaticContainer::get(Model::class)->generateRandomTokenAuth();
$superUserToken = $this->getSuperUserToken();
$requests = array($this->buildDummyRequest($superUserToken), $this->buildDummyRequest($dummyToken));
diff --git a/plugins/CoreHome/angularjs/common/services/piwik-api.js b/plugins/CoreHome/angularjs/common/services/piwik-api.js
index e2195f7ff1..e34ca41593 100644
--- a/plugins/CoreHome/angularjs/common/services/piwik-api.js
+++ b/plugins/CoreHome/angularjs/common/services/piwik-api.js
@@ -49,6 +49,7 @@ var hasBlockedContent = false;
function withTokenInUrl()
{
postParams['token_auth'] = piwik.token_auth;
+ postParams['force_api_session'] = '1';
}
function isRequestToApiMethod() {
@@ -191,6 +192,7 @@ var hasBlockedContent = false;
function getPostParams (params) {
if (isRequestToApiMethod() || piwik.shouldPropagateTokenAuth) {
params.token_auth = piwik.token_auth;
+ params.force_api_session = '1';
}
return params;
@@ -276,6 +278,7 @@ var hasBlockedContent = false;
if (_postParams_) {
if (postParams && postParams.token_auth && !_postParams_.token_auth) {
_postParams_.token_auth = postParams.token_auth;
+ _postParams_.force_api_session = '1';
}
postParams = _postParams_;
}
diff --git a/plugins/CoreHome/angularjs/common/services/piwik-api.spec.js b/plugins/CoreHome/angularjs/common/services/piwik-api.spec.js
index e33fa329e2..916c5b73e1 100644
--- a/plugins/CoreHome/angularjs/common/services/piwik-api.spec.js
+++ b/plugins/CoreHome/angularjs/common/services/piwik-api.spec.js
@@ -218,7 +218,7 @@
]).then(function (response) {
var restOfExpected = "index.php?method=API.getBulkRequest&module=API&format=JSON2&idSite=1&period=day&date= - "
+ "urls%5B%5D=%3Fmethod%3DSomePlugin.action%26param%3Dvalue&urls%5B%5D=%3Fmethod%3DSomeOtherPlugin.action"
- + "&token_auth=100bf5eeeed1468f3f9d93750044d3dd";
+ + "&token_auth=100bf5eeeed1468f3f9d93750044d3dd&force_api_session=1";
expect(response.length).to.equal(2);
expect(response[0]).to.equal("Response #1: " + restOfExpected);
diff --git a/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js b/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js
index 16a5a91105..d3c35b1115 100644
--- a/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js
+++ b/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js
@@ -113,7 +113,7 @@
}
if (piwik.shouldPropagateTokenAuth && broadcast.getValueFromUrl('token_auth')) {
- url += '&token_auth=' + broadcast.getValueFromUrl('token_auth');
+ url += '&force_api_session=1&token_auth=' + broadcast.getValueFromUrl('token_auth');
}
url += '&random=' + parseInt(Math.random() * 10000);
@@ -198,4 +198,4 @@
}
};
}
-})(); \ No newline at end of file
+})();
diff --git a/plugins/Dashboard/tests/UI/Dashboard_spec.js b/plugins/Dashboard/tests/UI/Dashboard_spec.js
index 6ac640b7a8..b3914f3bc3 100644
--- a/plugins/Dashboard/tests/UI/Dashboard_spec.js
+++ b/plugins/Dashboard/tests/UI/Dashboard_spec.js
@@ -299,7 +299,7 @@ describe("Dashboard", function () {
testEnvironment.testUseMockAuth = 0;
testEnvironment.save();
- var tokenAuth = "9ad1de7f8b329ab919d854c556f860c1";
+ var tokenAuth = "c4ca4238a0b923820dcc509a6f75849b";
await page.goto(url.replace("idDashboard=5", "idDashboard=1") + '&token_auth=' + tokenAuth);
expect(await page.screenshot({ fullPage: true })).to.matchImage('loaded_token_auth');
diff --git a/plugins/Feedback/tests/Integration/FeedbackTest.php b/plugins/Feedback/tests/Integration/FeedbackTest.php
index 072a12186b..33c88e1336 100644
--- a/plugins/Feedback/tests/Integration/FeedbackTest.php
+++ b/plugins/Feedback/tests/Integration/FeedbackTest.php
@@ -39,7 +39,6 @@ class FeedbackTest extends IntegrationTestCase
'a98732d98732',
'user1@example.com',
'user1',
- 'ab9879dc23876f19',
'2019-03-03'
);
FakeAccess::$identity = 'user1';
diff --git a/plugins/Login/Auth.php b/plugins/Login/Auth.php
index 3d572abcf9..a196b48b15 100644
--- a/plugins/Login/Auth.php
+++ b/plugins/Login/Auth.php
@@ -10,6 +10,7 @@ namespace Piwik\Plugins\Login;
use Piwik\AuthResult;
use Piwik\Auth\Password;
+use Piwik\Date;
use Piwik\Piwik;
use Piwik\Plugins\UsersManager\Model;
use Piwik\Plugins\UsersManager\UsersManager;
@@ -76,8 +77,9 @@ class Auth implements \Piwik\Auth
if ($this->passwordHelper->needsRehash($user['password'])) {
$newPasswordHash = $this->passwordHelper->hash($passwordHash);
- $this->userModel->updateUser($login, $newPasswordHash, $user['email'], $user['alias'], $user['token_auth']);
+ $this->userModel->updateUser($login, $newPasswordHash, $user['email'], $user['alias']);
}
+ $this->token_auth = null; // make sure to generate a random token
return $this->authenticationSuccess($user);
}
@@ -90,6 +92,7 @@ class Auth implements \Piwik\Auth
$user = $this->userModel->getUserByTokenAuth($token);
if (!empty($user['login'])) {
+ $this->userModel->setTokenAuthWasUsed($token, Date::now()->getDatetime());
return $this->authenticationSuccess($user);
}
@@ -98,13 +101,10 @@ class Auth implements \Piwik\Auth
private function authenticateWithLoginAndToken($token, $login)
{
- $user = $this->userModel->getUser($login);
+ $user = $this->userModel->getUserByTokenAuth($token);
- if (!empty($user['token_auth'])
- // authenticate either with the token or the "hash token"
- && ((SessionInitializer::getHashTokenAuth($login, $user['token_auth']) === $token)
- || $user['token_auth'] === $token)
- ) {
+ if (!empty($user['login']) && $user['login'] === $login) {
+ $this->userModel->setTokenAuthWasUsed($token, Date::now()->getDatetime());
return $this->authenticationSuccess($user);
}
@@ -113,12 +113,15 @@ class Auth implements \Piwik\Auth
private function authenticationSuccess(array $user)
{
- $this->setTokenAuth($user['token_auth']);
+ if (empty($this->token_auth)) {
+ $this->token_auth = $this->userModel->generateRandomTokenAuth();
+ // we generated one randomly which will then be stored in the session and used across the session
+ }
$isSuperUser = (int) $user['superuser_access'];
$code = $isSuperUser ? AuthResult::SUCCESS_SUPERUSER_AUTH_CODE : AuthResult::SUCCESS;
- return new AuthResult($code, $user['login'], $user['token_auth']);
+ return new AuthResult($code, $user['login'], $this->token_auth);
}
/**
diff --git a/plugins/Login/Login.php b/plugins/Login/Login.php
index d60358c709..9f6ed6f820 100644
--- a/plugins/Login/Login.php
+++ b/plugins/Login/Login.php
@@ -41,7 +41,7 @@ class Login extends \Piwik\Plugin
// for brute force prevention of all tracking + reporting api requests
'Request.initAuthenticationObject' => 'onInitAuthenticationObject',
- 'API.UsersManager.getTokenAuth' => 'beforeLoginCheckBruteForce', // doesn't require auth but can be used to authenticate
+ 'API.UsersManager.createAppSpecificTokenAuth' => 'beforeLoginCheckBruteForce', // doesn't require auth but can be used to authenticate
// for brute force prevention of all UI requests
'Controller.Login.logme' => 'beforeLoginCheckBruteForce',
diff --git a/plugins/Login/SessionInitializer.php b/plugins/Login/SessionInitializer.php
index 0b31f62114..b997358a4b 100644
--- a/plugins/Login/SessionInitializer.php
+++ b/plugins/Login/SessionInitializer.php
@@ -182,8 +182,6 @@ class SessionInitializer
protected function processSuccessfulSession(AuthResult $authResult, $rememberMe)
{
$cookie = $this->getAuthCookie($rememberMe);
- $cookie->set('login', $authResult->getIdentity());
- $cookie->set('token_auth', $this->getHashTokenAuth($authResult->getIdentity(), $authResult->getTokenAuth()));
$cookie->setSecure(ProxyHttp::isHttps());
$cookie->setHttpOnly(true);
$cookie->save();
diff --git a/plugins/Login/tests/Integration/LoginTest.php b/plugins/Login/tests/Integration/LoginTest.php
index a0351b5ea1..8a45a53096 100644
--- a/plugins/Login/tests/Integration/LoginTest.php
+++ b/plugins/Login/tests/Integration/LoginTest.php
@@ -11,6 +11,7 @@ namespace Piwik\Plugins\Login\tests\Integration;
use Piwik\AuthResult;
use Piwik\Common;
use Piwik\Config;
+use Piwik\Date;
use Piwik\DbHelper;
use Piwik\NoAccessException;
use Piwik\Plugins\Login\Auth;
@@ -243,6 +244,17 @@ class LoginTest extends IntegrationTestCase
$this->assertSuperUserLogin($rc, 'user');
}
+ public function test_authenticate_successUserLoginAndTokenAuthWithAnonymous()
+ {
+ DbHelper::createAnonymousUser();
+
+ $user = $this->_setUpUser();
+
+ // valid login & token auth
+ $rc = $this->authenticate('anonymous', 'anonymous');
+ $this->assertUserLogin($rc, 'anonymous', strlen('anonymous'));
+ }
+
public function test_authenticate_successUserLoginAndTokenAuth()
{
$user = $this->_setUpUser();
@@ -271,7 +283,8 @@ class LoginTest extends IntegrationTestCase
$rc = $this->auth->authenticate();
$this->assertUserLogin($rc);
// Check that the token auth is correct in the result
- $this->assertEquals($user['tokenAuth'], $rc->getTokenAuth());
+ $this->assertEquals(32, strlen($rc->getTokenAuth()));
+ $this->assertTrue(ctype_xdigit($rc->getTokenAuth()));
}
public function test_authenticate_successWithSuperUserPassword()
@@ -307,7 +320,8 @@ class LoginTest extends IntegrationTestCase
$rc = $this->auth->authenticate();
$this->assertUserLogin($rc);
// Check that the token auth is correct in the result
- $this->assertEquals($user['tokenAuth'], $rc->getTokenAuth());
+ $this->assertEquals(32, strlen($rc->getTokenAuth()));
+ $this->assertTrue(ctype_xdigit($rc->getTokenAuth()));
}
/**
@@ -324,7 +338,8 @@ class LoginTest extends IntegrationTestCase
$this->assertUserLogin($rc);
// Check that the login + token auth is correct in the result
$this->assertEquals($user['login'], $rc->getIdentity());
- $this->assertEquals($user['tokenAuth'], $rc->getTokenAuth());
+ $this->assertEquals(32, strlen($rc->getTokenAuth()));
+ $this->assertTrue(ctype_xdigit($rc->getTokenAuth()));
}
protected function _setUpUser()
@@ -338,9 +353,10 @@ class LoginTest extends IntegrationTestCase
API::getInstance()->addUser($user['login'], $user['password'], $user['email'], $user['alias']);
$model = new \Piwik\Plugins\UsersManager\Model();
- $dbUser = $model->getUser($user['login']);
+ $tokenAuth = $model->generateRandomTokenAuth();
+ $model->addTokenAuth($user['login'], $tokenAuth, 'many users test', Date::now()->getDatetime());
- $user['tokenAuth'] = $dbUser['token_auth'];
+ $user['tokenAuth'] = $tokenAuth;
return $user;
}
diff --git a/plugins/Login/tests/Integration/SessionInitializerTest.php b/plugins/Login/tests/Integration/SessionInitializerTest.php
index c2215de7a9..7be28c174c 100644
--- a/plugins/Login/tests/Integration/SessionInitializerTest.php
+++ b/plugins/Login/tests/Integration/SessionInitializerTest.php
@@ -36,6 +36,7 @@ class SessionInitializerTest extends IntegrationTestCase
$this->assertAuthCookieIsAbsent();
$sessionInitializer = new TestSessionInitializer();
+ $this->assertEmpty($sessionInitializer->cookie);
$sessionInitializer->initSession($this->makeMockAuth(AuthResult::SUCCESS), true);
$this->assertAuthCookieIsCreated($sessionInitializer->cookie);
@@ -69,8 +70,7 @@ class SessionInitializerTest extends IntegrationTestCase
private function assertAuthCookieIsCreated(Cookie $cookie)
{
- self::assertStringContainsString('login=czo5OiJ0ZXN0bG9naW4iOw==:token_auth=czozMjoiOWU5MDYxZjk2MDI0YTY3NWFmOGFkNWZmNmNiZGY2ZGMiOw==',
- $cookie->generateContentString());
+ $this->assertSame('', $cookie->generateContentString());
}
private function createAuthCookie()
diff --git a/plugins/Login/tests/UI/expected-screenshots/Login_bruteforcelog_noentries.png b/plugins/Login/tests/UI/expected-screenshots/Login_bruteforcelog_noentries.png
index 02fe11e89e..7c129e3f46 100644
--- a/plugins/Login/tests/UI/expected-screenshots/Login_bruteforcelog_noentries.png
+++ b/plugins/Login/tests/UI/expected-screenshots/Login_bruteforcelog_noentries.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:524375b5af2cbabc68f4973e66340276339deb907f9f0406a36f4fefc6fae014
-size 88970
+oid sha256:11249006c3f0eca81d59dc3b0334283d736d4d1e5c47ee698b53dc7a6b1acb2a
+size 89496
diff --git a/plugins/Login/tests/UI/expected-screenshots/Login_bruteforcelog_withentries.png b/plugins/Login/tests/UI/expected-screenshots/Login_bruteforcelog_withentries.png
index 93e8f1d9aa..0233d76503 100644
--- a/plugins/Login/tests/UI/expected-screenshots/Login_bruteforcelog_withentries.png
+++ b/plugins/Login/tests/UI/expected-screenshots/Login_bruteforcelog_withentries.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6fa4a84825aafc43395995ee62a3f7d4830506381d782ddbd841168fee818cc3
-size 106683
+oid sha256:3e698b7fa581131093a71889d3fecee464d1f1ecb830a058aad8fa1fb3db8765
+size 107286
diff --git a/plugins/Morpheus/javascripts/ajaxHelper.js b/plugins/Morpheus/javascripts/ajaxHelper.js
index 03d15be891..38ec2f287e 100644
--- a/plugins/Morpheus/javascripts/ajaxHelper.js
+++ b/plugins/Morpheus/javascripts/ajaxHelper.js
@@ -492,7 +492,8 @@ function ajaxHelper() {
this._getDefaultPostParams = function () {
if (this.withToken || this._isRequestToApiMethod() || piwik.shouldPropagateTokenAuth) {
return {
- token_auth: piwik.token_auth
+ token_auth: piwik.token_auth,
+ force_api_session: '1'
};
}
diff --git a/plugins/Overlay/javascripts/Overlay_Helper.js b/plugins/Overlay/javascripts/Overlay_Helper.js
index c7fe53e986..6f2208d494 100644
--- a/plugins/Overlay/javascripts/Overlay_Helper.js
+++ b/plugins/Overlay/javascripts/Overlay_Helper.js
@@ -29,7 +29,7 @@ var Overlay_Helper = {
var token_auth = piwik.broadcast.getValueFromUrl("token_auth");
if (token_auth.length && piwik.shouldPropagateTokenAuth) {
- url += '&token_auth=' + encodeURIComponent(token_auth);
+ url += '&force_api_session=1&token_auth=' + encodeURIComponent(token_auth);
}
if (link) {
diff --git a/plugins/Overlay/templates/index.twig b/plugins/Overlay/templates/index.twig
index c98d310787..42294e2ae4 100644
--- a/plugins/Overlay/templates/index.twig
+++ b/plugins/Overlay/templates/index.twig
@@ -69,7 +69,7 @@
var iframeSrc = 'index.php?module=Overlay&action=startOverlaySession&idSite={{ idSite }}&period={{ period }}&date={{ rawDate }}&segment={{ segment }}';
if (piwik.shouldPropagateTokenAuth) {
- iframeSrc += '&token_auth=' + piwik.token_auth;
+ iframeSrc += '&force_api_session=1&token_auth=' + piwik.token_auth;
}
Piwik_Overlay.init(iframeSrc, '{{ idSite }}', '{{ period }}', '{{ rawDate }}', '{{ segment }}');
diff --git a/plugins/Overlay/templates/index_noframe.twig b/plugins/Overlay/templates/index_noframe.twig
index 78c18bf281..c3f32be6b6 100644
--- a/plugins/Overlay/templates/index_noframe.twig
+++ b/plugins/Overlay/templates/index_noframe.twig
@@ -8,7 +8,7 @@
<script type="text/javascript">
var newLocation = 'index.php?module=Overlay&action=startOverlaySession&idSite={{ idSite }}&period={{ period }}&date={{ date }}&segment={{ segment }}';
if (piwik.shouldPropagateTokenAuth) {
- newLocation += '&token_auth=' + piwik.token_auth;
+ newLocation += '&force_api_session=1&token_auth=' + piwik.token_auth;
}
var locationParts = window.location.href.split('#');
diff --git a/plugins/Provider b/plugins/Provider
deleted file mode 160000
-Subproject 65ad2d63ab289ca3dc032a3e4dc4d28c35582cc
diff --git a/plugins/TwoFactorAuth/Controller.php b/plugins/TwoFactorAuth/Controller.php
index bb37cd5a64..cb9dedf7ab 100644
--- a/plugins/TwoFactorAuth/Controller.php
+++ b/plugins/TwoFactorAuth/Controller.php
@@ -148,7 +148,7 @@ class Controller extends \Piwik\Plugin\Controller
$this->twoFa->disable2FAforUser(Piwik::getCurrentUserLogin());
$this->passwordVerify->forgetVerifiedPassword();
- $this->redirectToIndex('UsersManager', 'userSettings', null, null, null, array(
+ $this->redirectToIndex('UsersManager', 'userSecurity', null, null, null, array(
'disableNonce' => false
));
}
diff --git a/plugins/TwoFactorAuth/TwoFactorAuth.php b/plugins/TwoFactorAuth/TwoFactorAuth.php
index 9b86925b36..f5d17aff94 100644
--- a/plugins/TwoFactorAuth/TwoFactorAuth.php
+++ b/plugins/TwoFactorAuth/TwoFactorAuth.php
@@ -32,9 +32,9 @@ class TwoFactorAuth extends \Piwik\Plugin
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
'API.UsersManager.deleteUser.end' => 'deleteRecoveryCodes',
- 'API.UsersManager.getTokenAuth.end' => 'onApiGetTokenAuth',
+ 'API.UsersManager.createAppSpecificTokenAuth.end' => 'onCreateAppSpecificTokenAuth',
'Request.dispatch.end' => array('function' => 'onRequestDispatchEnd', 'after' => true),
- 'Template.userSettings.afterTokenAuth' => 'render2FaUserSettings',
+ 'Template.userSecurity.afterPassword' => 'render2FaUserSettings',
'Login.authenticate.processSuccessfulSession.end' => 'onSuccessfulSession'
);
}
@@ -107,7 +107,7 @@ class TwoFactorAuth extends \Piwik\Plugin
return !empty($user);
}
- public function onApiGetTokenAuth($returnedValue, $params)
+ public function onCreateAppSpecificTokenAuth($returnedValue, $params)
{
if (!SettingsPiwik::isMatomoInstalled()) {
return;
diff --git a/plugins/TwoFactorAuth/templates/setupFinished.twig b/plugins/TwoFactorAuth/templates/setupFinished.twig
index 456e8f5189..02dec629f1 100644
--- a/plugins/TwoFactorAuth/templates/setupFinished.twig
+++ b/plugins/TwoFactorAuth/templates/setupFinished.twig
@@ -6,6 +6,6 @@
</h2>
<h3>{{ 'TwoFactorAuth_SetupFinishedSubtitle'|translate }}</h3>
<p><br />
- <a class="btn" href="{{ linkTo({'module': 'UsersManager', 'action': 'userSettings'}) }}">{{ 'General_Continue'|translate }}</a></p>
+ <a class="btn" href="{{ linkTo({'module': 'UsersManager', 'action': 'userSecurity'}) }}">{{ 'General_Continue'|translate }}</a></p>
</div>
{% endblock %}
diff --git a/plugins/TwoFactorAuth/tests/Fixtures/TwoFactorFixture.php b/plugins/TwoFactorAuth/tests/Fixtures/TwoFactorFixture.php
index f5b047ced5..181595f582 100644
--- a/plugins/TwoFactorAuth/tests/Fixtures/TwoFactorFixture.php
+++ b/plugins/TwoFactorAuth/tests/Fixtures/TwoFactorFixture.php
@@ -82,7 +82,7 @@ class TwoFactorFixture extends Fixture
if ($this->userWith2Fa === $user) {
$userModel = new Model();
- $userModel->updateUserTokenAuth($user, 'c4ca4238a0b923820dcc509a6f75849b');
+ $userModel->addTokenAuth($user, 'c4ca4238a0b923820dcc509a6f75849b', 'twofa test', Date::now()->getDatetime());
}
}
diff --git a/plugins/TwoFactorAuth/tests/Integration/TwoFactorAuthTest.php b/plugins/TwoFactorAuth/tests/Integration/TwoFactorAuthTest.php
index 797620443c..1df67e1e36 100644
--- a/plugins/TwoFactorAuth/tests/Integration/TwoFactorAuthTest.php
+++ b/plugins/TwoFactorAuth/tests/Integration/TwoFactorAuthTest.php
@@ -69,53 +69,59 @@ class TwoFactorAuthTest extends IntegrationTestCase
unset($_GET['authCode']);
}
- public function test_onApiGetTokenAuth_canAuthenticateWhenUserNotUsesTwoFA()
+ public function test_onCreateAppSpecificTokenAuth_canAuthenticateWhenUserNotUsesTwoFA()
{
- $token = Request::processRequest('UsersManager.getTokenAuth', array(
+ $token = Request::processRequest('UsersManager.createAppSpecificTokenAuth', array(
'userLogin' => $this->userWithout2Fa,
- 'md5Password' => md5($this->userPassword)
+ 'md5Password' => md5($this->userPassword),
+ 'description' => 'twofa test'
));
$this->assertEquals(32, strlen($token));
}
- public function test_onApiGetTokenAuth_returnsRandomTokenWhenNotAuthenticatedEvenWhen2FAenabled()
+ public function test_onCreateAppSpecificTokenAuth_returnsRandomTokenWhenNotAuthenticatedEvenWhen2FAenabled()
{
- $token = Request::processRequest('UsersManager.getTokenAuth', array(
+ $token = Request::processRequest('UsersManager.createAppSpecificTokenAuth', array(
'userLogin' => $this->userWith2Fa,
- 'md5Password' => md5('invalidPAssword')
+ 'md5Password' => md5('invalidPAssword'),
+ 'description' => 'twofa test'
));
$this->assertEquals(32, strlen($token));
}
- public function test_onApiGetTokenAuth_throwsErrorWhenMissingTokenWhenUsing2FaAndAuthenticatedCorrectly()
+ public function test_onCreateAppSpecificTokenAuth_throwsErrorWhenMissingTokenWhenUsing2FaAndAuthenticatedCorrectly()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('TwoFactorAuth_MissingAuthCodeAPI');
- Request::processRequest('UsersManager.getTokenAuth', array(
+ Request::processRequest('UsersManager.createAppSpecificTokenAuth', array(
+
'userLogin' => $this->userWith2Fa,
- 'md5Password' => md5($this->userPassword)
+ 'md5Password' => md5($this->userPassword),
+ 'description' => 'twofa test'
));
}
- public function test_onApiGetTokenAuth_throwsErrorWhenInvalidTokenWhenUsing2FaAndAuthenticatedCorrectly()
+ public function test_onCreateAppSpecificTokenAuth_throwsErrorWhenInvalidTokenWhenUsing2FaAndAuthenticatedCorrectly()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('TwoFactorAuth_InvalidAuthCode');
$_GET['authCode'] = '111222';
- Request::processRequest('UsersManager.getTokenAuth', array(
+ Request::processRequest('UsersManager.createAppSpecificTokenAuth', array(
'userLogin' => $this->userWith2Fa,
- 'md5Password' => md5($this->userPassword)
+ 'md5Password' => md5($this->userPassword),
+ 'description' => 'twofa test'
));
}
- public function test_onApiGetTokenAuth_returnsCorrectTokenWhenProvidingCorrectAuthTokenOnAuthentication()
+ public function test_onCreateAppSpecificTokenAuth_returnsCorrectTokenWhenProvidingCorrectAuthTokenOnAuthentication()
{
$_GET['authCode'] = $this->generateValidAuthCode($this->user2faSecret);
- $token = Request::processRequest('UsersManager.getTokenAuth', array(
+ $token = Request::processRequest('UsersManager.createAppSpecificTokenAuth', array(
'userLogin' => $this->userWith2Fa,
- 'md5Password' => md5($this->userPassword)
+ 'md5Password' => md5($this->userPassword),
+ 'description' => 'twofa test'
));
$this->assertEquals(32, strlen($token));
}
diff --git a/plugins/TwoFactorAuth/tests/UI/TwoFactorAuth_spec.js b/plugins/TwoFactorAuth/tests/UI/TwoFactorAuth_spec.js
index 368ec0632f..cdfcb10c95 100644
--- a/plugins/TwoFactorAuth/tests/UI/TwoFactorAuth_spec.js
+++ b/plugins/TwoFactorAuth/tests/UI/TwoFactorAuth_spec.js
@@ -13,7 +13,7 @@ describe("TwoFactorAuth", function () {
this.fixture = "Piwik\\Plugins\\TwoFactorAuth\\tests\\Fixtures\\TwoFactorFixture";
var generalParams = 'idSite=1&period=day&date=2010-01-03',
- userSettings = '?module=UsersManager&action=userSettings&' + generalParams,
+ userSettings = '?module=UsersManager&action=userSecurity&' + generalParams,
logoutUrl = '?module=Login&action=logout&period=day&date=yesterday';
diff --git a/plugins/UserCountry/Columns/City.php b/plugins/UserCountry/Columns/City.php
index 95bf56cb1c..b9f8d0a38a 100644
--- a/plugins/UserCountry/Columns/City.php
+++ b/plugins/UserCountry/Columns/City.php
@@ -33,8 +33,8 @@ class City extends Base
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
$value = $this->getUrlOverrideValueIfAllowed('city', $request);
-
if ($value !== false) {
+ $value = substr($value, 0, 255);
return $value;
}
diff --git a/plugins/UserCountry/Columns/Country.php b/plugins/UserCountry/Columns/Country.php
index 608be68e6b..139e987746 100644
--- a/plugins/UserCountry/Columns/Country.php
+++ b/plugins/UserCountry/Columns/Country.php
@@ -82,8 +82,8 @@ class Country extends Base
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
$value = $this->getUrlOverrideValueIfAllowed('country', $request);
-
if ($value !== false) {
+ $value = substr($value, 0, 3);
return $value;
}
diff --git a/plugins/UserCountry/Columns/Region.php b/plugins/UserCountry/Columns/Region.php
index 71af217b53..99f578ae73 100644
--- a/plugins/UserCountry/Columns/Region.php
+++ b/plugins/UserCountry/Columns/Region.php
@@ -33,8 +33,8 @@ class Region extends Base
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
$value = $this->getUrlOverrideValueIfAllowed('region', $request);
-
if ($value !== false) {
+ $value = substr($value, 0, 3);
return $value;
}
diff --git a/plugins/UserCountryMap/javascripts/realtime-map.js b/plugins/UserCountryMap/javascripts/realtime-map.js
index 39d97a7497..2f42048818 100644
--- a/plugins/UserCountryMap/javascripts/realtime-map.js
+++ b/plugins/UserCountryMap/javascripts/realtime-map.js
@@ -148,7 +148,7 @@
return $.ajax({
url: 'index.php?' + $.param(params),
dataType: 'json',
- data: { token_auth: tokenAuth },
+ data: { token_auth: tokenAuth, force_api_session: '1' },
type: 'POST'
});
}
diff --git a/plugins/UserCountryMap/javascripts/visitor-map.js b/plugins/UserCountryMap/javascripts/visitor-map.js
index 396fac73a3..82bdb13852 100644
--- a/plugins/UserCountryMap/javascripts/visitor-map.js
+++ b/plugins/UserCountryMap/javascripts/visitor-map.js
@@ -122,7 +122,7 @@
return $.ajax({
url: 'index.php?' + $.param(params),
dataType: dataType,
- data: { token_auth: token_auth },
+ data: { token_auth: token_auth, force_api_session: '1' },
type: 'POST'
});
}
diff --git a/plugins/UsersManager/API.php b/plugins/UsersManager/API.php
index 08cf1312c0..c2d4dba6aa 100644
--- a/plugins/UsersManager/API.php
+++ b/plugins/UsersManager/API.php
@@ -684,9 +684,8 @@ class API extends \Piwik\Plugin\API
$alias = $this->getCleanAlias($alias, $userLogin);
$passwordTransformed = $this->password->hash($passwordTransformed);
- $token_auth = $this->createTokenAuth($userLogin);
- $this->model->addUser($userLogin, $passwordTransformed, $email, $alias, $token_auth, Date::now()->getDatetime());
+ $this->model->addUser($userLogin, $passwordTransformed, $email, $alias, Date::now()->getDatetime());
// we reload the access list which doesn't yet take in consideration this new user
Access::getInstance()->reloadAccess();
@@ -844,29 +843,6 @@ class API extends \Piwik\Plugin\API
}
/**
- * Regenerate the token_auth associated with a user.
- *
- * If the user currently logged in regenerates his own token, he will be logged out.
- * His previous token will be rendered invalid.
- *
- * @param string $userLogin
- * @throws Exception
- */
- public function regenerateTokenAuth($userLogin)
- {
- $this->checkUserIsNotAnonymous($userLogin);
-
- Piwik::checkUserHasSuperUserAccessOrIsTheUser($userLogin);
-
- $this->model->updateUserTokenAuth(
- $userLogin,
- $this->createTokenAuth($userLogin)
- );
-
- Cache::deleteTrackerCache();
- }
-
- /**
* Updates a user in the database.
* Only login and password are required (case when we update the password).
*
@@ -889,7 +865,6 @@ class API extends \Piwik\Plugin\API
$this->checkUserExists($userLogin);
$userInfo = $this->model->getUser($userLogin);
- $token_auth = $userInfo['token_auth'];
$changeShouldRequirePasswordConfirmation = false;
$passwordHasBeenUpdated = false;
@@ -936,7 +911,7 @@ class API extends \Piwik\Plugin\API
$alias = $this->getCleanAlias($alias, $userLogin);
- $this->model->updateUser($userLogin, $password, $email, $alias, $token_auth);
+ $this->model->updateUser($userLogin, $password, $email, $alias);
Cache::deleteTrackerCache();
@@ -1336,34 +1311,27 @@ class API extends \Piwik\Plugin\API
}
/**
- * Generates a new random authentication token.
- *
- * @param string $userLogin Login
- * @return string
- */
- public function createTokenAuth($userLogin)
- {
- return md5($userLogin . microtime(true) . Common::generateUniqId() . SettingsPiwik::getSalt());
- }
-
- /**
- * Returns the user's API token.
+ * Generates an app specific API token every time you call this method. You should ideally store this token securely
+ * in your app and not generate a new token every time.
*
* If the username/password combination is incorrect an invalid token will be returned.
*
* @param string $userLogin Login or Email address
* @param string $md5Password hashed string of the password (using current hash function; MD5-named for historical reasons)
+ * @param string $description The description for this app specific password, for example your app name. Max 100 characters are allowed
+ * @param string $expireDate Optionally a date when the token should expire
+ * @param string $expireHours Optionally number of hours for how long the token should be valid before it expires.
+ * If expireDate is set and expireHours, then expireDate will be used.
+ * If expireDate is set and expireHours, then expireDate will be used.
* @return string
*/
- public function getTokenAuth($userLogin, $md5Password)
+ public function createAppSpecificTokenAuth($userLogin, $md5Password, $description, $expireDate = null, $expireHours = 0)
{
UsersManager::checkPasswordHash($md5Password, Piwik::translate('UsersManager_ExceptionPasswordMD5HashExpected'));
$user = $this->model->getUser($userLogin);
-
if (empty($user) && Piwik::isValidEmailString($userLogin)) {
$user = $this->model->getUserByEmail($userLogin);
-
if (!empty($user['login'])) {
$userLogin = $user['login'];
}
@@ -1384,7 +1352,16 @@ class API extends \Piwik\Plugin\API
$userUpdater->updateUserWithoutCurrentPassword($userLogin, $this->password->hash($md5Password));
}
- return $user['token_auth'];
+ if (empty($expireDate) && !empty($expireHours) && is_numeric($expireHours)) {
+ $expireDate = Date::now()->addHour($expireHours)->getDatetime();
+ } elseif (!empty($expireDate)) {
+ $expireDate = Date::factory($expireDate)->getDatetime();
+ }
+
+ $generatedToken = $this->model->generateRandomTokenAuth();
+ $this->model->addTokenAuth($userLogin, $generatedToken, $description, Date::now()->getDatetime(), $expireDate);
+
+ return $generatedToken;
}
public function newsletterSignup()
diff --git a/plugins/UsersManager/Controller.php b/plugins/UsersManager/Controller.php
index 49fb7bd563..f75bb81b35 100644
--- a/plugins/UsersManager/Controller.php
+++ b/plugins/UsersManager/Controller.php
@@ -13,31 +13,55 @@ use Piwik\API\Request;
use Piwik\API\ResponseBuilder;
use Piwik\Common;
use Piwik\Container\StaticContainer;
+use Piwik\Date;
+use Piwik\Nonce;
+use Piwik\Notification;
use Piwik\Option;
use Piwik\Piwik;
use Piwik\Plugin;
use Piwik\Plugin\ControllerAdmin;
use Piwik\Plugins\LanguagesManager\API as APILanguagesManager;
use Piwik\Plugins\LanguagesManager\LanguagesManager;
+use Piwik\Plugins\Login\PasswordVerifier;
+use Piwik\Plugins\TagManager\Validators\TriggerIds;
use Piwik\Plugins\UsersManager\API as APIUsersManager;
use Piwik\SettingsPiwik;
use Piwik\Site;
use Piwik\Tracker\IgnoreCookie;
use Piwik\Translation\Translator;
use Piwik\Url;
+use Piwik\Validators\BaseValidator;
+use Piwik\Validators\CharacterLength;
+use Piwik\Validators\NotEmpty;
use Piwik\View;
use Piwik\Session\SessionInitializer;
class Controller extends ControllerAdmin
{
+ const NONCE_CHANGE_PASSWORD = 'changePasswordNonce';
+ const NONCE_ADD_AUTH_TOKEN = 'addAuthTokenNonce';
+ const NONCE_DELETE_AUTH_TOKEN = 'deleteAuthTokenNonce';
+
/**
* @var Translator
*/
private $translator;
- public function __construct(Translator $translator)
+ /**
+ * @var PasswordVerifier
+ */
+ private $passwordVerify;
+
+ /**
+ * @var Model
+ */
+ private $userModel;
+
+ public function __construct(Translator $translator, PasswordVerifier $passwordVerify, Model $userModel)
{
$this->translator = $translator;
+ $this->passwordVerify = $passwordVerify;
+ $this->userModel = $userModel;
parent::__construct();
}
@@ -249,6 +273,112 @@ class Controller extends ControllerAdmin
}
/**
+ * The "User Security" admin UI screen view
+ */
+ public function userSecurity()
+ {
+ Piwik::checkUserIsNotAnonymous();
+
+ $tokens = $this->userModel->getAllNonSystemTokensForLogin(Piwik::getCurrentUserLogin());
+ $tokens = array_map(function ($token){
+ foreach (['date_created', 'last_used', 'date_expired'] as $key) {
+ if (!empty($token[$key])) {
+ $token[$key] = Date::factory($token[$key])->getLocalized(Date::DATE_FORMAT_LONG);
+ }
+ }
+
+ return $token;
+ }, $tokens);
+ $hasTokensWithExpireDate = !empty(array_filter(array_column($tokens, 'date_expired')));
+
+ return $this->renderTemplate('userSecurity', array(
+ 'isUsersAdminEnabled' => UsersManager::isUsersAdminEnabled(),
+ 'changePasswordNonce' => Nonce::getNonce(self::NONCE_CHANGE_PASSWORD),
+ 'deleteTokenNonce' => Nonce::getNonce(self::NONCE_DELETE_AUTH_TOKEN),
+ 'hasTokensWithExpireDate' => $hasTokensWithExpireDate,
+ 'tokens' => $tokens
+ ));
+ }
+
+ /**
+ * The "User Security" admin UI screen view
+ */
+ public function deleteToken()
+ {
+ Piwik::checkUserIsNotAnonymous();
+
+ $idTokenAuth = Common::getRequestVar('idtokenauth', '', 'string', $_POST);
+
+ if (!empty($idTokenAuth)) {
+ $params = array(
+ 'module' => 'UsersManager',
+ 'action' => 'deleteToken',
+ 'idtokenauth' => $idTokenAuth,
+ 'nonce' => Nonce::getNonce(self::NONCE_DELETE_AUTH_TOKEN)
+ );
+
+ if (!$this->passwordVerify->requirePasswordVerifiedRecently($params)) {
+ throw new Exception('Not allowed');
+ }
+
+ Nonce::checkNonce(self::NONCE_DELETE_AUTH_TOKEN);
+
+ if ($idTokenAuth === 'all') {
+ $this->userModel->deleteAllTokensForUser(Piwik::getCurrentUserLogin());
+
+ $notification = new Notification(Piwik::translate('UsersManager_TokensSuccessfullyDeleted'));
+ $notification->context = Notification::CONTEXT_SUCCESS;
+ Notification\Manager::notify('successdeletetokens', $notification);
+
+ } elseif (is_numeric($idTokenAuth)) {
+ $this->userModel->deleteToken($idTokenAuth, Piwik::getCurrentUserLogin());
+
+ $notification = new Notification(Piwik::translate('UsersManager_TokenSuccessfullyDeleted'));
+ $notification->context = Notification::CONTEXT_SUCCESS;
+ Notification\Manager::notify('successdeletetoken', $notification);
+ }
+ }
+
+ $this->redirectToIndex('UsersManager', 'userSecurity');
+ }
+
+ /**
+ * The "User Security" admin UI screen view
+ */
+ public function addNewToken()
+ {
+ Piwik::checkUserIsNotAnonymous();
+
+ $params = array('module' => 'UsersManager', 'action' => 'addNewToken');
+
+ if (!$this->passwordVerify->requirePasswordVerifiedRecently($params)) {
+ throw new Exception('Not allowed');
+ }
+
+ $noDescription = false;
+
+ if (!empty($_POST['description'])) {
+ Nonce::checkNonce(self::NONCE_ADD_AUTH_TOKEN);
+
+ $description = Common::getRequestVar('description', '', 'string');
+ $login = Piwik::getCurrentUserLogin();
+
+ $generatedToken = $this->userModel->generateRandomTokenAuth();
+
+ $this->userModel->addTokenAuth($login, $generatedToken, $description, Date::now()->getDatetime());
+
+ return $this->renderTemplate('addNewTokenSuccess', array('generatedToken' => $generatedToken));
+ } elseif (isset($_POST['description'])) {
+ $noDescription = true;
+ }
+
+ return $this->renderTemplate('addNewToken', array(
+ 'nonce' => Nonce::getNonce(self::NONCE_ADD_AUTH_TOKEN),
+ 'noDescription' => $noDescription
+ ));
+ }
+
+ /**
* The "Anonymous Settings" admin UI screen view
*/
public function anonymousSettings()
@@ -389,9 +519,7 @@ class Controller extends ControllerAdmin
Piwik::checkUserHasSuperUserAccessOrIsTheUser($userLogin);
- if (UsersManager::isUsersAdminEnabled()) {
- $this->processPasswordChange($userLogin);
- }
+ $this->processEmailChange($userLogin);
LanguagesManager::setLanguageForSession($language);
@@ -418,6 +546,26 @@ class Controller extends ControllerAdmin
return $toReturn;
}
+
+ /**
+ * Records settings from the "User Settings" page
+ * @throws Exception
+ */
+ public function recordPasswordChange()
+ {
+ $userLogin = Piwik::getCurrentUserLogin();
+
+ Piwik::checkUserHasSuperUserAccessOrIsTheUser($userLogin);
+ Nonce::checkNonce(self::NONCE_CHANGE_PASSWORD);
+
+ $this->processPasswordChange($userLogin);
+
+ $notification = new Notification(Piwik::translate('CoreAdminHome_SettingsSaveSuccess'));
+ $notification->context = Notification::CONTEXT_SUCCESS;
+ Notification\Manager::notify('successpass', $notification);
+ $this->redirectToIndex('UsersManager', 'userSecurity');
+ }
+
private function noAdminAccessToWebsite($idSiteSelected, $defaultReportSiteName, $message)
{
$view = new View('@UsersManager/noWebsiteAdminAccess');
@@ -430,43 +578,60 @@ class Controller extends ControllerAdmin
return $view->render();
}
- private function processPasswordChange($userLogin)
+ private function processEmailChange($userLogin)
{
- $email = Common::getRequestVar('email');
- $password = Common::getRequestVar('password', false);
- $passwordBis = Common::getRequestVar('passwordBis', false);
- $passwordCurrent = Common::getRequestVar('passwordConfirmation', false);
-
- $newPassword = false;
- if (!empty($password) || !empty($passwordBis)) {
- if ($password != $passwordBis) {
- throw new Exception($this->translator->translate('Login_PasswordsDoNotMatch'));
- }
- $newPassword = $password;
+ if (!UsersManager::isUsersAdminEnabled()) {
+ return;
}
- if ($newPassword !== false && !Url::isValidHost()) {
- throw new Exception("Cannot change password or email with untrusted hostname!");
+ if (!Url::isValidHost()) {
+ throw new Exception("Cannot change email with untrusted hostname!");
}
-
+
+ $email = Common::getRequestVar('email');
+ $passwordCurrent = Common::getRequestvar('passwordConfirmation', false);
+
// UI disables password change on invalid host, but check here anyway
Request::processRequest('UsersManager.updateUser', [
'userLogin' => $userLogin,
- 'password' => $newPassword,
'email' => $email,
'passwordConfirmation' => $passwordCurrent
], $default = []);
+ }
+
+ private function processPasswordChange($userLogin)
+ {
+ if (!UsersManager::isUsersAdminEnabled()) {
+ return;
+ }
+
+ if (!Url::isValidHost()) {
+ // UI disables password change on invalid host, but check here anyway
+ throw new Exception("Cannot change password with untrusted hostname!");
+ }
- if ($newPassword !== false) {
- // logs the user in with the new password
- $newPassword = Common::unsanitizeInputValue($newPassword);
- $sessionInitializer = new SessionInitializer();
- $auth = StaticContainer::get('Piwik\Auth');
- $auth->setTokenAuth(null); // ensure authenticated through password
- $auth->setLogin($userLogin);
- $auth->setPassword($newPassword);
- $sessionInitializer->initSession($auth);
+ $newPassword = Common::getRequestvar('password', false);
+ $passwordBis = Common::getRequestvar('passwordBis', false);
+ $passwordCurrent = Common::getRequestvar('passwordConfirmation', false);
+
+ if ($newPassword !== $passwordBis) {
+ throw new Exception($this->translator->translate('Login_PasswordsDoNotMatch'));
}
+
+ Request::processRequest('UsersManager.updateUser', [
+ 'userLogin' => $userLogin,
+ 'password' => $newPassword,
+ 'passwordConfirmation' => $passwordCurrent
+ ], $default = []);
+
+ // logs the user in with the new password
+ $newPassword = Common::unsanitizeInputValue($newPassword);
+ $sessionInitializer = new SessionInitializer();
+ $auth = StaticContainer::get('Piwik\Auth');
+ $auth->setTokenAuth(null); // ensure authenticated through password
+ $auth->setLogin($userLogin);
+ $auth->setPassword($newPassword);
+ $sessionInitializer->initSession($auth);
}
/**
diff --git a/plugins/UsersManager/Menu.php b/plugins/UsersManager/Menu.php
index 7945480d4e..25d345f52c 100644
--- a/plugins/UsersManager/Menu.php
+++ b/plugins/UsersManager/Menu.php
@@ -25,6 +25,7 @@ class Menu extends \Piwik\Plugin\Menu
if (!Piwik::isUserIsAnonymous()) {
$menu->addItem('UsersManager_MenuPersonal', 'General_Settings', $this->urlForAction('userSettings'), 0);
+ $menu->addItem('UsersManager_MenuPersonal', 'General_Security', $this->urlForAction('userSecurity'), 1);
}
}
}
diff --git a/plugins/UsersManager/Model.php b/plugins/UsersManager/Model.php
index 839fdbd578..15dd22c40b 100644
--- a/plugins/UsersManager/Model.php
+++ b/plugins/UsersManager/Model.php
@@ -10,6 +10,7 @@ namespace Piwik\Plugins\UsersManager;
use Piwik\Auth\Password;
use Piwik\Common;
+use Piwik\Config;
use Piwik\Date;
use Piwik\Db;
use Piwik\Option;
@@ -17,6 +18,10 @@ use Piwik\Piwik;
use Piwik\Plugins\SitesManager\SitesManager;
use Piwik\Plugins\UsersManager\Sql\SiteAccessFilter;
use Piwik\Plugins\UsersManager\Sql\UserTableFilter;
+use Piwik\SettingsPiwik;
+use Piwik\Validators\BaseValidator;
+use Piwik\Validators\CharacterLength;
+use Piwik\Validators\NotEmpty;
/**
* The UsersManager API lets you Manage Users and their permissions to access specific websites.
@@ -32,8 +37,12 @@ use Piwik\Plugins\UsersManager\Sql\UserTableFilter;
*/
class Model
{
+ const MAX_LENGTH_TOKEN_DESCRIPTION = 100;
+ const TOKEN_HASH_ALGO = 'sha512';
+
private static $rawPrefix = 'user';
- private $table;
+ private $userTable;
+ private $tokenTable;
/**
* @var Password
@@ -43,7 +52,8 @@ class Model
public function __construct()
{
$this->passwordHelper = new Password();
- $this->table = Common::prefixTable(self::$rawPrefix);
+ $this->userTable = Common::prefixTable(self::$rawPrefix);
+ $this->tokenTable = Common::prefixTable('user_token_auth');
}
/**
@@ -63,7 +73,7 @@ class Model
}
$db = $this->getDb();
- $users = $db->fetchAll("SELECT * FROM " . $this->table . "
+ $users = $db->fetchAll("SELECT * FROM " . $this->userTable . "
$where
ORDER BY login ASC", $bind);
@@ -78,7 +88,7 @@ class Model
public function getUsersLogin()
{
$db = $this->getDb();
- $users = $db->fetchAll("SELECT login FROM " . $this->table . " ORDER BY login ASC");
+ $users = $db->fetchAll("SELECT login FROM " . $this->userTable . " ORDER BY login ASC");
$return = array();
foreach ($users as $login) {
@@ -229,7 +239,7 @@ class Model
{
$db = $this->getDb();
- $matchedUsers = $db->fetchAll("SELECT * FROM {$this->table} WHERE login = ?", $userLogin);
+ $matchedUsers = $db->fetchAll("SELECT * FROM {$this->userTable} WHERE login = ?", $userLogin);
// for BC in 2.15 LTS, if there is a user w/ an exact match to the requested login, return that user.
// this is done since before this change, login was case sensitive. until 3.0, we want to maintain
@@ -243,33 +253,190 @@ class Model
return reset($matchedUsers);
}
+ public function hashTokenAuth($tokenAuth)
+ {
+ $salt = SettingsPiwik::getSalt();
+ return hash(self::TOKEN_HASH_ALGO, $tokenAuth . $salt);
+ }
+
+ public function generateRandomTokenAuth()
+ {
+ $count = 0;
+
+ do {
+ $token = $this->generateTokenAuth();
+
+ $count++;
+ if ($count > 20) {
+ // something seems wrong as the odds of that happening is basically 0. Only catching it to prevent
+ // endless loop in case there is some bug somewhere
+ throw new \Exception('Failed to generate token');
+ }
+
+ } while ($this->getUserByTokenAuth($token));
+
+ return $token;
+ }
+
+ private function generateTokenAuth()
+ {
+ return md5(Common::getRandomString(32, 'abcdef1234567890') . microtime(true) . Common::generateUniqId() . SettingsPiwik::getSalt());
+ }
+
+ public function addTokenAuth($login, $tokenAuth, $description, $dateCreated, $dateExpired = null, $isSystemToken = false)
+ {
+ if (!$this->getUser($login)) {
+ throw new \Exception('User ' . $login . ' does not exist');
+ }
+
+ BaseValidator::check('Description', $description, [new NotEmpty(), new CharacterLength(1, self::MAX_LENGTH_TOKEN_DESCRIPTION)]);
+
+ if (empty($dateExpired)) {
+ $dateExpired = null;
+ }
+
+ $isSystemToken = (int) $isSystemToken;
+
+ $insertSql = "INSERT INTO " . $this->tokenTable . ' (login, description, password, date_created, date_expired, system_token, hash_algo) VALUES (?, ?, ?, ?, ?, ?, ?)';
+
+ $tokenAuth = $this->hashTokenAuth($tokenAuth);
+
+ $db = $this->getDb();
+ $db->query($insertSql, [$login, $description, $tokenAuth, $dateCreated, $dateExpired, $isSystemToken, self::TOKEN_HASH_ALGO]);
+
+ return $db->lastInsertId();
+ }
+
+ private function getTokenByTokenAuth($tokenAuth)
+ {
+ $tokenAuth = $this->hashTokenAuth($tokenAuth);
+ $db = $this->getDb();
+
+ return $db->fetchRow("SELECT * FROM " . $this->tokenTable . " WHERE `password` = ?", $tokenAuth);
+ }
+
+ private function getQueryNotExpiredToken()
+ {
+ return array(
+ 'sql' => ' (date_expired is null or date_expired > ?)',
+ 'bind' => array(Date::now()->getDatetime())
+ );
+ }
+
+ private function getTokenByTokenAuthIfNotExpired($tokenAuth)
+ {
+ $tokenAuth = $this->hashTokenAuth($tokenAuth);
+ $db = $this->getDb();
+
+ $expired = $this->getQueryNotExpiredToken();
+ $bind = array_merge(array($tokenAuth), $expired['bind']);
+
+ $token = $db->fetchRow("SELECT * FROM " . $this->tokenTable . " WHERE `password` = ? and " . $expired['sql'], $bind);
+
+ return $token;
+ }
+
+ public function deleteExpiredTokens($expiredSince)
+ {
+ $db = $this->getDb();
+
+ return $db->query("DELETE FROM " . $this->tokenTable . " WHERE `date_expired` is not null and date_expired < ?", $expiredSince);
+ }
+
+ public function deleteAllTokensForUser($login)
+ {
+ $db = $this->getDb();
+
+ return $db->query("DELETE FROM " . $this->tokenTable . " WHERE `login` = ?", $login);
+ }
+
+ public function getAllNonSystemTokensForLogin($login)
+ {
+ $db = $this->getDb();
+
+
+ $expired = $this->getQueryNotExpiredToken();
+ $bind = array_merge(array($login), $expired['bind']);
+
+ return $db->fetchAll("SELECT * FROM " . $this->tokenTable . " WHERE `login` = ? and system_token = 0 and " . $expired['sql'] . ' order by idusertokenauth ASC', $bind);
+ }
+
+ public function getAllHashedTokensForLogins($logins)
+ {
+ if (empty($logins)) {
+ return array();
+ }
+
+ $db = $this->getDb();
+ $placeholder = Common::getSqlStringFieldsArray($logins);
+
+ $expired = $this->getQueryNotExpiredToken();
+ $bind = array_merge($logins, $expired['bind']);
+
+ $tokens = $db->fetchAll("SELECT password FROM " . $this->tokenTable . " WHERE `login` IN (".$placeholder.") and " . $expired['sql'], $bind);
+ return array_column($tokens, 'password');
+ }
+
+ public function deleteToken($idTokenAuth, $login)
+ {
+ $db = $this->getDb();
+
+ return $db->query("DELETE FROM " . $this->tokenTable . " WHERE `idusertokenauth` = ? and login = ?", array($idTokenAuth, $login));
+ }
+
+ public function setTokenAuthWasUsed($tokenAuth, $dateLastUsed)
+ {
+ $token = $this->getTokenByTokenAuth($tokenAuth);
+ if (!empty($token)) {
+ $this->updateTokenAuthTable($token['idusertokenauth'], array(
+ 'last_used' => $dateLastUsed
+ ));
+ }
+ }
+
+ private function updateTokenAuthTable($idTokenAuth, $fields) {
+ $set = array();
+ $bind = array();
+ foreach ($fields as $key => $val) {
+ $set[] = "`$key` = ?";
+ $bind[] = $val;
+ }
+
+ $bind[] = $idTokenAuth;
+
+ $db = $this->getDb();
+ $db->query(sprintf('UPDATE `%s` SET %s WHERE `idusertokenauth` = ?', $this->tokenTable, implode(', ', $set)), $bind);
+ }
+
public function getUserByEmail($userEmail)
{
$db = $this->getDb();
- return $db->fetchRow("SELECT * FROM " . $this->table . " WHERE email = ?", $userEmail);
+ return $db->fetchRow("SELECT * FROM " . $this->userTable . " WHERE email = ?", $userEmail);
}
public function getUserByTokenAuth($tokenAuth)
{
- $db = $this->getDb();
- return $db->fetchRow('SELECT * FROM ' . $this->table . ' WHERE token_auth = ?', $tokenAuth);
+ $token = $this->getTokenByTokenAuthIfNotExpired($tokenAuth);
+ if (!empty($token)) {
+ $db = $this->getDb();
+ return $db->fetchRow("SELECT * FROM " . $this->userTable . " WHERE `login` = ?", $token['login']);
+ }
}
- public function addUser($userLogin, $hashedPassword, $email, $alias, $tokenAuth, $dateRegistered)
+ public function addUser($userLogin, $hashedPassword, $email, $alias, $dateRegistered)
{
$user = array(
'login' => $userLogin,
'password' => $hashedPassword,
'alias' => $alias,
'email' => $email,
- 'token_auth' => $tokenAuth,
'date_registered' => $dateRegistered,
'superuser_access' => 0,
'ts_password_modified' => Date::now()->getDatetime(),
);
$db = $this->getDb();
- $db->insert($this->table, $user);
+ $db->insert($this->userTable, $user);
}
public function setSuperUserAccess($userLogin, $hasSuperUserAccess)
@@ -297,7 +464,7 @@ class Model
$bind[] = $userLogin;
$db = $this->getDb();
- $db->query(sprintf('UPDATE `%s` SET %s WHERE `login` = ?', $this->table, implode(', ', $set)), $bind);
+ $db->query(sprintf('UPDATE `%s` SET %s WHERE `login` = ?', $this->userTable, implode(', ', $set)), $bind);
}
/**
@@ -308,7 +475,7 @@ class Model
public function getUsersHavingSuperUserAccess()
{
$db = $this->getDb();
- $users = $db->fetchAll("SELECT login, email, token_auth, superuser_access
+ $users = $db->fetchAll("SELECT login, email, superuser_access
FROM " . Common::prefixTable("user") . "
WHERE superuser_access = 1
ORDER BY date_registered ASC");
@@ -316,12 +483,11 @@ class Model
return $users;
}
- public function updateUser($userLogin, $hashedPassword, $email, $alias, $tokenAuth)
+ public function updateUser($userLogin, $hashedPassword, $email, $alias)
{
$fields = array(
'alias' => $alias,
'email' => $email,
- 'token_auth' => $tokenAuth
);
if (!empty($hashedPassword)) {
$fields['password'] = $hashedPassword;
@@ -329,17 +495,10 @@ class Model
$this->updateUserFields($userLogin, $fields);
}
- public function updateUserTokenAuth($userLogin, $tokenAuth)
- {
- $this->updateUserFields($userLogin, array(
- 'token_auth' => $tokenAuth
- ));
- }
-
public function userExists($userLogin)
{
$db = $this->getDb();
- $count = $db->fetchOne("SELECT count(*) FROM " . $this->table . " WHERE login = ?", $userLogin);
+ $count = $db->fetchOne("SELECT count(*) FROM " . $this->userTable . " WHERE login = ?", $userLogin);
return $count != 0;
}
@@ -347,7 +506,7 @@ class Model
public function userEmailExists($userEmail)
{
$db = $this->getDb();
- $count = $db->fetchOne("SELECT count(*) FROM " . $this->table . " WHERE email = ?", $userEmail);
+ $count = $db->fetchOne("SELECT count(*) FROM " . $this->userTable . " WHERE email = ?", $userEmail);
return $count != 0;
}
@@ -380,7 +539,8 @@ class Model
public function deleteUserOnly($userLogin)
{
$db = $this->getDb();
- $db->query("DELETE FROM " . $this->table . " WHERE login = ?", $userLogin);
+ $db->query("DELETE FROM " . $this->userTable . " WHERE login = ?", $userLogin);
+ $db->query("DELETE FROM " . $this->tokenTable . " WHERE login = ?", $userLogin);
/**
* Triggered after a user has been deleted.
@@ -428,7 +588,7 @@ class Model
$bind = array_merge($bind, $whereBind);
- $sql = 'SELECT u.login FROM ' . $this->table . " u $joins $where";
+ $sql = 'SELECT u.login FROM ' . $this->userTable . " u $joins $where";
$db = $this->getDb();
@@ -468,7 +628,7 @@ class Model
}
$sql = 'SELECT SQL_CALC_FOUND_ROWS u.*, GROUP_CONCAT(a.access SEPARATOR "|") as access
- FROM ' . $this->table . " u
+ FROM ' . $this->userTable . " u
$joins
$where
GROUP BY u.login
diff --git a/plugins/UsersManager/Tasks.php b/plugins/UsersManager/Tasks.php
index f3f6a35528..29ea9ed3ba 100644
--- a/plugins/UsersManager/Tasks.php
+++ b/plugins/UsersManager/Tasks.php
@@ -8,6 +8,7 @@
namespace Piwik\Plugins\UsersManager;
use Piwik\Access;
+use Piwik\Date;
class Tasks extends \Piwik\Plugin\Tasks
{
@@ -29,9 +30,14 @@ class Tasks extends \Piwik\Plugin\Tasks
public function schedule()
{
+ $this->daily("cleanupExpiredTokens");
$this->daily("setUserDefaultReportPreference");
}
+ public function cleanupExpiredTokens() {
+ $this->usersModel->deleteExpiredTokens(Date::now()->getDatetime());
+ }
+
public function setUserDefaultReportPreference()
{
// We initialize the default report user preference for each user (if it hasn't been inited before) for performance,
diff --git a/plugins/UsersManager/UsersManager.php b/plugins/UsersManager/UsersManager.php
index 0340acda20..405d17c1cf 100644
--- a/plugins/UsersManager/UsersManager.php
+++ b/plugins/UsersManager/UsersManager.php
@@ -43,7 +43,6 @@ class UsersManager extends \Piwik\Plugin
'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
'Platform.initialized' => 'onPlatformInitialized',
'System.addSystemSummaryItems' => 'addSystemSummaryItems',
- 'CronArchive.getTokenAuth' => 'getCronArchiveTokenAuth'
);
}
@@ -94,22 +93,17 @@ class UsersManager extends \Piwik\Plugin
public function recordAdminUsersInCache(&$attributes, $idSite)
{
$model = new Model();
- $adminLogins = $model->getUsersLoginWithSiteAccess($idSite, Admin::ID);
+ $logins = $model->getUsersLoginWithSiteAccess($idSite, Admin::ID);
$writeLogins = $model->getUsersLoginWithSiteAccess($idSite, Write::ID);
+ $logins = array_merge($logins, $writeLogins);
- $attributes['tracking_token_auth'] = array();
+ $token_auths = $model->getAllHashedTokensForLogins($logins);
- if (!empty($adminLogins)) {
- $users = $model->getUsers($adminLogins);
- foreach ($users as $user) {
- $attributes['tracking_token_auth'][] = self::hashTrackingToken($user['token_auth'], $idSite);
- }
- }
+ $attributes['tracking_token_auth'] = array();
- if (!empty($writeLogins)) {
- $users = $model->getUsers($writeLogins);
- foreach ($users as $user) {
- $attributes['tracking_token_auth'][] = self::hashTrackingToken($user['token_auth'], $idSite);
+ if (!empty($token_auths)) {
+ foreach ($token_auths as $token_auth) {
+ $attributes['tracking_token_auth'][] = self::hashTrackingToken($token_auth, $idSite);
}
}
}
@@ -119,16 +113,6 @@ class UsersManager extends \Piwik\Plugin
return sha1($idSite . $tokenAuth . SettingsPiwik::getSalt());
}
- public function getCronArchiveTokenAuth(&$tokens)
- {
- $model = new Model();
- $superUsers = $model->getUsersHavingSuperUserAccess();
-
- foreach($superUsers as $superUser) {
- $tokens[] = $superUser['token_auth'];
- }
- }
-
/**
* Delete user preferences associated with a particular site
*/
@@ -247,7 +231,7 @@ class UsersManager extends \Piwik\Plugin
*/
public static function checkPasswordHash($passwordHash, $exceptionMessage)
{
- if (strlen($passwordHash) != 32) { // MD5 hash length
+ if (strlen($passwordHash) != 32 || !ctype_xdigit($passwordHash)) { // MD5 hash length
throw new Exception($exceptionMessage);
}
}
diff --git a/plugins/UsersManager/angularjs/personal-settings/personal-settings.controller.js b/plugins/UsersManager/angularjs/personal-settings/personal-settings.controller.js
index 635d6e93db..047b08cb10 100644
--- a/plugins/UsersManager/angularjs/personal-settings/personal-settings.controller.js
+++ b/plugins/UsersManager/angularjs/personal-settings/personal-settings.controller.js
@@ -34,7 +34,7 @@
id: 'PersonalSettingsSuccess', context: 'success'});
notification.scrollToNotification();
- self.doesRequirePasswordConfirmation = !!self.password;
+ self.doesRequirePasswordConfirmation = false;
self.passwordCurrent = '';
self.loading = false;
}, function (errorMessage) {
@@ -74,25 +74,6 @@
});
};
- this.regenerateTokenAuth = function () {
- var parameters = { userLogin: piwik.userLogin };
-
- self.loading = true;
-
- piwikHelper.modalConfirm('#confirmTokenRegenerate', {yes: function () {
- piwikApi.withTokenInUrl();
- piwikApi.post({
- module: 'API',
- method: 'UsersManager.regenerateTokenAuth'
- }, parameters).then(function (success) {
- $window.location.reload();
- self.loading = false;
- }, function (errorMessage) {
- self.loading = false;
- });
- }});
- };
-
this.cancelSave = function () {
this.passwordCurrent = '';
};
@@ -116,14 +97,6 @@
timeformat: this.timeformat,
};
- if (this.password) {
- postParams.password = this.password;
- }
-
- if (this.passwordBis) {
- postParams.passwordBis = this.passwordBis;
- }
-
if (this.passwordCurrent) {
postParams.passwordConfirmation = this.passwordCurrent;
}
diff --git a/plugins/UsersManager/lang/en.json b/plugins/UsersManager/lang/en.json
index c8f7e4de47..8f23bae7c7 100644
--- a/plugins/UsersManager/lang/en.json
+++ b/plugins/UsersManager/lang/en.json
@@ -13,6 +13,16 @@
"SaveBasicInfo": "Save Basic Info",
"Alias": "Alias",
"AllWebsites": "All websites",
+ "LastUsed": "Last used",
+ "ExpireDate": "Expire date",
+ "AuthTokens": "Auth tokens",
+ "AuthTokenPurpose": "What are you using this token for?",
+ "NoTokenCreatedYetCreateNow": "No token created yet, %1$screate a new token now%2$s.",
+ "TokenSuccessfullyGenerated": "Token successfully generated",
+ "ConfirmTokenCopied": "I confirm I copied the token.",
+ "GoBackSecurityPage": "Go back to security page.",
+ "PleaseStoreToken": "Please store your token securely as you will not be able to access or see the token again.",
+ "CreateNewToken": "Create new token",
"AnonymousAccessConfirmation": "You are about to grant the anonymous user the 'view' access to this website. This means your analytics reports and your visitors information will be publicly viewable by anyone even without a login. Are you sure you want to proceed?",
"AnonymousUser": "Anonymous user",
"AnonymousUserHasViewAccess": "Note: the %1$s user has %2$s access to this website.",
@@ -90,7 +100,14 @@
"TokenAuth": "API Authentication Token",
"TokenRegenerateConfirmSelf": "Changing the API authentication token will invalidate your own token. If the current token is in use, you need to update all API clients with the newly generated token. Do you really want to change your authentication token?",
"TokenRegenerateTitle": "Regenerate",
+ "TokensSuccessfullyDeleted": "All tokens were successfully deleted",
+ "TokenSuccessfullyDeleted": "Token was successfully deleted",
+ "DeleteAllTokens": "Delete all tokens",
+ "ExpiredTokensDeleteAutomatically": "Tokens with an expire date will be deleted automatically.",
+ "TokensWithExpireDateCreationBySystem": "Tokens with expire date can currently only be created by the system",
+ "TokenAuthIntro": "Tokens you have generated can be used to access the Matomo reporting API, Matomo tracking API, and exported Matomo widgets and have the same permissions as your regular user login. You can use these tokens also for the Matomo Mobile app.",
"TypeYourPasswordAgain": "Type your new password again.",
+ "TypeYourCurrentPassword": "Please type your current password to confirm the password change.",
"User": "User",
"UserHasPermission":"%1$s currently has %2$s access for %3$s.",
"UserHasNoPermission":"%1$s currently has %2$s to %3$s",
diff --git a/plugins/UsersManager/stylesheets/usersManager.less b/plugins/UsersManager/stylesheets/usersManager.less
index 3def95dce6..c3a47dd7bf 100644
--- a/plugins/UsersManager/stylesheets/usersManager.less
+++ b/plugins/UsersManager/stylesheets/usersManager.less
@@ -97,4 +97,11 @@
margin-right: 1em;
margin-top: 1em;
}
+}
+
+.uiTest pre.generatedTokenAuth {
+ visibility: hidden;
+}
+.uiTest .listAuthTokens .creationDate {
+ visibility: hidden;
} \ No newline at end of file
diff --git a/plugins/UsersManager/templates/addNewToken.twig b/plugins/UsersManager/templates/addNewToken.twig
new file mode 100644
index 0000000000..fa04b2f26b
--- /dev/null
+++ b/plugins/UsersManager/templates/addNewToken.twig
@@ -0,0 +1,37 @@
+{% extends 'admin.twig' %}
+
+{% set title %}{{ 'General_Security'|translate }}{% endset %}
+
+{% block content %}
+
+ <div piwik-content-block content-title="{{ 'UsersManager_AuthTokens'|translate|e('html_attr') }}">
+ <p>
+ {{ 'UsersManager_TokenAuthIntro'|translate }}
+ </p>
+
+ {% if noDescription %}
+ <br>
+ <div class="alert alert-danger">
+ {{ 'General_Description'|translate }}: {{ 'General_ValidatorErrorEmptyValue'|translate }}
+ </div>
+ {% endif %}
+
+ <form action="{{ linkTo({'module': 'UsersManager', 'action': 'addNewToken'}) }}" method="post" class="addTokenForm">
+ <div piwik-field uicontrol="text" name="description"
+ data-title="{{ 'General_Description'|translate|e('html_attr') }}"
+ maxlength="100" required
+ inline-help="{{ 'UsersManager_AuthTokenPurpose'|translate|e('html_attr') }}">
+ </div>
+
+ <input type="hidden" value="{{ nonce|e('html_attr') }}" name="nonce">
+
+ <input type="submit"
+ value="{{ 'UsersManager_CreateNewToken'|translate|e('html_attr') }}"
+ class="btn"/>
+ {% set backlink = linkTo({'module': 'UsersManager', 'action': 'userSecurity'}) %}
+ {{ 'General_OrCancel'|translate("<a class='entityCancelLink' href='" ~ backlink ~ "'>","</a>")|raw }}
+
+ </form>
+ </div>
+
+{% endblock %}
diff --git a/plugins/UsersManager/templates/addNewTokenSuccess.twig b/plugins/UsersManager/templates/addNewTokenSuccess.twig
new file mode 100644
index 0000000000..ba0ac62b09
--- /dev/null
+++ b/plugins/UsersManager/templates/addNewTokenSuccess.twig
@@ -0,0 +1,17 @@
+{% extends 'admin.twig' %}
+
+{% set title %}{{ 'General_Security'|translate }}{% endset %}
+
+{% block content %}
+
+ <div piwik-content-block content-title="{{ 'UsersManager_TokenSuccessfullyGenerated'|translate|e('html_attr') }}">
+ <p>
+ {{ 'UsersManager_PleaseStoreToken'|translate }}
+ </p>
+ <pre piwik-select-on-focus style="font-size: 40px;" class="generatedTokenAuth"><code>{{ generatedToken }}</code></pre>
+
+ <a href="{{ linkTo({'module': 'UsersManager', 'action': 'userSecurity'}) }}" class="btn"
+ >{{ 'UsersManager_ConfirmTokenCopied'|translate }} {{ 'UsersManager_GoBackSecurityPage'|translate }}</a>
+ </div>
+
+{% endblock %}
diff --git a/plugins/UsersManager/templates/userSecurity.twig b/plugins/UsersManager/templates/userSecurity.twig
new file mode 100644
index 0000000000..58c03b571e
--- /dev/null
+++ b/plugins/UsersManager/templates/userSecurity.twig
@@ -0,0 +1,121 @@
+{% extends 'admin.twig' %}
+
+{% set title %}{{ 'General_Security'|translate }}{% endset %}
+
+{% block content %}
+{% if isUsersAdminEnabled %}
+ <div piwik-content-block content-title="{{ 'General_ChangePassword'|translate|e('html_attr') }}" feature="true">
+ <form id="userSettingsTable" method="post" action="{{ linkTo({'module': 'UsersManager', 'action': 'recordPasswordChange'}) }}">
+
+ <input type="hidden" value="{{ changePasswordNonce|e('html_attr') }}" name="nonce">
+
+ {% if isValidHost is defined and isValidHost %}
+
+ <div piwik-field uicontrol="password" name="password" autocomplete="off"
+ ng-model="personalSettings.password"
+ ng-change="personalSettings.requirePasswordConfirmation()"
+ data-title="{{ 'Login_NewPassword'|translate|e('html_attr') }}"
+ value="" inline-help="{{ 'UsersManager_IfYouWouldLikeToChangeThePasswordTypeANewOne'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="password" name="passwordBis" autocomplete="off"
+ ng-model="personalSettings.passwordBis"
+ ng-change="personalSettings.requirePasswordConfirmation()"
+ data-title="{{ 'Login_NewPasswordRepeat'|translate|e('html_attr') }}"
+ value="" inline-help="{{ 'UsersManager_TypeYourPasswordAgain'|translate|e('html_attr') }}">
+ </div>
+
+ <div piwik-field uicontrol="password" name="passwordConfirmation" autocomplete="off"
+ ng-model="personalSettings.current_password"
+ data-title="{{ 'UsersManager_YourCurrentPassword'|translate|e('html_attr') }}"
+ value="" inline-help="{{ 'UsersManager_TypeYourCurrentPassword'|translate|e('html_attr') }}">
+ </div>
+
+ <input type="submit"
+ value="{{ 'General_Save'|translate|e('html_attr') }}"
+ class="btn"/>
+ {% endif %}
+
+ {% if isValidHost is not defined or not isValidHost %}
+ <div class="alert alert-danger">
+ {{ 'UsersManager_InjectedHostCannotChangePwd'|translate(invalidHost) }}
+ {% if not isSuperUser %}{{ 'UsersManager_EmailYourAdministrator'|translate(invalidHostMailLinkStart,'</a>')|raw }}{% endif %}
+ </div>
+ {% endif %}
+
+ </form>
+ </div>
+
+ {{ postEvent('Template.userSecurity.afterPassword') }}
+{% endif %}
+
+ <a name="authtokens" id="authtokens"></a>
+ <div piwik-content-block content-title="{{ 'UsersManager_AuthTokens'|translate|e('html_attr') }}">
+ <p>
+ {{ 'UsersManager_TokenAuthIntro'|translate }}
+ {% if hasTokensWithExpireDate %}{{ 'UsersManager_ExpiredTokensDeleteAutomatically'|translate }}{% endif %}
+ </p>
+ <table piwik-content-table class="listAuthTokens">
+ <thead>
+ <tr>
+ <th>{{ 'General_CreationDate'|translate }}</th>
+ <th>{{ 'General_Description'|translate }}</th>
+ <th>{{ 'UsersManager_LastUsed'|translate }}</th>
+ {% if hasTokensWithExpireDate %}<th title="{{ 'UsersManager_TokensWithExpireDateCreationBySystem'|translate|e('html_attr') }}">{{ 'UsersManager_ExpireDate'|translate }}</th>{% endif %}
+ <th>{{ 'General_Actions'|translate }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% if tokens is empty %}
+ <tr>
+ <td colspan="{% if hasTokensWithExpireDate %}5{% else %}4{% endif %}">
+ {{ 'UsersManager_NoTokenCreatedYetCreateNow'|translate('<a href="' ~ (linkTo({'module': 'UsersManager', 'action': 'addNewToken'})|e('html_attr'))~ '">', '</a>')|raw }}
+ </td></tr>
+ {% else %}
+ {% for theToken in tokens %}
+ <tr>
+ <td><span class="creationDate">{{ theToken.date_created }}</span></td>
+ <td>{{ theToken.description }}</td>
+ <td>{% if theToken.last_used %}{{ theToken.last_used }}{% else %}{{ 'General_Never'|translate }}{% endif %}</td>
+ {% if hasTokensWithExpireDate %}
+ <td title="{{ 'UsersManager_TokensWithExpireDateCreationBySystem'|translate|e('html_attr') }}">
+ {% if theToken.date_expired %}{{ theToken.date_expired }}{% else %}{{ 'General_Never'|translate }}{% endif %}
+ </td>
+ {% endif %}
+ <td>
+ <form method="post" action="{{ linkTo({'module': 'UsersManager', 'action': 'deleteToken'}) }}" style="display: inline">
+ <input name="nonce" type="hidden" value="{{ deleteTokenNonce|e('html_attr') }}">
+ <input name="idtokenauth" type="hidden" value="{{ theToken.idusertokenauth|e('html_attr') }}">
+ <button type="submit" class="table-action"
+ title="{{ 'General_Delete'|translate|e('html_attr') }}">
+ <span class="icon-delete"></span>
+ </button>
+ </form>
+ </td>
+ </tr>
+ {% endfor %}
+ {% endif %}
+ </tbody>
+ </table>
+
+ <div class="tableActionBar">
+ <a href="{{ linkTo({'module': 'UsersManager', 'action': 'addNewToken'})|e('html_attr') }}" class="addNewToken">
+ <span class="icon-add"></span>
+ {{ 'UsersManager_CreateNewToken'|translate }}
+ </a>
+
+ {% if tokens is not empty %}
+ <form method="post" action="{{ linkTo({'module': 'UsersManager', 'action': 'deleteToken'}) }}" style="display: inline">
+ <input name="nonce" type="hidden" value="{{ deleteTokenNonce|e('html_attr') }}">
+ <input name="idtokenauth" type="hidden" value="all">
+ <button type="submit" class="table-action">
+ <span class="icon-delete"></span> {{ 'UsersManager_DeleteAllTokens'|translate }}
+ </button>
+ </form>
+ {% endif %}
+ </div>
+
+ </div>
+
+
+{% endblock %}
diff --git a/plugins/UsersManager/templates/userSettings.twig b/plugins/UsersManager/templates/userSettings.twig
index 22c9a58ccb..370c710cc8 100644
--- a/plugins/UsersManager/templates/userSettings.twig
+++ b/plugins/UsersManager/templates/userSettings.twig
@@ -4,12 +4,6 @@
{% block content %}
-<div class="ui-confirm" id="confirmTokenRegenerate">
- <h2>{{ 'UsersManager_TokenRegenerateConfirmSelf'|translate }}</h2>
- <input role="yes" type="button" value="{{ 'General_Yes'|translate }}"/>
- <input role="no" type="button" value="{{ 'General_No'|translate }}"/>
-</div>
-
<div piwik-content-block content-title="{{ title|e('html_attr') }}" feature="true">
<form id="userSettingsTable" piwik-form ng-controller="PersonalSettingsController as personalSettings">
@@ -74,31 +68,6 @@
value="{{ defaultDate }}" options="{{ availableDefaultDates|json_encode }}">
</div>
- {% if isValidHost is defined and isValidHost and isUsersAdminEnabled %}
-
- <div piwik-field uicontrol="password" name="password" autocomplete="off"
- ng-model="personalSettings.password"
- ng-change="personalSettings.requirePasswordConfirmation()"
- introduction="{{ 'General_ChangePassword'|translate|e('html_attr') }}"
- data-title="{{ 'Login_NewPassword'|translate|e('html_attr') }}"
- value="" inline-help="{{ 'UsersManager_IfYouWouldLikeToChangeThePasswordTypeANewOne'|translate|e('html_attr') }}">
- </div>
-
- <div piwik-field uicontrol="password" name="passwordBis" autocomplete="off"
- ng-model="personalSettings.passwordBis"
- ng-change="personalSettings.requirePasswordConfirmation()"
- data-title="{{ 'Login_NewPasswordRepeat'|translate|e('html_attr') }}"
- value="" inline-help="{{ 'UsersManager_TypeYourPasswordAgain'|translate|e('html_attr') }}">
- </div>
- {% endif %}
-
- {% if isValidHost is not defined or not isValidHost %}
- <div class="alert alert-danger">
- {{ 'UsersManager_InjectedHostCannotChangePwd'|translate(invalidHost) }}
- {% if not isSuperUser %}{{ 'UsersManager_EmailYourAdministrator'|translate(invalidHostMailLinkStart,'</a>')|raw }}{% endif %}
- </div>
- {% endif %}
-
<div piwik-save-button onconfirm="personalSettings.save()"
saving="personalSettings.loading"></div>
@@ -122,40 +91,31 @@
</form>
</div>
+{% endblock %}
+
{% if showNewsletterSignup %}
-<div ng-controller="PersonalSettingsController as personalSettings">
- <div piwik-content-block id="newsletterSignup"
- ng-show="personalSettings.showNewsletterSignup"
- content-title="{{ 'UsersManager_NewsletterSignupTitle'|translate|e('html_attr') }}">
-
- <div piwik-field uicontrol="checkbox" name="newsletterSignupCheckbox"
- ng-model="personalSettings.newsletterSignupCheckbox"
- full-width="true"
- data-title="{{ 'UsersManager_NewsletterSignupMessage'|translate('<a href="https://matomo.org/privacy-policy/" target="_blank">', '</a>')|e('html_attr') }}"
- >
- </div>
+ <div ng-controller="PersonalSettingsController as personalSettings">
+ <div piwik-content-block id="newsletterSignup"
+ ng-show="personalSettings.showNewsletterSignup"
+ content-title="{{ 'UsersManager_NewsletterSignupTitle'|translate|e('html_attr') }}">
+
+ <div piwik-field uicontrol="checkbox" name="newsletterSignupCheckbox"
+ ng-model="personalSettings.newsletterSignupCheckbox"
+ full-width="true"
+ data-title="{{ 'UsersManager_NewsletterSignupMessage'|translate('<a href="https://matomo.org/privacy-policy/" target="_blank">', '</a>')|e('html_attr') }}"
+ >
+ </div>
- <div piwik-save-button id="newsletterSignupBtn"
- onconfirm="personalSettings.signupForNewsletter()"
- data-disabled="!personalSettings.newsletterSignupCheckbox"
- value="{{ '{{ personalSettings.newsletterSignupButtonTitle }}'|raw }}"
- saving="personalSettings.isProcessingNewsletterSignup">
+ <div piwik-save-button id="newsletterSignupBtn"
+ onconfirm="personalSettings.signupForNewsletter()"
+ data-disabled="!personalSettings.newsletterSignupCheckbox"
+ value="{{ '{{ personalSettings.newsletterSignupButtonTitle }}'|raw }}"
+ saving="personalSettings.isProcessingNewsletterSignup">
+ </div>
</div>
</div>
-</div>
{% endif %}
-<div piwik-content-block
- content-title="{{ 'UsersManager_TokenAuth'|translate|e('html_attr') }}">
- <pre piwik-select-on-focus id="token_auth_user" piwik-show-sensitive-data="{{ userTokenAuth }}"></pre>
-
- <button class="btn btn-link"
- ng-controller="PersonalSettingsController as personalSettings"
- ng-click="personalSettings.regenerateTokenAuth()">{{ 'UsersManager_TokenRegenerateTitle'|translate }}</button>
-</div>
-
-{{ postEvent('Template.userSettings.afterTokenAuth') }}
-
<div piwik-plugin-settings mode="user"></div>
<div piwik-content-block
@@ -174,4 +134,4 @@
</a></span>
</div>
-{% endblock %}
+{% endblock %} \ No newline at end of file
diff --git a/plugins/UsersManager/tests/Fixtures/ManyUsers.php b/plugins/UsersManager/tests/Fixtures/ManyUsers.php
index 9eba719d2a..4497ed319d 100644
--- a/plugins/UsersManager/tests/Fixtures/ManyUsers.php
+++ b/plugins/UsersManager/tests/Fixtures/ManyUsers.php
@@ -7,6 +7,7 @@
*/
namespace Piwik\Plugins\UsersManager\tests\Fixtures;
+use Piwik\Date;
use Piwik\Plugins\UsersManager\API;
use Piwik\Plugins\UsersManager\Model;
use Piwik\Plugins\UsersManager\UserUpdater;
@@ -27,6 +28,7 @@ class ManyUsers extends Fixture
public $siteCopyCount;
public $userCopyCount;
public $addTextSuffixes;
+ public $users = array();
public $baseUsers = array(
'login1' => array('superuser' => 1),
@@ -129,8 +131,11 @@ class ManyUsers extends Fixture
}
}
+ $tokenAuth = $model->generateRandomTokenAuth();
+ $model->addTokenAuth($login, $tokenAuth, 'many users test', Date::now()->getDatetime());
+
$user = $model->getUser($login);
- $this->users[$login]['token'] = $user['token_auth'];
+ $this->users[$login]['token'] = $tokenAuth;
}
}
}
diff --git a/plugins/UsersManager/tests/Integration/ModelTest.php b/plugins/UsersManager/tests/Integration/ModelTest.php
index be2c79d333..0a99be16de 100644
--- a/plugins/UsersManager/tests/Integration/ModelTest.php
+++ b/plugins/UsersManager/tests/Integration/ModelTest.php
@@ -43,6 +43,7 @@ class ModelTest extends IntegrationTestCase
private $model;
private $login = 'userLogin';
+ private $login2 = 'userLogin2';
public function setUp(): void
{
@@ -58,6 +59,7 @@ class ModelTest extends IntegrationTestCase
Fixture::createWebsite('2014-01-01 00:00:00');
Fixture::createWebsite('2014-01-01 00:00:00');
$this->api->addUser($this->login, 'password', 'userlogin@password.de');
+ $this->api->addUser($this->login2, 'password2', 'userlogin2@password.de');
}
public function test_getSitesAccessFromUser_noAccess()
@@ -109,4 +111,262 @@ class ModelTest extends IntegrationTestCase
), $this->model->getSitesAccessFromUser($this->login));
}
+ public function test_getAllNonSystemTokensForLogin_whenNoTokenConfigured()
+ {
+ $tokens = $this->model->getAllNonSystemTokensForLogin($this->login);
+ $this->assertSame(array(), $tokens);
+ }
+
+ public function test_addTokenAuth_minimal()
+ {
+ $this->model->addTokenAuth($this->login, 'token', 'MyDescription', '2020-01-02 03:04:05');
+ $tokens = $this->model->getAllNonSystemTokensForLogin($this->login);
+ $this->assertEquals(array(array(
+ 'idusertokenauth' => '1',
+ 'login' => 'userLogin',
+ 'description' => 'MyDescription',
+ 'password' => '2265daba0872fc3aef169d079365e590f0cbc8ed46c2a7984c8a642803cfd96cb47804a63cf22a79f6ca469268c29ee9e72a5059b62d0a598fe42dfc8dcc51bc',
+ 'hash_algo' => 'sha512',
+ 'system_token' => '0',
+ 'last_used' => null,
+ 'date_created' => '2020-01-02 03:04:05',
+ 'date_expired' => null
+ )), $tokens);
+ }
+
+ public function test_addTokenAuth_expire()
+ {
+ $id = $this->model->addTokenAuth($this->login, 'token', 'MyDescription', '2020-01-02 03:04:05', '2030-01-05 03:04:05');
+ $this->assertEquals(1, $id);
+ $tokens = $this->model->getAllNonSystemTokensForLogin($this->login);
+ $this->assertEquals(array(array(
+ 'idusertokenauth' => '1',
+ 'login' => 'userLogin',
+ 'description' => 'MyDescription',
+ 'password' => '2265daba0872fc3aef169d079365e590f0cbc8ed46c2a7984c8a642803cfd96cb47804a63cf22a79f6ca469268c29ee9e72a5059b62d0a598fe42dfc8dcc51bc',
+ 'hash_algo' => 'sha512',
+ 'system_token' => '0',
+ 'last_used' => null,
+ 'date_created' => '2020-01-02 03:04:05',
+ 'date_expired' => '2030-01-05 03:04:05'
+ )), $tokens);
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectedExceptionMessage does not exist
+ */
+ public function test_addTokenAuth_throwsException_ifUserNotExists()
+ {
+ $this->model->addTokenAuth('foobar', 'token', 'MyDescription', '2020-01-02 03:04:05', '2030-01-05 03:04:05');
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectedExceptionMessage Duplicate entry
+ */
+ public function test_addTokenAuth_throwsException_FailsAddingSameTwice()
+ {
+ $this->model->addTokenAuth($this->login, 'token', 'My description', '2020-01-02 03:04:05');
+ $this->model->addTokenAuth($this->login, 'token', 'My duplicate', '2020-01-03 03:04:05');
+ }
+
+ public function test_addTokenAuth_returnsId()
+ {
+ $id = $this->model->addTokenAuth($this->login, 'token', 'MyDescription', '2020-01-02 03:04:05');
+ $this->assertEquals(1, $id);
+ $id = $this->model->addTokenAuth($this->login, 'token2', 'MyDescription', '2020-01-02 03:04:05');
+ $this->assertEquals(2, $id);
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectedExceptionMessage General_ValidatorErrorEmptyValue
+ */
+ public function test_addTokenAuth_throwsException_NoDescription()
+ {
+ $this->model->addTokenAuth($this->login, 'token', '', '2020-01-02 03:04:05');
+ }
+
+ public function test_getAllNonSystemTokensForLogin_doesNotReturnSystemTokens()
+ {
+ $this->model->addTokenAuth($this->login, 'token2', 'api usage token', '2020-01-02 03:04:05', null, true);
+ $tokens = $this->model->getAllNonSystemTokensForLogin($this->login);
+ $this->assertEquals(array(), $tokens);
+ }
+
+ public function test_getAllNonSystemTokensForLogin_doesNotReturnExpiredTokens()
+ {
+ $this->model->addTokenAuth($this->login, 'token2', 'api usage token', '2020-01-02 03:04:05', '2019-01-05 03:04:05');
+ $tokens = $this->model->getAllNonSystemTokensForLogin($this->login);
+ $this->assertEquals(array(), $tokens);
+ }
+
+ public function test_getAllNonSystemTokensForLogin_returnsNotExpiredToken()
+ {
+ $this->model->addTokenAuth($this->login, 'token', 'MyDescription', '2020-01-02 03:04:05', '2030-01-05 03:04:05');
+ $tokens = $this->model->getAllNonSystemTokensForLogin($this->login);
+ $this->assertEquals(array(array(
+ 'idusertokenauth' => '1',
+ 'login' => 'userLogin',
+ 'description' => 'MyDescription',
+ 'password' => '2265daba0872fc3aef169d079365e590f0cbc8ed46c2a7984c8a642803cfd96cb47804a63cf22a79f6ca469268c29ee9e72a5059b62d0a598fe42dfc8dcc51bc',
+ 'hash_algo' => 'sha512',
+ 'system_token' => '0',
+ 'last_used' => null,
+ 'date_created' => '2020-01-02 03:04:05',
+ 'date_expired' => '2030-01-05 03:04:05'
+ )), $tokens);
+ }
+
+ public function test_getUserByTokenAuth_findsUserWhenTokenNotYetExpired()
+ {
+ $this->model->addTokenAuth($this->login, 'token', 'MyDescription', '2020-01-02 03:04:05', '2030-01-05 03:04:05');
+ $user = $this->model->getUserByTokenAuth('token');
+ $this->assertSame($this->login, $user['login']);
+ }
+
+ public function test_getUserByTokenAuth_findsUserWhenNoExpireDateSet()
+ {
+ $this->model->addTokenAuth($this->login, 'token', 'MyDescription', '2020-01-02 03:04:05');
+ $user = $this->model->getUserByTokenAuth('token');
+ $this->assertSame($this->login, $user['login']);
+ }
+
+ public function test_getUserByTokenAuth_notFindsUserWhenTokenIsExpired()
+ {
+ $this->model->addTokenAuth($this->login, 'token', 'MyDescription', '2020-01-02 03:04:05', '2019-03-04 00:05:06');
+ $user = $this->model->getUserByTokenAuth('token');
+ $this->assertEmpty($user);
+ }
+
+ public function test_getUserByTokenAuth_findsUserWhenTokenIsSystemToken()
+ {
+ $this->model->addTokenAuth($this->login, 'token', 'MyDescription', '2020-01-02 03:04:05', null, true);
+ $user = $this->model->getUserByTokenAuth('token');
+ $this->assertSame($this->login, $user['login']);
+ }
+
+ public function test_generateRandomTokenAuth_correctFormat()
+ {
+ $token = $this->model->generateRandomTokenAuth();
+ $this->assertSame(32, strlen($token));
+ $this->assertTrue(ctype_xdigit($token));
+ }
+
+ public function test_generateRandomTokenAuth_isAlwaysDifferent()
+ {
+ $this->assertNotEquals($this->model->generateRandomTokenAuth(), $this->model->generateRandomTokenAuth());
+ }
+
+ public function test_hashTokenAuth()
+ {
+ $this->assertSame('2265daba0872fc3aef169d079365e590f0cbc8ed46c2a7984c8a642803cfd96cb47804a63cf22a79f6ca469268c29ee9e72a5059b62d0a598fe42dfc8dcc51bc', $this->model->hashTokenAuth('token'));
+ $this->assertSame('02c2e43dcb393097a1221465812a4e9b1e1e80f16e92b313fd4ce8c5ee5b8272a17cd8cdc1ce63578494eaba739c6f7abba7890506ef6bf8d607538778f2a849', $this->model->hashTokenAuth('token2'));
+ }
+
+ public function test_getAllHashedTokensForLogins_noLoginsSet()
+ {
+ $this->assertSame(array(), $this->model->getAllHashedTokensForLogins(array()));
+ }
+
+ public function test_getAllHashedTokensForLogins_noTokensExist()
+ {
+ $this->assertSame(array(), $this->model->getAllHashedTokensForLogins(array('foo', 'bar')));
+ }
+
+ public function test_getAllHashedTokensForLogins()
+ {
+ $this->model->addTokenAuth($this->login, 'token', 'MyDescription', '2020-01-02 03:04:05', null, true);
+ $this->model->addTokenAuth($this->login, 'token2', 'MyDescription', '2020-01-02 03:04:05', null, false);
+ // does not return expired tokens
+ $this->model->addTokenAuth($this->login, 'token3', 'MyDescription', '2020-01-02 03:04:05', '2019-02-03 00:01:02', true);
+
+ $this->assertSame(array(), $this->model->getAllHashedTokensForLogins(array('foo', 'bar')));
+
+ $this->assertSame(array(
+ '2265daba0872fc3aef169d079365e590f0cbc8ed46c2a7984c8a642803cfd96cb47804a63cf22a79f6ca469268c29ee9e72a5059b62d0a598fe42dfc8dcc51bc',
+ '02c2e43dcb393097a1221465812a4e9b1e1e80f16e92b313fd4ce8c5ee5b8272a17cd8cdc1ce63578494eaba739c6f7abba7890506ef6bf8d607538778f2a849'
+ ), $this->model->getAllHashedTokensForLogins(array('foo', $this->login, 'bar')));
+ }
+
+ public function test_deleteToken()
+ {
+ $id1 = $this->model->addTokenAuth($this->login, 'token', 'MyDescription1', '2020-01-02 03:04:05');
+ $id2 = $this->model->addTokenAuth($this->login, 'token2', 'MyDescription2', '2020-01-03 03:04:05');
+
+ // should not have deleted anything as it doesn't match
+ $this->model->deleteToken(999, $this->login);
+ $this->model->deleteToken($id1, 'foobar');
+
+ $tokens = $this->model->getAllNonSystemTokensForLogin($this->login);
+ $this->assertCount(2, $tokens);
+ $this->assertEquals($id1, $tokens[0]['idusertokenauth']);
+ $this->assertEquals($id2, $tokens[1]['idusertokenauth']);
+
+ // should only delete that id
+ $this->model->deleteToken($id1, $this->login);
+
+ $tokens = $this->model->getAllNonSystemTokensForLogin($this->login);
+ $this->assertCount(1, $tokens);
+ $this->assertEquals($id2, $tokens[0]['idusertokenauth']);
+ }
+
+ public function test_deleteAllTokensForUser()
+ {
+ $this->model->addTokenAuth($this->login, 'token', 'MyDescription1', '2020-01-02 03:04:05');
+ $this->model->addTokenAuth($this->login, 'token2', 'MyDescription2', '2020-01-03 03:04:05');
+ $this->model->addTokenAuth($this->login2, 'token3', 'MyDescription2', '2020-01-03 03:04:05');
+
+ // should not have deleted anything as it doesn't match
+ $this->model->deleteAllTokensForUser('foobar');
+
+ $this->assertCount(2, $this->model->getAllNonSystemTokensForLogin($this->login));
+ $this->assertCount(1, $this->model->getAllNonSystemTokensForLogin($this->login2));
+
+ // should only delete tokens for that login
+ $this->model->deleteAllTokensForUser($this->login);
+
+ $tokens = $this->model->getAllNonSystemTokensForLogin($this->login);
+ $this->assertCount(0, $this->model->getAllNonSystemTokensForLogin($this->login));
+ $this->assertCount(1, $this->model->getAllNonSystemTokensForLogin($this->login2));
+ }
+
+ public function test_setTokenAuthWasUsed()
+ {
+ $this->model->addTokenAuth($this->login, 'token2', 'MyDescription', '2020-01-02 03:04:05');
+ $this->model->setTokenAuthWasUsed('token2', '2025-01-02 03:04:05');
+
+ $tokens = $this->model->getAllNonSystemTokensForLogin($this->login);
+ $this->assertSame('2025-01-02 03:04:05', $tokens[0]['last_used']);
+ }
+
+ public function test_setTokenAuthWasUsed_doesNotFailWhenTokenNotExists()
+ {
+ $this->model->setTokenAuthWasUsed('tokenFooBar', '2025-01-02 03:04:05');
+ }
+
+ public function test_deleteExpiredTokens()
+ {
+ $id1 = $this->model->addTokenAuth($this->login, 'token', 'MyDescription1', '2020-01-01 03:04:05', '2020-01-02 03:04:05');
+ $id2 = $this->model->addTokenAuth($this->login, 'token2', 'MyDescription2', '2020-01-02 03:04:05');
+ $id3 = $this->model->addTokenAuth($this->login, 'token3', 'MyDescription3', '2020-01-03 03:04:05', '2022-01-02 03:04:05');
+ $id4 = $this->model->addTokenAuth($this->login2, 'token4', 'MyDescription4', '2020-01-04 03:04:05', '2024-01-02 03:04:05');
+ $id5 = $this->model->addTokenAuth($this->login2, 'token5', 'MyDescription5', '2020-01-05 03:04:05');
+ $id6 = $this->model->addTokenAuth($this->login2, 'token6', 'MyDescription6', '2020-01-06 03:04:05', '2018-01-02 03:04:05');
+
+ // id1 and id6 are expired and should have been deleted
+ $this->model->deleteExpiredTokens('2021-01-02 03:04:05');
+
+ $tokens = $this->model->getAllNonSystemTokensForLogin($this->login);
+ $this->assertSame($id2, $tokens[0]['idusertokenauth']);
+ $this->assertSame($id3, $tokens[1]['idusertokenauth']);
+ $this->assertCount(2, $tokens);
+
+ $tokens = $this->model->getAllNonSystemTokensForLogin($this->login2);
+ $this->assertSame($id4, $tokens[0]['idusertokenauth']);
+ $this->assertSame($id5, $tokens[1]['idusertokenauth']);
+ $this->assertCount(2, $tokens);
+ }
+
}
diff --git a/plugins/UsersManager/tests/Integration/UserAccessFilterTest.php b/plugins/UsersManager/tests/Integration/UserAccessFilterTest.php
index 86c150e2ed..5dacff9e16 100644
--- a/plugins/UsersManager/tests/Integration/UserAccessFilterTest.php
+++ b/plugins/UsersManager/tests/Integration/UserAccessFilterTest.php
@@ -283,16 +283,16 @@ class UserAccessFilterTest extends IntegrationTestCase
private function createManyUsers()
{
- $this->model->addUser('login1', md5('pass'), 'email1@example.com', 'alias1', md5('token1'), '2008-01-01 00:00:00');
- $this->model->addUser('login2', md5('pass'), 'email2@example.com', 'alias2', md5('token2'), '2008-01-01 00:00:00');
+ $this->model->addUser('login1', md5('pass'), 'email1@example.com', 'alias1', '2008-01-01 00:00:00');
+ $this->model->addUser('login2', md5('pass'), 'email2@example.com', 'alias2', '2008-01-01 00:00:00');
// login3 won't have access to any site
- $this->model->addUser('login3', md5('pass'), 'email3@example.com', 'alias3', md5('token3'), '2008-01-01 00:00:00');
- $this->model->addUser('login4', md5('pass'), 'email4@example.com', 'alias4', md5('token4'), '2008-01-01 00:00:00');
- $this->model->addUser('login5', md5('pass'), 'email5@example.com', 'alias5', md5('token5'), '2008-01-01 00:00:00');
- $this->model->addUser('login6', md5('pass'), 'email6@example.com', 'alias6', md5('token6'), '2008-01-01 00:00:00');
- $this->model->addUser('login7', md5('pass'), 'email7@example.com', 'alias7', md5('token7'), '2008-01-01 00:00:00');
- $this->model->addUser('login8', md5('pass'), 'email8@example.com', 'alias8', md5('token8'), '2008-01-01 00:00:00');
- $this->model->addUser('anonymous', '', 'ano@example.com', 'anonymous', 'anonymous', '2008-01-01 00:00:00');
+ $this->model->addUser('login3', md5('pass'), 'email3@example.com', 'alias3', '2008-01-01 00:00:00');
+ $this->model->addUser('login4', md5('pass'), 'email4@example.com', 'alias4', '2008-01-01 00:00:00');
+ $this->model->addUser('login5', md5('pass'), 'email5@example.com', 'alias5', '2008-01-01 00:00:00');
+ $this->model->addUser('login6', md5('pass'), 'email6@example.com', 'alias6', '2008-01-01 00:00:00');
+ $this->model->addUser('login7', md5('pass'), 'email7@example.com', 'alias7', '2008-01-01 00:00:00');
+ $this->model->addUser('login8', md5('pass'), 'email8@example.com', 'alias8', '2008-01-01 00:00:00');
+ $this->model->addUser('anonymous', '', 'ano@example.com', 'anonymous', '2008-01-01 00:00:00');
$this->model->setSuperUserAccess('login1', true); // we treat this one as our superuser
diff --git a/plugins/UsersManager/tests/Integration/UsersManagerTest.php b/plugins/UsersManager/tests/Integration/UsersManagerTest.php
index d7ae784bf1..2a66f9dad2 100644
--- a/plugins/UsersManager/tests/Integration/UsersManagerTest.php
+++ b/plugins/UsersManager/tests/Integration/UsersManagerTest.php
@@ -109,11 +109,6 @@ class UsersManagerTest extends IntegrationTestCase
unset($userAfter['password']);
// implicitly checks password!
- $userModel = $this->model->getUser($user['login']);
- $userAfter['token_auth'] = $userModel['token_auth'];
-
- $user['token_auth'] = $this->api->getTokenAuth($user["login"], md5($newPassword));
-
$user['email'] = $newEmail;
$user['alias'] = $newAlias;
$user['superuser_access'] = 0;
@@ -282,12 +277,6 @@ class UsersManagerTest extends IntegrationTestCase
// check that password and token are properly set
$this->assertEquals(60, strlen($user['password']));
- $userModel = $this->model->getUser($login);
- $this->assertEquals(32, strlen($userModel['token_auth']));
-
- $userModel = $this->model->getUser($login);
- $this->assertEquals($userModel['token_auth'], $this->api->getTokenAuth($login, UsersManager::getPasswordHash($password)));
-
// check that all fields are the same
$this->assertEquals($login, $user['login']);
$this->assertEquals($email, $user['email']);
diff --git a/plugins/UsersManager/tests/System/ApiTest.php b/plugins/UsersManager/tests/System/ApiTest.php
index 0a7d808ffb..5908f9245c 100644
--- a/plugins/UsersManager/tests/System/ApiTest.php
+++ b/plugins/UsersManager/tests/System/ApiTest.php
@@ -8,6 +8,9 @@
namespace Piwik\Plugins\UsersManager\tests\System;
+use Piwik\Date;
+use Piwik\Plugins\UsersManager\API;
+use Piwik\Plugins\UsersManager\Model;
use Piwik\Plugins\UsersManager\tests\Fixtures\ManyUsers;
use Piwik\Tests\Framework\TestCase\SystemTestCase;
@@ -24,6 +27,24 @@ class ApiTest extends SystemTestCase
public static $fixture = null; // initialized below class definition
/**
+ * @var API
+ */
+ private $api;
+
+ /**
+ * @var Model
+ */
+ private $model;
+
+ public function setUp() : void
+ {
+ parent::setUp(); // TODO: Change the autogenerated stub
+
+ $this->api = API::getInstance();
+ $this->model = new Model();
+ }
+
+ /**
* @dataProvider getApiForTesting
*/
public function testApi($api, $params = array())
@@ -62,6 +83,81 @@ class ApiTest extends SystemTestCase
return $apiToTest;
}
+ public function test_createAppSpecificTokenAuth()
+ {
+ $this->model->deleteAllTokensForUser('login1');
+ $token = $this->api->createAppSpecificTokenAuth('login1', md5('password'), 'test');
+ $this->assertMd5($token);
+
+ $user = $this->model->getUserByTokenAuth($token);
+ $this->assertSame('login1', $user['login']);
+ }
+
+ public function test_createAppSpecificTokenAuth_canLoginByEmail()
+ {
+ $this->model->deleteAllTokensForUser('login1');
+ $token = $this->api->createAppSpecificTokenAuth('login1@example.com', md5('password'), 'test');
+ $this->assertMd5($token);
+
+ $user = $this->model->getUserByTokenAuth($token);
+ $this->assertSame('login1', $user['login']);
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectedExceptionMessage is expecting a MD5-hashed password
+ */
+ public function test_createAppSpecificTokenAuth_notValidPasswordFormat()
+ {
+ $this->api->createAppSpecificTokenAuth('login1', 'foobar', 'test');
+ }
+
+ public function test_createAppSpecificTokenAuth_generatesRandomTokenWhenPasswordNotValid()
+ {
+ $this->model->deleteAllTokensForUser('login1');
+ $token = $this->api->createAppSpecificTokenAuth('login1', md5('foooooo'), 'test');
+ $this->assertMd5($token);
+
+ $user = $this->model->getUserByTokenAuth($token);
+ $this->assertEmpty($user);
+ }
+
+ public function test_createAppSpecificTokenAuth_withExpireDate()
+ {
+ $this->model->deleteAllTokensForUser('login1');
+ $token = $this->api->createAppSpecificTokenAuth('login1', md5('password'), 'test', '2026-01-02 03:04:05');
+ $this->assertMd5($token);
+
+ $tokens = $this->model->getAllNonSystemTokensForLogin('login1');
+ $this->assertEquals($this->model->hashTokenAuth($token), $tokens[0]['password']);
+ $this->assertEquals('login1', $tokens[0]['login']);
+ $this->assertEquals('test', $tokens[0]['description']);
+ $this->assertEquals('2026-01-02 03:04:05', $tokens[0]['date_expired']);
+ }
+
+ public function test_createAppSpecificTokenAuth_withExpireHours()
+ {
+ $expireInHours = 48;
+ $this->model->deleteAllTokensForUser('login1');
+ $token = $this->api->createAppSpecificTokenAuth('login1', md5('password'), 'test', null, $expireInHours);
+ $this->assertMd5($token);
+
+ $tokens = $this->model->getAllNonSystemTokensForLogin('login1');
+ $this->assertEquals($this->model->hashTokenAuth($token), $tokens[0]['password']);
+ $this->assertEquals('login1', $tokens[0]['login']);
+ $this->assertNotEmpty($tokens[0]['date_expired']);
+
+ $dateExpired = Date::factory($tokens[0]['date_expired']);
+ $dateExpired->isLater(Date::now()->addHour($expireInHours - 1 ));
+ $dateExpired->isEarlier(Date::now()->addHour($expireInHours + 1));
+ }
+
+ private function assertMd5($string)
+ {
+ $this->assertSame(32, strlen($string));
+ $this->assertTrue(ctype_xdigit($string));
+ }
+
public static function getOutputPrefix()
{
return '';
diff --git a/plugins/UsersManager/tests/UI/UserSettings_spec.js b/plugins/UsersManager/tests/UI/UserSettings_spec.js
index 190673c6ca..1f4dc719b0 100644
--- a/plugins/UsersManager/tests/UI/UserSettings_spec.js
+++ b/plugins/UsersManager/tests/UI/UserSettings_spec.js
@@ -11,7 +11,8 @@ describe("UserSettings", function () {
this.timeout(0);
this.fixture = "Piwik\\Plugins\\UsersManager\\tests\\Fixtures\\ManyUsers";
- var url = "?module=UsersManager&action=userSettings";
+ var userSettingsUrl = "?module=UsersManager&action=userSettings";
+ var userSecurityUrl = "?module=UsersManager&action=userSecurity";
before(async function() {
await page.webpage.setViewport({
@@ -20,8 +21,35 @@ describe("UserSettings", function () {
});
});
+ it('should show user security page', async function () {
+ await page.goto(userSecurityUrl);
+ expect(await page.screenshotSelector('.admin')).to.matchImage('load_security');
+ });
+
+ it('should ask for password when trying to add token', async function () {
+ await page.click('.addNewToken');
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('.loginSection');
+ expect(await page.screenshotSelector('.loginSection')).to.matchImage('add_token_check_password');
+ });
+
+ it('should accept correct password', async function () {
+ await page.type('#login_form_password', 'superUserPass');
+ await page.click('#login_form_submit');
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('.addTokenForm');
+ expect(await page.screenshotSelector('.admin')).to.matchImage('add_token');
+ });
+
+ it('should create new token', async function () {
+ await page.type('.addTokenForm input[id=description]', 'test description');
+ await page.click('.addTokenForm .btn');
+ await page.waitForNetworkIdle();
+ expect(await page.screenshotSelector('.admin')).to.matchImage('add_token_success');
+ });
+
it('should show user settings page', async function () {
- await page.goto(url);
+ await page.goto(userSettingsUrl);
expect(await page.screenshotSelector('.admin')).to.matchImage('load');
});
@@ -34,7 +62,7 @@ describe("UserSettings", function () {
it('should not prompt user to subscribe to newsletter again', async function () {
// Assumes previous test has clicked on the signup button - so we shouldn't see it this time
- await page.goto(url);
+ await page.goto(userSettingsUrl);
expect(await page.screenshotSelector('.admin')).to.matchImage('already_signed_up');
});
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_add_token.png b/plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_add_token.png
new file mode 100644
index 0000000000..8f8b683dac
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_add_token.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e0a217eaf00dd0e60598067698cdcff92c794433438122b07284c89093d84ae2
+size 30211
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_add_token_check_password.png b/plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_add_token_check_password.png
new file mode 100644
index 0000000000..d511b3665a
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_add_token_check_password.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bf4e7b2dd1d68df9db0826eb9051f796bf3c25426659a9e84792ae4879835f17
+size 13422
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_add_token_success.png b/plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_add_token_success.png
new file mode 100644
index 0000000000..45a8da92a3
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_add_token_success.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c2117369cad613d3fccc5848a29ec09212a49a355266db0f57058cd7af639544
+size 19921
diff --git a/plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_load_security.png b/plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_load_security.png
new file mode 100644
index 0000000000..f0312d5958
--- /dev/null
+++ b/plugins/UsersManager/tests/UI/expected-screenshots/UserSettings_load_security.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:13053d0f3133b73a4f4c9cff719d1c6cd735dfffda162e7e775acb1db6b81162
+size 115909
diff --git a/plugins/Widgetize/templates/index.twig b/plugins/Widgetize/templates/index.twig
index 3c4d1529e0..2ab204877c 100644
--- a/plugins/Widgetize/templates/index.twig
+++ b/plugins/Widgetize/templates/index.twig
@@ -21,8 +21,8 @@
If you want your widgets to be viewable by everybody, you first have to set the 'view' permissions
to the anonymous user in the <a href='index.php?module=UsersManager' rel='noreferrer noopener' target='_blank'>Users Management section</a>.
<br/>Alternatively, if you are publishing widgets on a password protected or private page,
- you don't necessarily have to allow 'anonymous' to view your reports. In this case, you can add the secret token_auth parameter (found in the
- <a href='{{ linkTo({'module':'API','action':'listAllAPI'}) }}' rel='noreferrer noopener' target='_blank'>API page</a>) in the widget URL.
+ you don't necessarily have to allow 'anonymous' to view your reports. In this case, you can add the secret <code>token_auth</code> parameter in the widget URL.
+ You can manage your auth tokens on your <a href='{{ linkTo({'module':'UsersManager','action':'userSecurity'}) }}' rel='noreferrer noopener' target='_blank'>Security page</a>.
</p>
</div>
<div piwik-content-block content-title="Widgetize dashboards">