Skip to content

Commit

Permalink
App specific token_auths (matomo-org#15410)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* 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 <[email protected]>
Co-authored-by: diosmosis <[email protected]>
  • Loading branch information
3 people authored Mar 18, 2020
1 parent e493fee commit f0c246c
Show file tree
Hide file tree
Showing 91 changed files with 1,464 additions and 389 deletions.
9 changes: 0 additions & 9 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,6 @@
[submodule "plugins/DeviceDetectorCache"]
path = plugins/DeviceDetectorCache
url = https://github.com/matomo-org/plugin-DeviceDetectorCache.git
[submodule "plugins/Provider"]
path = plugins/Provider
url = https://github.com/matomo-org/plugin-Provider.git

# Add new Plugin submodule above this line ^^
#
# Note: when you add a submodule that SHOULD be left in the packaged release such as the few submodules below,
# then you MUST add these submodules names in the SUBMODULES_PACKAGED_WITH_CORE variable in:
# https://github.com/matomo-org/matomo-package/blob/master/scripts/build-package.sh
[submodule "misc/log-analytics"]
path = misc/log-analytics
url = https://github.com/matomo-org/matomo-log-analytics.git
Expand Down
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ The Product Changelog at **[matomo.org/changelog](https://matomo.org/changelog)*

## Matomo 4.0.0

### Breaking Changes
### New API
* A new API `UsersManager.createAppSpecificTokenAuth` has been added to create an app specific token for a user.

### Breaking changes
* The API `UsersManager.getTokenAuth` has been removed. Instead you need to use `UsersManager.createAppSpecificTokenAuth` and store this token in your application.
* The API `UsersManager.createTokenAuth` has been removed. Instead you need to use `UsersManager.createAppSpecificTokenAuth` and store this token in your application.
* Deprecated `piwik` font was removed. Use `matomo` font instead
* The JavaScript AjaxHelper does not longer support synchronous requests. All requests will be sent async instead.
* The deprecated Platform API method `\Piwik\Plugin::getListHooksRegistered()` has been removed. Use `\Piwik\Plugin::registerEvents()` instead
Expand Down
27 changes: 26 additions & 1 deletion core/Access.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Piwik\Container\StaticContainer;
use Piwik\Exception\InvalidRequestParameterException;
use Piwik\Plugins\SitesManager\API as SitesManagerApi;
use Piwik\Session\SessionAuth;

/**
* Singleton that manages user access to Piwik resources.
Expand Down Expand Up @@ -155,8 +156,32 @@ public function reloadAccess(Auth $auth = null)
return false;
}

$result = null;

$forceApiSession = Common::getRequestVar('force_api_session', 0, 'int', $_POST);
if ($forceApiSession && Piwik::getModule() === 'API' && (Piwik::getAction() === 'index' || !Piwik::getAction())) {
$tokenAuth = Common::getRequestVar('token_auth', '', 'string', $_POST);
if (!empty($tokenAuth)) {
Session::start();
$auth = StaticContainer::get(SessionAuth::class);
$auth->setTokenAuth($tokenAuth);
$result = $auth->authenticate();
if (!$result->wasAuthenticationSuccessful()) {
/**
* Ensures brute force logic to be executed
* @ignore
* @internal
*/
Piwik::postEvent('API.Request.authenticate.failed');
}
// if not successful, we will fallback to regular auth
}
}

// access = array ( idsite => accessIdSite, idsite2 => accessIdSite2)
$result = $this->auth->authenticate();
if (!$result || !$result->wasAuthenticationSuccessful()) {
$result = $this->auth->authenticate();
}

if (!$result->wasAuthenticationSuccessful()) {
return false;
Expand Down
16 changes: 3 additions & 13 deletions core/CliMulti.php
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,7 @@ private function executeNotAsyncHttp($url, Output $output)
}

