PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 1.1.3
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v1.1.3
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / plugins / TwoFactorAuth / Controller.php
matomo / app / plugins / TwoFactorAuth Last commit date
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