Commands
6 years ago
Dao
6 years ago
config
6 years ago
stylesheets
6 years ago
templates
6 years ago
API.php
6 years ago
Controller.php
6 years ago
FormTwoFactorAuthCode.php
6 years ago
SystemSettings.php
6 years ago
TwoFactorAuth.php
6 years ago
TwoFactorAuthentication.php
6 years ago
Validator.php
6 years ago
Controller.php
323 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Matomo - free/libre analytics platform |
| 4 | * |
| 5 | * @link https://matomo.org |
| 6 | * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later |
| 7 | */ |
| 8 | namespace Piwik\Plugins\TwoFactorAuth; |
| 9 | |
| 10 | use Endroid\QrCode\QrCode; |
| 11 | use Piwik\API\Request; |
| 12 | use Piwik\Common; |
| 13 | use Piwik\Container\StaticContainer; |
| 14 | use Piwik\IP; |
| 15 | use Piwik\Nonce; |
| 16 | use Piwik\Piwik; |
| 17 | use Piwik\Plugins\Login\PasswordVerifier; |
| 18 | use Piwik\Plugins\TwoFactorAuth\Dao\RecoveryCodeDao; |
| 19 | use Piwik\Session\SessionFingerprint; |
| 20 | use Piwik\Session\SessionNamespace; |
| 21 | use Piwik\Url; |
| 22 | use Piwik\View; |
| 23 | use Exception; |
| 24 | |
| 25 | class Controller extends \Piwik\Plugin\Controller |
| 26 | { |
| 27 | const AUTH_CODE_NONCE = 'TwoFactorAuth.saveAuthCode'; |
| 28 | const LOGIN_2FA_NONCE = 'TwoFactorAuth.loginAuthCode'; |
| 29 | const DISABLE_2FA_NONCE = 'TwoFactorAuth.disableAuthCode'; |
| 30 | const REGENERATE_CODES_2FA_NONCE = 'TwoFactorAuth.regenerateCodes'; |
| 31 | const VERIFY_PASSWORD_NONCE = 'TwoFactorAuth.verifyPassword'; |
| 32 | |
| 33 | /** |
| 34 | * @var SystemSettings |
| 35 | */ |
| 36 | private $settings; |
| 37 | |
| 38 | /** |
| 39 | * @var RecoveryCodeDao |
| 40 | */ |
| 41 | private $recoveryCodeDao; |
| 42 | |
| 43 | /** |
| 44 | * @var PasswordVerifier |
| 45 | */ |
| 46 | private $passwordVerify; |
| 47 | |
| 48 | /** |
| 49 | * @var TwoFactorAuthentication |
| 50 | */ |
| 51 | private $twoFa; |
| 52 | |
| 53 | /** |
| 54 | * @var Validator |
| 55 | */ |
| 56 | private $validator; |
| 57 | |
| 58 | public function __construct(SystemSettings $systemSettings, RecoveryCodeDao $recoveryCodeDao, PasswordVerifier $passwordVerify, TwoFactorAuthentication $twoFa, Validator $validator) |
| 59 | { |
| 60 | $this->settings = $systemSettings; |
| 61 | $this->recoveryCodeDao = $recoveryCodeDao; |
| 62 | $this->passwordVerify = $passwordVerify; |
| 63 | $this->twoFa = $twoFa; |
| 64 | $this->validator = $validator; |
| 65 | |
| 66 | parent::__construct(); |
| 67 | } |
| 68 | |
| 69 | public function loginTwoFactorAuth() |
| 70 | { |
| 71 | $this->validator->checkCanUseTwoFa(); |
| 72 | $this->validator->check2FaEnabled(); |
| 73 | $this->validator->checkNotVerified2FAYet(); |
| 74 | |
| 75 | $messageNoAccess = null; |
| 76 | |
| 77 | $view = new View('@TwoFactorAuth/loginTwoFactorAuth'); |
| 78 | $form = new FormTwoFactorAuthCode(); |
| 79 | $form->removeAttribute('action'); // remove action attribute, otherwise hash part will be lost |
| 80 | if ($form->validate()) { |
| 81 | $nonce = $form->getSubmitValue('form_nonce'); |
| 82 | if ($nonce && Nonce::verifyNonce(self::LOGIN_2FA_NONCE, $nonce) && $form->validate()) { |
| 83 | $authCode = $form->getSubmitValue('form_authcode'); |
| 84 | if ($authCode && is_string($authCode)) { |
| 85 | $authCode = str_replace('-', '', $authCode); |
| 86 | $authCode = strtoupper($authCode); // recovery codes are stored upper case, app codes are only numbers |
| 87 | $authCode = trim($authCode); |
| 88 | } |
| 89 | |
| 90 | if ($this->twoFa->validateAuthCode(Piwik::getCurrentUserLogin(), $authCode)) { |
| 91 | $sessionFingerprint = new SessionFingerprint(); |
| 92 | $sessionFingerprint->setTwoFactorAuthenticationVerified(); |
| 93 | Url::redirectToUrl(Url::getCurrentUrl()); |
| 94 | } else { |
| 95 | $messageNoAccess = Piwik::translate('TwoFactorAuth_InvalidAuthCode'); |
| 96 | try { |
| 97 | $bruteForce = StaticContainer::get('Piwik\Plugins\Login\Security\BruteForceDetection'); |
| 98 | if ($bruteForce->isEnabled()) { |
| 99 | $bruteForce->addFailedAttempt(IP::getIpFromHeader()); |
| 100 | } |
| 101 | } catch (Exception $e) { |
| 102 | // ignore error eg if login plugin is disabled |
| 103 | } |
| 104 | } |
| 105 | } else { |
| 106 | $messageNoAccess = Piwik::translate('Login_InvalidNonceOrHeadersOrReferrer', array('<a target="_blank" rel="noreferrer noopener" href="https://matomo.org/faq/how-to-install/#faq_98">', '</a>')); |
| 107 | } |
| 108 | } |
| 109 | $superUsers = Request::processRequest('UsersManager.getUsersHavingSuperUserAccess', [], []); |
| 110 | $view->superUserEmails = implode(',', array_column($superUsers, 'email')); |
| 111 | $view->loginModule = Piwik::getLoginPluginName(); |
| 112 | $view->AccessErrorString = $messageNoAccess; |
| 113 | $view->addForm($form); |
| 114 | $this->setBasicVariablesView($view); |
| 115 | $view->nonce = Nonce::getNonce(self::LOGIN_2FA_NONCE); |
| 116 | |
| 117 | return $view->render(); |
| 118 | } |
| 119 | |
| 120 | public function userSettings() |
| 121 | { |
| 122 | $this->validator->checkCanUseTwoFa(); |
| 123 | |
| 124 | return $this->renderTemplate('userSettings', array( |
| 125 | 'isEnabled' => $this->twoFa->isUserUsingTwoFactorAuthentication(Piwik::getCurrentUserLogin()), |
| 126 | 'isForced' => $this->twoFa->isUserRequiredToHaveTwoFactorEnabled(), |
| 127 | 'disableNonce' => Nonce::getNonce(self::DISABLE_2FA_NONCE) |
| 128 | )); |
| 129 | } |
| 130 | |
| 131 | public function disableTwoFactorAuth() |
| 132 | { |
| 133 | $this->validator->checkCanUseTwoFa(); |
| 134 | $this->validator->check2FaEnabled(); |
| 135 | $this->validator->checkVerified2FA(); |
| 136 | |
| 137 | if ($this->twoFa->isUserRequiredToHaveTwoFactorEnabled()) { |
| 138 | throw new Exception('Two-factor authentication cannot be disabled as it is enforced'); |
| 139 | } |
| 140 | |
| 141 | $nonce = Common::getRequestVar('disableNonce', null, 'string'); |
| 142 | $params = array('module' => 'TwoFactorAuth', 'action' => 'disableTwoFactorAuth', 'disableNonce' => $nonce); |
| 143 | |
| 144 | if ($this->passwordVerify->requirePasswordVerifiedRecently($params)) { |
| 145 | |
| 146 | Nonce::checkNonce(self::DISABLE_2FA_NONCE, $nonce); |
| 147 | |
| 148 | $this->twoFa->disable2FAforUser(Piwik::getCurrentUserLogin()); |
| 149 | $this->passwordVerify->forgetVerifiedPassword(); |
| 150 | |
| 151 | $this->redirectToIndex('UsersManager', 'userSettings', null, null, null, array( |
| 152 | 'disableNonce' => false |
| 153 | )); |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | private function make2faSession() |
| 158 | { |
| 159 | return new SessionNamespace('TwoFactorAuthenticator'); |
| 160 | } |
| 161 | |
| 162 | public function onLoginSetupTwoFactorAuth() |
| 163 | { |
| 164 | // called when 2fa is required, but user has not yet set up 2fa |
| 165 | |
| 166 | return $this->setupTwoFactorAuth($standalone = true); |
| 167 | } |
| 168 | |
| 169 | /** |
| 170 | * Action to setup two factor authentication |
| 171 | * |
| 172 | * @return string |
| 173 | * @throws \Exception |
| 174 | */ |
| 175 | public function setupTwoFactorAuth($standalone = false) |
| 176 | { |
| 177 | $this->validator->checkCanUseTwoFa(); |
| 178 | |
| 179 | if ($standalone) { |
| 180 | $view = new View('@TwoFactorAuth/setupTwoFactorAuthStandalone'); |
| 181 | $this->setBasicVariablesView($view); |
| 182 | $view->submitAction = 'onLoginSetupTwoFactorAuth'; |
| 183 | } else { |
| 184 | $view = new View('@TwoFactorAuth/setupTwoFactorAuth'); |
| 185 | $this->setGeneralVariablesView($view); |
| 186 | $view->submitAction = 'setupTwoFactorAuth'; |
| 187 | |
| 188 | $redirectParams = array('module' => 'TwoFactorAuth', 'action' => 'setupTwoFactorAuth'); |
| 189 | if (!$this->passwordVerify->requirePasswordVerified($redirectParams)) { |
| 190 | // should usually not go in here but redirect instead |
| 191 | throw new Exception('You have to verify your password first.'); |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | $session = $this->make2faSession(); |
| 196 | |
| 197 | if (empty($session->secret)) { |
| 198 | $session->secret = $this->twoFa->generateSecret(); |
| 199 | } |
| 200 | |
| 201 | $secret = $session->secret; |
| 202 | $session->setExpirationSeconds(60 * 15, 'secret'); |
| 203 | |
| 204 | $authCode = Common::getRequestVar('authCode', '', 'string'); |
| 205 | $authCodeNonce = Common::getRequestVar('authCodeNonce', '', 'string'); |
| 206 | $hasSubmittedForm = !empty($authCodeNonce) || !empty($authCode); |
| 207 | $accessErrorString = ''; |
| 208 | $login = Piwik::getCurrentUserLogin(); |
| 209 | |
| 210 | if (!empty($secret) && !empty($authCode) |
| 211 | && Nonce::verifyNonce(self::AUTH_CODE_NONCE, $authCodeNonce)) { |
| 212 | if ($this->twoFa->validateAuthCodeDuringSetup(trim($authCode), $secret)) { |
| 213 | $this->twoFa->saveSecret($login, $secret); |
| 214 | $fingerprint = new SessionFingerprint(); |
| 215 | $fingerprint->setTwoFactorAuthenticationVerified(); |
| 216 | unset($session->secret); |
| 217 | $this->passwordVerify->forgetVerifiedPassword(); |
| 218 | |
| 219 | Piwik::postEvent('TwoFactorAuth.enabled', array($login)); |
| 220 | |
| 221 | if ($standalone) { |
| 222 | $this->redirectToIndex('CoreHome', 'index'); |
| 223 | return; |
| 224 | } |
| 225 | |
| 226 | $view = new View('@TwoFactorAuth/setupFinished'); |
| 227 | $this->setGeneralVariablesView($view); |
| 228 | return $view->render(); |
| 229 | } else { |
| 230 | $accessErrorString = Piwik::translate('TwoFactorAuth_WrongAuthCodeTryAgain'); |
| 231 | } |
| 232 | } elseif (!$standalone) { |
| 233 | // the user has not posted the form... at least not with a valid nonce... we make sure the password verify |
| 234 | // is valid for at least another 15 minutes and if not, ask for another password confirmation to avoid |
| 235 | // the user may be posting a valid auth code after rendering this screen but the password verify is invalid |
| 236 | // by then. |
| 237 | $redirectParams = array('module' => 'TwoFactorAuth', 'action' => 'setupTwoFactorAuth'); |
| 238 | if (!$this->passwordVerify->requirePasswordVerifiedRecently($redirectParams)) { |
| 239 | throw new Exception('You have to verify your password first.'); |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | if (!$this->recoveryCodeDao->getAllRecoveryCodesForLogin($login) |
| 244 | || (!$hasSubmittedForm && !$this->twoFa->isUserUsingTwoFactorAuthentication($login))) { |
| 245 | // we cannot generate new codes after form has been submitted and user is not yet using 2fa cause we would |
| 246 | // change recovery codes in the background without the user noticing... we cannot simply do this: |
| 247 | // if !getAllRecoveryCodesForLogin => createRecoveryCodesForLogin. Because it could be a security issue that |
| 248 | // user might start the setup but never finishes. Before setting up 2fa the first time we have to change |
| 249 | // the recovery codes |
| 250 | $this->recoveryCodeDao->createRecoveryCodesForLogin($login); |
| 251 | } |
| 252 | |
| 253 | $view->title = $this->settings->twoFactorAuthTitle->getValue(); |
| 254 | $view->description = $login; |
| 255 | $view->authCodeNonce = Nonce::getNonce(self::AUTH_CODE_NONCE); |
| 256 | $view->AccessErrorString = $accessErrorString; |
| 257 | $view->isAlreadyUsing2fa = $this->twoFa->isUserUsingTwoFactorAuthentication($login); |
| 258 | $view->newSecret = $secret; |
| 259 | $view->twoFaBarCodeSetupUrl = $this->getTwoFaBarCodeSetupUrl($secret); |
| 260 | $view->codes = $this->recoveryCodeDao->getAllRecoveryCodesForLogin($login); |
| 261 | $view->standalone = $standalone; |
| 262 | |
| 263 | return $view->render(); |
| 264 | } |
| 265 | |
| 266 | public function showRecoveryCodes() |
| 267 | { |
| 268 | $this->validator->checkCanUseTwoFa(); |
| 269 | $this->validator->checkVerified2FA(); |
| 270 | $this->validator->check2FaEnabled(); |
| 271 | |
| 272 | $regenerateNonce = Common::getRequestVar('regenerateNonce', '', 'string', $_POST); |
| 273 | $postedValidNonce = !empty($regenerateNonce) && Nonce::verifyNonce(self::REGENERATE_CODES_2FA_NONCE, $regenerateNonce); |
| 274 | |
| 275 | $regenerateSuccess = false; |
| 276 | $regenerateError = false; |
| 277 | |
| 278 | if ($postedValidNonce && $this->passwordVerify->hasBeenVerified()) { |
| 279 | $this->passwordVerify->forgetVerifiedPassword(); |
| 280 | $this->recoveryCodeDao->createRecoveryCodesForLogin(Piwik::getCurrentUserLogin()); |
| 281 | $regenerateSuccess = true; |
| 282 | // no need to redirect as password was verified nonce |
| 283 | // if user has posted a valid nonce, we do not need to require password again as nonce must have been generated recent |
| 284 | // avoids use case where eg password verify is only valid for one more minute when opening the page but user regenerates 2min later |
| 285 | } elseif (!$this->passwordVerify->requirePasswordVerifiedRecently(array('module' => 'TwoFactorAuth', 'action' => 'showRecoveryCodes'))) { |
| 286 | // should usually not go in here but redirect instead |
| 287 | throw new Exception('You have to verify your password first.'); |
| 288 | } |
| 289 | |
| 290 | if (!$postedValidNonce && !empty($regenerateNonce)) { |
| 291 | $regenerateError = true; |
| 292 | } |
| 293 | |
| 294 | $recoveryCodes = $this->recoveryCodeDao->getAllRecoveryCodesForLogin(Piwik::getCurrentUserLogin()); |
| 295 | |
| 296 | return $this->renderTemplate('showRecoveryCodes', array( |
| 297 | 'codes' => $recoveryCodes, |
| 298 | 'regenerateNonce' => Nonce::getNonce(self::REGENERATE_CODES_2FA_NONCE), |
| 299 | 'regenerateError' => $regenerateError, |
| 300 | 'regenerateSuccess' => $regenerateSuccess |
| 301 | )); |
| 302 | } |
| 303 | |
| 304 | private function getTwoFaBarCodeSetupUrl($secret) |
| 305 | { |
| 306 | $title = $this->settings->twoFactorAuthTitle->getValue(); |
| 307 | $descr = Piwik::getCurrentUserLogin(); |
| 308 | |
| 309 | $url = 'otpauth://totp/'.urlencode($descr).'?secret='.$secret; |
| 310 | if(isset($title)) { |
| 311 | $url .= '&issuer='.urlencode($title); |
| 312 | } |
| 313 | |
| 314 | return $url; |
| 315 | } |
| 316 | |
| 317 | protected function getQRUrl($description, $title) |
| 318 | { |
| 319 | return sprintf('index.php?module=TwoFactorAuth&action=showQrCode&cb=%s&title=%s&descr=%s', Common::getRandomString(8), urlencode($title), urlencode($description)); |
| 320 | } |
| 321 | |
| 322 | } |
| 323 |