if ($this->runAsSuperUser) {
$tokenAuths = self::getSuperUserTokenAuths();
$tokenAuth = reset($tokenAuths);
$tokenAuth = self::getSuperUserTokenAuth();

if (strpos($url, '?') === false) {
$url .= '?';
Expand Down Expand Up @@ -433,18 +432,9 @@ private function requestUrls(array $piwikUrls)
return $results;
}

private static function getSuperUserTokenAuths()
private static function getSuperUserTokenAuth()
{
$tokens = array();

/**
* Used to be in CronArchive, moved to CliMulti.
*
* @ignore
*/
Piwik::postEvent('CronArchive.getTokenAuth', array(&$tokens));

return $tokens;
return Piwik::requestTemporarySystemAuthToken('CliMultiNonAsyncArchive', 36);
}

public function setUrlToPiwik($urlToPiwik)
Expand Down
26 changes: 21 additions & 5 deletions core/Db/Schema/Mysql.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Piwik\Db;
use Piwik\DbHelper;
use Piwik\Option;
use Piwik\Plugins\UsersManager\Model;
use Piwik\Version;

/**
Expand Down Expand Up @@ -45,12 +46,24 @@ public function getTablesCreateSql()
alias VARCHAR(45) NOT NULL,
email VARCHAR(100) NOT NULL,
twofactor_secret VARCHAR(40) NOT NULL DEFAULT '',
token_auth CHAR(32) NOT NULL,
superuser_access TINYINT(2) unsigned NOT NULL DEFAULT '0',
date_registered TIMESTAMP NULL,
ts_password_modified TIMESTAMP NULL,
PRIMARY KEY(login),
UNIQUE KEY uniq_keytoken(token_auth)
PRIMARY KEY(login)
) ENGINE=$engine DEFAULT CHARSET=utf8
",
'user_token_auth' => "CREATE TABLE {$prefixTables}user_token_auth (
idusertokenauth BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
login VARCHAR(100) NOT NULL,
description VARCHAR(".Model::MAX_LENGTH_TOKEN_DESCRIPTION.") NOT NULL,
password VARCHAR(255) NOT NULL,
hash_algo VARCHAR(30) NOT NULL,
system_token TINYINT(1) NOT NULL DEFAULT 0,
last_used DATETIME NULL,
date_created DATETIME NOT NULL,
date_expired DATETIME NULL,
PRIMARY KEY(idusertokenauth),
UNIQUE KEY uniq_password(password)
) ENGINE=$engine DEFAULT CHARSET=utf8
",

Expand Down Expand Up @@ -504,12 +517,15 @@ public function createTables()
public function createAnonymousUser()
{
$now = Date::factory('now')->getDatetime();

// The anonymous user is the user that is assigned by default
// note that the token_auth value is anonymous, which is assigned by default as well in the Login plugin
$db = $this->getDb();
$db->query("INSERT IGNORE INTO " . Common::prefixTable("user") . "
VALUES ( 'anonymous', '', 'anonymous', '[email protected]', '', 'anonymous', 0, '$now', '$now' );");
(`login`, `password`, `alias`, `email`, `twofactor_secret`, `superuser_access`, `date_registered`, `ts_password_modified`)
VALUES ( 'anonymous', '', 'anonymous', '[email protected]', '', 0, '$now', '$now' );");

$model = new Model();
$model->addTokenAuth('anonymous', 'anonymous', 'anonymous default token', $now);
}

/**
Expand Down
46 changes: 46 additions & 0 deletions core/Piwik.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Piwik\Period\Week;
use Piwik\Period\Year;
use Piwik\Plugins\UsersManager\API as APIUsersManager;
use Piwik\Plugins\UsersManager\Model;
use Piwik\Translation\Translator;

/**
Expand Down Expand Up @@ -257,6 +258,51 @@ public static function checkUserHasSuperUserAccessOrIsTheUser($theUser)
}
}

/**
* Request a token auth to authenticate in a request.
*
* Note: During one request the token is only being requested once and used throughout the request. So you want to make
* sure the token is valid for enough time for the whole request to finish.
*
* @param string $reason some short string/text explaining the reason for the token generation, eg "CliMultiAsyncHttpArchiving"
* @param int $validForHours For how many hours the token should be valid. Should not be valid for more than 14 days.
* @return mixed
*/
public static function requestTemporarySystemAuthToken($reason, $validForHours)
{
static $token = array();

if (isset($token[$reason])) {
// note: For now we do not increase the expire time when it is already requested
return $token[$reason];
}

$twoWeeksInHours = 14 * 24;
if ($validForHours > $twoWeeksInHours) {
throw new Exception('The token cannot be valid for so many hours: ' . $validForHours);
}

$model = new Model();
$users = $model->getUsersHavingSuperUserAccess();
if (!empty($users)) {
$user = reset($users);
$expireDate = Date::now()->addHour($validForHours)->getDatetime();

$token[$reason] = $model->generateRandomTokenAuth();

$model->addTokenAuth(
$user['login'],
$token[$reason],
'System generated ' . $reason,
Date::now()->getDatetime(),
$expireDate,
true);

return $token[$reason];
}

}

/**
* Check whether the given user has superuser access.
*
Expand Down
25 changes: 20 additions & 5 deletions core/Session/SessionAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@

use Piwik\Auth;
use Piwik\AuthResult;
use Piwik\Common;
use Piwik\Config;
use Piwik\Date;
use Piwik\Plugins\UsersManager\Model;
use Piwik\Plugins\UsersManager\Model as UsersModel;
use Piwik\Session;

Expand Down Expand Up @@ -42,6 +44,8 @@ class SessionAuth implements Auth
*/
private $user;

private $tokenAuth;

public function __construct(UsersModel $userModel = null, $shouldDestroySession = true)
{
$this->userModel = $userModel ?: new UsersModel();
Expand All @@ -55,7 +59,7 @@ public function getName()

public function setTokenAuth($token_auth)
{
// empty
$this->tokenAuth = $token_auth;
}

public function getLogin()
Expand Down Expand Up @@ -113,7 +117,17 @@ public function authenticate()

$this->updateSessionExpireTime($sessionFingerprint);

return $this->makeAuthSuccess($user);
if (!empty($this->tokenAuth) && $this->tokenAuth !== $sessionFingerprint->getSessionTokenAuth()) {
return $this->makeAuthFailure();
}

if ($sessionFingerprint->getSessionTokenAuth()) {
$tokenAuth = $sessionFingerprint->getSessionTokenAuth();
} else {
$tokenAuth = $this->userModel->generateRandomTokenAuth();
}

return $this->makeAuthSuccess($user, $tokenAuth);
}

private function isSessionStartedBeforePasswordChange(SessionFingerprint $sessionFingerprint, $tsPasswordModified)
Expand All @@ -137,14 +151,15 @@ private function makeAuthFailure()
return new AuthResult(AuthResult::FAILURE, null, null);
}

