site-accueil-insa/matomo/plugins/TwoFactorAuth/TwoFactorAuth.php

268 lines
10 KiB
PHP

<?php
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\TwoFactorAuth;
use Piwik\API\Request;
use Piwik\Common;
use Piwik\Container\StaticContainer;
use Piwik\FrontController;
use Piwik\Piwik;
use Piwik\Plugins\TwoFactorAuth\Dao\RecoveryCodeDao;
use Piwik\Plugins\UsersManager\Model;
use Piwik\Session;
use Piwik\Session\SessionFingerprint;
use Exception;
use Piwik\SettingsPiwik;
class TwoFactorAuth extends \Piwik\Plugin
{
/**
* @see \Piwik\Plugin::registerEvents
*/
public function registerEvents()
{
return array(
'Request.dispatch' => array('function' => 'onRequestDispatch', 'after' => true),
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
'API.UsersManager.deleteUser.end' => 'deleteRecoveryCodes',
'API.UsersManager.createAppSpecificTokenAuth.end' => 'onCreateAppSpecificTokenAuth',
'Request.dispatch.end' => array('function' => 'onRequestDispatchEnd', 'after' => true),
'Template.userSecurity.afterPassword' => 'render2FaUserSettings',
'Login.authenticate.processSuccessfulSession.end' => 'onSuccessfulSession',
'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
);
}
public function getClientSideTranslationKeys(&$translations)
{
$translations[] = 'TwoFactorAuth_WarningChangingConfiguredDevice';
$translations[] = 'TwoFactorAuth_SetupIntroFollowSteps';
$translations[] = 'TwoFactorAuth_StepX';
$translations[] = 'TwoFactorAuth_RecoveryCodes';
$translations[] = 'TwoFactorAuth_RecoveryCodesExplanation';
$translations[] = 'TwoFactorAuth_RecoveryCodesSecurity';
$translations[] = 'TwoFactorAuth_RecoveryCodesAllUsed';
$translations[] = 'General_Download';
$translations[] = 'General_Print';
$translations[] = 'General_Copy';
$translations[] = 'TwoFactorAuth_SetupBackupRecoveryCodes';
$translations[] = 'General_Next';
$translations[] = 'TwoFactorAuth_SetupAuthenticatorOnDeviceStep1';
$translations[] = 'General_Or';
$translations[] = 'TwoFactorAuth_ConfirmSetup';
$translations[] = 'TwoFactorAuth_VerifyAuthCodeIntro';
$translations[] = 'TwoFactorAuth_AuthenticationCode';
$translations[] = 'TwoFactorAuth_VerifyAuthCodeHelp';
$translations[] = 'General_Confirm';
$translations[] = 'TwoFactorAuth_SetupAuthenticatorOnDeviceStep2';
$translations[] = 'TwoFactorAuth_SetupAuthenticatorOnDevice';
}
public function getStylesheetFiles(&$stylesheets)
{
$stylesheets[] = "plugins/TwoFactorAuth/stylesheets/twofactorauth.less";
}
public function getJsFiles(&$jsFiles)
{
$jsFiles[] = "plugins/TwoFactorAuth/javascripts/twofactorauth.js";
$jsFiles[] = "node_modules/qrcodejs2/qrcode.min.js";
}
public function deleteRecoveryCodes($returnedValue, $params)
{
$model = new Model();
if (!empty($params['parameters']['userLogin'])
&& !$model->userExists($params['parameters']['userLogin'])) {
// we delete only if the deletion was really successful
$dao = StaticContainer::get(RecoveryCodeDao::class);
$dao->deleteAllRecoveryCodesForLogin($params['parameters']['userLogin']);
}
}
public function render2FaUserSettings(&$out)
{
$validator = $this->getValidator();
if ($validator->canUseTwoFa()) {
$content = FrontController::getInstance()->dispatch('TwoFactorAuth', 'userSettings');
if (!empty($content)) {
$out .= $content;
}
}
}
public function onSuccessfulSession($login)
{
if (Piwik::getModule() === 'Login' && Piwik::getAction() === 'logme' && $login) {
// we allow user to send an "authCode" along logme to directly log in... if not, user will see the
// auth code verification screen after logme
$authCode = Common::getRequestVar('authCode', '', 'string');
$twoFa = $this->getTwoFa();
if ($authCode
&& TwoFactorAuthentication::isUserUsingTwoFactorAuthentication($login)
&& $twoFa->validateAuthCode($login, $authCode)) {
$sessionFingerprint = new SessionFingerprint();
$sessionFingerprint->setTwoFactorAuthenticationVerified();
}
}
}
private function getTwoFa()
{
return StaticContainer::get(TwoFactorAuthentication::class);
}
private function getValidator()
{
return StaticContainer::get(Validator::class);
}
private function isValidTokenAuth($tokenAuth)
{
$model = new Model();
$user = $model->getUserByTokenAuth($tokenAuth);
return !empty($user);
}
public function onCreateAppSpecificTokenAuth($returnedValue, $params)
{
if (!SettingsPiwik::isMatomoInstalled()) {
return;
}
if (!empty($returnedValue) && !empty($params['parameters']['userLogin'])) {
$login = $params['parameters']['userLogin'];
$twoFa = $this->getTwoFa();
if (TwoFactorAuthentication::isUserUsingTwoFactorAuthentication($login) && $this->isValidTokenAuth($returnedValue)) {
$authCode = Common::getRequestVar('authCode', '', 'string');
// we only return an error when the login/password combo was correct. otherwise you could brute force
// auth tokens
if (!$authCode) {
http_response_code(401);
throw new Exception(Piwik::translate('TwoFactorAuth_MissingAuthCodeAPI'));
}
if (!$twoFa->validateAuthCode($login, $authCode)) {
http_response_code(401);
throw new Exception(Piwik::translate('TwoFactorAuth_InvalidAuthCode'));
}
} else if ($twoFa->isUserRequiredToHaveTwoFactorEnabled()
&& !TwoFactorAuthentication::isUserUsingTwoFactorAuthentication($login)) {
throw new Exception(Piwik::translate('TwoFactorAuth_RequiredAuthCodeNotConfiguredAPI'));
}
}
}
public function onRequestDispatch(&$module, &$action, $parameters)
{
$validator = $this->getValidator();
if (!$validator->canUseTwoFa()) {
return;
}
if ($module === 'Proxy') {
return false;
}
if (!$this->requiresAuth($module, $action, $parameters)) {
return;
}
$twoFa = $this->getTwoFa();
$isUsing2FA = TwoFactorAuthentication::isUserUsingTwoFactorAuthentication(Piwik::getCurrentUserLogin());
if ($isUsing2FA && Session::isStarted()) {
$sessionFingerprint = new SessionFingerprint();
if (!$sessionFingerprint->hasVerifiedTwoFactor()) {
if (!Request::isRootRequestApiRequest()) {
$module = 'TwoFactorAuth';
$action = 'loginTwoFactorAuth';
} else if (Common::getRequestVar('force_api_session', 0) == 1) {
// don't allow API requests with session auth if 2fa code hasn't been verified.
throw new Exception(Piwik::translate('General_YourSessionHasExpired'));
}
}
} elseif (!$isUsing2FA && $twoFa->isUserRequiredToHaveTwoFactorEnabled()) {
$module = 'TwoFactorAuth';
$action = 'onLoginSetupTwoFactorAuth';
}
}
private function requiresAuth($module, $action, $parameters)
{
if ($module === 'TwoFactorAuth' && $action === 'showQrCode') {
return false;
}
if ($module === 'CorePluginsAdmin' && strtolower($action) === 'safemode') {
return false;
}
if ($module === 'CoreUpdater' && $action !== 'newVersionAvailable' && $action !== 'oneClickUpdate') {
return false;
}
if ($module === Piwik::getLoginPluginName() && $action === 'logout') {
return false;
}
$auth = StaticContainer::get('Piwik\Auth');
if ($auth && !$auth->getLogin() && method_exists($auth, 'getTokenAuth') && $auth->getTokenAuth()) {
// when authenticated by token only, we do not require 2fa
// needed eg for rendering exported widgets authenticated by token
return false;
}
$requiresAuth = true;
Piwik::postEvent('TwoFactorAuth.requiresTwoFactorAuthentication', array(&$requiresAuth, $module, $action, $parameters));
return $requiresAuth;
}
public function onRequestDispatchEnd(&$result, $module, $action, $parameters)
{
$validator = $this->getValidator();
if (!$validator->canUseTwoFa()) {
return;
}
if (!$this->requiresAuth($module, $action, $parameters)) {
return;
}
$twoFa = $this->getTwoFa();
$isUsing2FA = TwoFactorAuthentication::isUserUsingTwoFactorAuthentication(Piwik::getCurrentUserLogin());
if ($isUsing2FA && !Request::isRootRequestApiRequest()) {
$sessionFingerprint = new SessionFingerprint();
if (!$sessionFingerprint->hasVerifiedTwoFactor()) {
$result = $this->removeTokenFromOutput($result);
}
} elseif (!$isUsing2FA && $twoFa->isUserRequiredToHaveTwoFactorEnabled()) {
$result = $this->removeTokenFromOutput($result);
}
}
private function removeTokenFromOutput($output)
{
if (empty($output)) {
return $output;
}
$token = Piwik::getCurrentUserTokenAuth();
// make sure to not leak the token... otherwise someone could log in using someone's credentials...
// and then maybe in the auth screen look into the DOM to find the token... and then bypass the
// auth code using API
return str_replace($token, md5('') . '2fareplaced', $output);
}
}