private function makeAuthSuccess($user)
private function makeAuthSuccess($user, $tokenAuth)
{
$this->user = $user;
$this->tokenAuth = $tokenAuth;

$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'], $tokenAuth);
}

protected function initNewBlankSession(SessionFingerprint $sessionFingerprint)
Expand Down Expand Up @@ -178,7 +193,7 @@ protected function destroyCurrentSession(SessionFingerprint $sessionFingerprint)

public function getTokenAuth()
{
return $this->user['token_auth'];
return $this->tokenAuth;
}

private function updateSessionExpireTime(SessionFingerprint $sessionFingerprint)
Expand Down
18 changes: 17 additions & 1 deletion core/Session/SessionFingerprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace Piwik\Session;

use Piwik\Common;
use Piwik\Config;
use Piwik\Date;

Expand Down Expand Up @@ -42,6 +43,7 @@ class SessionFingerprint
const USER_NAME_SESSION_VAR_NAME = 'user.name';
const SESSION_INFO_SESSION_VAR_NAME = 'session.info';
const SESSION_INFO_TWO_FACTOR_AUTH_VERIFIED = 'twofactorauth.verified';
const SESSION_INFO_TEMP_TOKEN_AUTH = 'user.token_auth_temp';

public function getUser()
{
Expand All @@ -61,6 +63,15 @@ public function getUserInfo()
return null;
}

public function getSessionTokenAuth()
{
if (!empty($_SESSION[self::SESSION_INFO_TEMP_TOKEN_AUTH])) {
return $_SESSION[self::SESSION_INFO_TEMP_TOKEN_AUTH];
}

return null;
}

public function hasVerifiedTwoFactor()
{
if (isset($_SESSION[self::SESSION_INFO_TWO_FACTOR_AUTH_VERIFIED])) {
Expand All @@ -75,11 +86,12 @@ public function setTwoFactorAuthenticationVerified()
$_SESSION[self::SESSION_INFO_TWO_FACTOR_AUTH_VERIFIED] = 1;
}

public function initialize($userName, $isRemembered = false, $time = null)
public function initialize($userName, $tokenAuth, $isRemembered = false, $time = null)
{
$time = $time ?: Date::now()->getTimestampUTC();
$_SESSION[self::USER_NAME_SESSION_VAR_NAME] = $userName;
$_SESSION[self::SESSION_INFO_TWO_FACTOR_AUTH_VERIFIED] = 0;
$_SESSION[self::SESSION_INFO_TEMP_TOKEN_AUTH] = $tokenAuth;
$_SESSION[self::SESSION_INFO_SESSION_VAR_NAME] = [
'ts' => $time,
'remembered' => $isRemembered,
Expand All @@ -100,6 +112,10 @@ public function clear()
if (isset($_SESSION[self::SESSION_INFO_TWO_FACTOR_AUTH_VERIFIED])) { // may not be available during tests
unset($_SESSION[self::SESSION_INFO_TWO_FACTOR_AUTH_VERIFIED]);
}

if (isset($_SESSION[self::SESSION_INFO_TEMP_TOKEN_AUTH])) { // may not be available during tests
unset($_SESSION[self::SESSION_INFO_TEMP_TOKEN_AUTH]);
}
}

public function getSessionStartTime()
Expand Down
2 changes: 1 addition & 1 deletion core/Session/SessionInitializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ protected function processFailedSession()
protected function processSuccessfulSession(AuthResult $authResult)
{
$sessionIdentifier = new SessionFingerprint();
$sessionIdentifier->initialize($authResult->getIdentity(), $this->isRemembered());
$sessionIdentifier->initialize($authResult->getIdentity(), $authResult->getTokenAuth(), $this->isRemembered());

/**
* @ignore
Expand Down
2 changes: 2 additions & 0 deletions core/Tracker/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ public static function authenticateSuperUserOrAdminOrWrite($tokenAuth, $idSite)
// Now checking the list of admin token_auth cached in the Tracker config file
if (!empty($idSite) && $idSite > 0) {
$website = Cache::getCacheWebsiteAttributes($idSite);
$userModel = new \Piwik\Plugins\UsersManager\Model();
$tokenAuth = $userModel->hashTokenAuth($tokenAuth);
$hashedToken = UsersManager::hashTrackingToken((string) $tokenAuth, $idSite);

if (array_key_exists('tracking_token_auth', $website)
Expand Down
4 changes: 4 additions & 0 deletions core/Updater/Migration/Plugin/Uninstall.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ public function shouldIgnoreError($exception)
public function exec()
{
$this->pluginManager->uninstallPlugin($this->pluginName);

// uninstallPlugin() loads all plugins in the filesystem, which we don't want for the rest of the updates
$this->pluginManager->unloadPlugins();
$this->pluginManager->loadActivatedPlugins();
}

}
4 changes: 2 additions & 2 deletions core/Updates/2.0.4-b5.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function __construct(MigrationFactory $factory)
public function getMigrations(Updater $updater)
{
return array(
$this->migration->db->addColumn('user', 'superuser_access', "TINYINT(2) UNSIGNED NOT NULL DEFAULT '0'", 'token_auth')
$this->migration->db->addColumn('user', 'superuser_access', "TINYINT(2) UNSIGNED NOT NULL DEFAULT '0'")
);
}

Expand Down Expand Up @@ -82,7 +82,7 @@ private static function migrateConfigSuperUserToDb()
'password' => $superUser['password'],
'alias' => $superUser['login'],
'email' => $superUser['email'],
'token_auth' => $userApi->getTokenAuth($superUser['login'], $superUser['password']),
'token_auth' => md5(Common::getRandomString(32)),
'date_registered' => Date::now()->getDatetime(),
'superuser_access' => 1
)
Expand Down
Loading

0 comments on commit f0c246c

Please sign in to comment.