1
|
|
|
<?php |
2
|
|
|
/****************************************************************************** |
3
|
|
|
* Wikipedia Account Creation Assistance tool * |
4
|
|
|
* * |
5
|
|
|
* All code in this file is released into the public domain by the ACC * |
6
|
|
|
* Development Team. Please see team.json for a list of contributors. * |
7
|
|
|
******************************************************************************/ |
8
|
|
|
|
9
|
|
|
namespace Waca\Pages\UserAuth\MultiFactor; |
10
|
|
|
|
11
|
|
|
use BaconQrCode\Renderer\Image\Svg; |
12
|
|
|
use BaconQrCode\Writer; |
13
|
|
|
use u2flib_server\U2F; |
14
|
|
|
use Waca\DataObjects\User; |
15
|
|
|
use Waca\Exceptions\ApplicationLogicException; |
16
|
|
|
use Waca\PdoDatabase; |
17
|
|
|
use Waca\Security\CredentialProviders\ICredentialProvider; |
18
|
|
|
use Waca\Security\CredentialProviders\PasswordCredentialProvider; |
19
|
|
|
use Waca\Security\CredentialProviders\ScratchTokenCredentialProvider; |
20
|
|
|
use Waca\Security\CredentialProviders\TotpCredentialProvider; |
21
|
|
|
use Waca\Security\CredentialProviders\U2FCredentialProvider; |
22
|
|
|
use Waca\Security\CredentialProviders\YubikeyOtpCredentialProvider; |
23
|
|
|
use Waca\SessionAlert; |
24
|
|
|
use Waca\Tasks\InternalPageBase; |
25
|
|
|
use Waca\WebRequest; |
26
|
|
|
|
27
|
|
|
class PageMultiFactor extends InternalPageBase |
28
|
|
|
{ |
29
|
|
|
/** |
30
|
|
|
* Main function for this page, when no specific actions are called. |
31
|
|
|
* @return void |
32
|
|
|
*/ |
33
|
|
|
protected function main() |
34
|
|
|
{ |
35
|
|
|
$database = $this->getDatabase(); |
36
|
|
|
$currentUser = User::getCurrent($database); |
37
|
|
|
|
38
|
|
|
$yubikeyOtpCredentialProvider = new YubikeyOtpCredentialProvider($database, $this->getSiteConfiguration(), |
39
|
|
|
$this->getHttpHelper()); |
40
|
|
|
$this->assign('yubikeyOtpIdentity', $yubikeyOtpCredentialProvider->getYubikeyData($currentUser->getId())); |
41
|
|
|
$this->assign('yubikeyOtpEnrolled', $yubikeyOtpCredentialProvider->userIsEnrolled($currentUser->getId())); |
42
|
|
|
|
43
|
|
|
$totpCredentialProvider = new TotpCredentialProvider($database, $this->getSiteConfiguration()); |
44
|
|
|
$this->assign('totpEnrolled', $totpCredentialProvider->userIsEnrolled($currentUser->getId())); |
45
|
|
|
|
46
|
|
|
$u2fCredentialProvider = new U2FCredentialProvider($database, $this->getSiteConfiguration()); |
47
|
|
|
$this->assign('u2fEnrolled', $u2fCredentialProvider->userIsEnrolled($currentUser->getId())); |
48
|
|
|
|
49
|
|
|
$scratchCredentialProvider = new ScratchTokenCredentialProvider($database, $this->getSiteConfiguration()); |
50
|
|
|
$this->assign('scratchEnrolled', $scratchCredentialProvider->userIsEnrolled($currentUser->getId())); |
51
|
|
|
$this->assign('scratchRemaining', $scratchCredentialProvider->getRemaining($currentUser->getId())); |
52
|
|
|
|
53
|
|
|
$this->setTemplate('mfa/mfa.tpl'); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
protected function enableYubikeyOtp() |
57
|
|
|
{ |
58
|
|
|
$database = $this->getDatabase(); |
59
|
|
|
$currentUser = User::getCurrent($database); |
60
|
|
|
|
61
|
|
|
$otpCredentialProvider = new YubikeyOtpCredentialProvider($database, |
62
|
|
|
$this->getSiteConfiguration(), $this->getHttpHelper()); |
63
|
|
|
|
64
|
|
|
if (WebRequest::wasPosted()) { |
65
|
|
|
$this->validateCSRFToken(); |
66
|
|
|
|
67
|
|
|
$passwordCredentialProvider = new PasswordCredentialProvider($database, |
68
|
|
|
$this->getSiteConfiguration()); |
69
|
|
|
|
70
|
|
|
$password = WebRequest::postString('password'); |
71
|
|
|
$otp = WebRequest::postString('otp'); |
72
|
|
|
|
73
|
|
|
$result = $passwordCredentialProvider->authenticate($currentUser, $password); |
74
|
|
|
|
75
|
|
|
if ($result) { |
76
|
|
|
try { |
77
|
|
|
$otpCredentialProvider->setCredential($currentUser, 2, $otp); |
78
|
|
|
SessionAlert::success('Enabled YubiKey OTP.'); |
79
|
|
|
|
80
|
|
|
$scratchProvider = new ScratchTokenCredentialProvider($database, $this->getSiteConfiguration()); |
81
|
|
View Code Duplication |
if($scratchProvider->getRemaining($currentUser->getId()) < 3) { |
|
|
|
|
82
|
|
|
$scratchProvider->setCredential($currentUser, 2, null); |
83
|
|
|
$tokens = $scratchProvider->getTokens(); |
84
|
|
|
$this->assign('tokens', $tokens); |
85
|
|
|
$this->setTemplate('mfa/regenScratchTokens.tpl'); |
86
|
|
|
return; |
87
|
|
|
} |
88
|
|
|
} |
89
|
|
|
catch (ApplicationLogicException $ex) { |
90
|
|
|
SessionAlert::error('Error enabling YubiKey OTP: ' . $ex->getMessage()); |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
$this->redirect('multiFactor'); |
94
|
|
|
} |
95
|
|
|
else { |
96
|
|
|
SessionAlert::error('Error enabling YubiKey OTP - invalid credentials.'); |
97
|
|
|
$this->redirect('multiFactor'); |
98
|
|
|
} |
99
|
|
|
} |
100
|
|
|
else { |
101
|
|
|
if ($otpCredentialProvider->userIsEnrolled($currentUser->getId())) { |
102
|
|
|
// user is not enrolled, we shouldn't have got here. |
103
|
|
|
throw new ApplicationLogicException('User is already enrolled in the selected MFA mechanism'); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
$this->assignCSRFToken(); |
107
|
|
|
$this->setTemplate('mfa/enableYubikey.tpl'); |
108
|
|
|
} |
109
|
|
|
} |
110
|
|
|
|
111
|
|
View Code Duplication |
protected function disableYubikeyOtp() |
|
|
|
|
112
|
|
|
{ |
113
|
|
|
$database = $this->getDatabase(); |
114
|
|
|
$currentUser = User::getCurrent($database); |
115
|
|
|
|
116
|
|
|
$otpCredentialProvider = new YubikeyOtpCredentialProvider($database, |
117
|
|
|
$this->getSiteConfiguration(), $this->getHttpHelper()); |
118
|
|
|
|
119
|
|
|
$factorType = 'YubiKey OTP'; |
120
|
|
|
|
121
|
|
|
$this->deleteCredential($database, $currentUser, $otpCredentialProvider, $factorType); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
protected function enableTotp() |
125
|
|
|
{ |
126
|
|
|
$database = $this->getDatabase(); |
127
|
|
|
$currentUser = User::getCurrent($database); |
128
|
|
|
|
129
|
|
|
$otpCredentialProvider = new TotpCredentialProvider($database, $this->getSiteConfiguration()); |
130
|
|
|
|
131
|
|
|
if (WebRequest::wasPosted()) { |
132
|
|
|
$this->validateCSRFToken(); |
133
|
|
|
|
134
|
|
|
// used for routing only, not security |
135
|
|
|
$stage = WebRequest::postString('stage'); |
136
|
|
|
|
137
|
|
|
if ($stage === "auth") { |
138
|
|
|
$password = WebRequest::postString('password'); |
139
|
|
|
|
140
|
|
|
$passwordCredentialProvider = new PasswordCredentialProvider($database, |
141
|
|
|
$this->getSiteConfiguration()); |
142
|
|
|
$result = $passwordCredentialProvider->authenticate($currentUser, $password); |
143
|
|
|
|
144
|
|
|
if ($result) { |
145
|
|
|
$otpCredentialProvider->setCredential($currentUser, 2, null); |
146
|
|
|
|
147
|
|
|
$provisioningUrl = $otpCredentialProvider->getProvisioningUrl($currentUser); |
148
|
|
|
|
149
|
|
|
$renderer = new Svg(); |
150
|
|
|
$renderer->setHeight(256); |
151
|
|
|
$renderer->setWidth(256); |
152
|
|
|
$writer = new Writer($renderer); |
153
|
|
|
$svg = $writer->writeString($provisioningUrl); |
154
|
|
|
|
155
|
|
|
$this->assign('svg', $svg); |
156
|
|
|
$this->assign('secret', $otpCredentialProvider->getSecret($currentUser)); |
157
|
|
|
|
158
|
|
|
$this->assignCSRFToken(); |
159
|
|
|
$this->setTemplate('mfa/enableTotpEnroll.tpl'); |
160
|
|
|
|
161
|
|
|
return; |
162
|
|
|
} |
163
|
|
|
else { |
164
|
|
|
SessionAlert::error('Error enabling TOTP - invalid credentials.'); |
165
|
|
|
$this->redirect('multiFactor'); |
166
|
|
|
|
167
|
|
|
return; |
168
|
|
|
} |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
if ($stage === "enroll") { |
172
|
|
|
// we *must* have a defined credential already here, |
173
|
|
|
if ($otpCredentialProvider->isPartiallyEnrolled($currentUser)) { |
174
|
|
|
$otp = WebRequest::postString('otp'); |
175
|
|
|
$result = $otpCredentialProvider->verifyEnable($currentUser, $otp); |
176
|
|
|
|
177
|
|
|
if ($result) { |
178
|
|
|
SessionAlert::success('Enabled TOTP.'); |
179
|
|
|
|
180
|
|
|
$scratchProvider = new ScratchTokenCredentialProvider($database, $this->getSiteConfiguration()); |
181
|
|
View Code Duplication |
if($scratchProvider->getRemaining($currentUser->getId()) < 3) { |
|
|
|
|
182
|
|
|
$scratchProvider->setCredential($currentUser, 2, null); |
183
|
|
|
$tokens = $scratchProvider->getTokens(); |
184
|
|
|
$this->assign('tokens', $tokens); |
185
|
|
|
$this->setTemplate('mfa/regenScratchTokens.tpl'); |
186
|
|
|
return; |
187
|
|
|
} |
188
|
|
|
} |
189
|
|
|
else { |
190
|
|
|
$otpCredentialProvider->deleteCredential($currentUser); |
191
|
|
|
SessionAlert::error('Error enabling TOTP: invalid token provided'); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
|
195
|
|
|
$this->redirect('multiFactor'); |
196
|
|
|
return; |
197
|
|
|
} |
198
|
|
|
else { |
199
|
|
|
SessionAlert::error('Error enabling TOTP - no enrollment found or enrollment expired.'); |
200
|
|
|
$this->redirect('multiFactor'); |
201
|
|
|
|
202
|
|
|
return; |
203
|
|
|
} |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
// urgh, dunno what happened, but it's not something expected. |
207
|
|
|
throw new ApplicationLogicException(); |
208
|
|
|
} |
209
|
|
|
else { |
210
|
|
|
if ($otpCredentialProvider->userIsEnrolled($currentUser->getId())) { |
211
|
|
|
// user is not enrolled, we shouldn't have got here. |
212
|
|
|
throw new ApplicationLogicException('User is already enrolled in the selected MFA mechanism'); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
$this->assignCSRFToken(); |
216
|
|
|
$this->setTemplate('mfa/enableTotpAuth.tpl'); |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
|
220
|
|
View Code Duplication |
protected function disableTotp() |
|
|
|
|
221
|
|
|
{ |
222
|
|
|
$database = $this->getDatabase(); |
223
|
|
|
$currentUser = User::getCurrent($database); |
224
|
|
|
|
225
|
|
|
$otpCredentialProvider = new TotpCredentialProvider($database, $this->getSiteConfiguration()); |
226
|
|
|
|
227
|
|
|
$factorType = 'TOTP'; |
228
|
|
|
|
229
|
|
|
$this->deleteCredential($database, $currentUser, $otpCredentialProvider, $factorType); |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
protected function enableU2F() { |
233
|
|
|
$database = $this->getDatabase(); |
234
|
|
|
$currentUser = User::getCurrent($database); |
235
|
|
|
|
236
|
|
|
$otpCredentialProvider = new U2FCredentialProvider($database, $this->getSiteConfiguration()); |
237
|
|
|
|
238
|
|
|
if (WebRequest::wasPosted()) { |
239
|
|
|
$this->validateCSRFToken(); |
240
|
|
|
|
241
|
|
|
// used for routing only, not security |
242
|
|
|
$stage = WebRequest::postString('stage'); |
243
|
|
|
|
244
|
|
|
if ($stage === "auth") { |
245
|
|
|
$password = WebRequest::postString('password'); |
246
|
|
|
|
247
|
|
|
$passwordCredentialProvider = new PasswordCredentialProvider($database, |
248
|
|
|
$this->getSiteConfiguration()); |
249
|
|
|
$result = $passwordCredentialProvider->authenticate($currentUser, $password); |
250
|
|
|
|
251
|
|
|
if ($result) { |
252
|
|
|
$otpCredentialProvider->setCredential($currentUser, 2, null); |
253
|
|
|
$this->assignCSRFToken(); |
254
|
|
|
|
255
|
|
|
list($data, $reqs) = $otpCredentialProvider->getRegistrationData(); |
256
|
|
|
|
257
|
|
|
$u2fRequest =json_encode($data); |
258
|
|
|
$u2fSigns = json_encode($reqs); |
259
|
|
|
|
260
|
|
|
$this->addJs('/vendor/yubico/u2flib-server/examples/assets/u2f-api.js'); |
261
|
|
|
$this->setTailScript(<<<JS |
262
|
|
|
var request = ${u2fRequest}; |
263
|
|
|
var signs = ${u2fSigns}; |
264
|
|
|
|
265
|
|
|
u2f.register([request], signs, function(data) { |
266
|
|
|
var form = document.getElementById('u2fEnroll'); |
267
|
|
|
var reg = document.getElementById('u2fData'); |
268
|
|
|
var req = document.getElementById('u2fRequest'); |
269
|
|
|
|
270
|
|
|
if(data.errorCode && data.errorCode != 0) { |
271
|
|
|
alert("registration failed with errror: " + data.errorCode); |
272
|
|
|
return; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
reg.value=JSON.stringify(data); |
276
|
|
|
req.value=JSON.stringify(request); |
277
|
|
|
form.submit(); |
278
|
|
|
}); |
279
|
|
|
JS |
280
|
|
|
); |
281
|
|
|
|
282
|
|
|
$this->setTemplate('mfa/enableU2FEnroll.tpl'); |
283
|
|
|
|
284
|
|
|
return; |
285
|
|
|
} |
286
|
|
|
else { |
287
|
|
|
SessionAlert::error('Error enabling TOTP - invalid credentials.'); |
288
|
|
|
$this->redirect('multiFactor'); |
289
|
|
|
|
290
|
|
|
return; |
291
|
|
|
} |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
if ($stage === "enroll") { |
295
|
|
|
// we *must* have a defined credential already here, |
296
|
|
|
if ($otpCredentialProvider->isPartiallyEnrolled($currentUser)) { |
297
|
|
|
|
298
|
|
|
$request = json_decode(WebRequest::postString('u2fRequest')); |
299
|
|
|
$u2fData = json_decode(WebRequest::postString('u2fData')); |
300
|
|
|
|
301
|
|
|
$otpCredentialProvider->enable($currentUser, $request, $u2fData); |
302
|
|
|
|
303
|
|
|
SessionAlert::success('Enabled TOTP.'); |
304
|
|
|
|
305
|
|
|
$scratchProvider = new ScratchTokenCredentialProvider($database, $this->getSiteConfiguration()); |
306
|
|
View Code Duplication |
if($scratchProvider->getRemaining($currentUser->getId()) < 3) { |
|
|
|
|
307
|
|
|
$scratchProvider->setCredential($currentUser, 2, null); |
308
|
|
|
$tokens = $scratchProvider->getTokens(); |
309
|
|
|
$this->assign('tokens', $tokens); |
310
|
|
|
$this->setTemplate('mfa/regenScratchTokens.tpl'); |
311
|
|
|
return; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
$this->redirect('multiFactor'); |
315
|
|
|
return; |
316
|
|
|
} |
317
|
|
|
else { |
318
|
|
|
SessionAlert::error('Error enabling TOTP - no enrollment found or enrollment expired.'); |
319
|
|
|
$this->redirect('multiFactor'); |
320
|
|
|
|
321
|
|
|
return; |
322
|
|
|
} |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
// urgh, dunno what happened, but it's not something expected. |
326
|
|
|
throw new ApplicationLogicException(); |
327
|
|
|
} |
328
|
|
|
else { |
329
|
|
|
if ($otpCredentialProvider->userIsEnrolled($currentUser->getId())) { |
330
|
|
|
// user is not enrolled, we shouldn't have got here. |
331
|
|
|
throw new ApplicationLogicException('User is already enrolled in the selected MFA mechanism'); |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
$this->assignCSRFToken(); |
335
|
|
|
$this->setTemplate('mfa/enableU2FAuth.tpl'); |
336
|
|
|
} |
337
|
|
|
} |
338
|
|
|
|
339
|
|
View Code Duplication |
protected function disableU2F() { |
|
|
|
|
340
|
|
|
$database = $this->getDatabase(); |
341
|
|
|
$currentUser = User::getCurrent($database); |
342
|
|
|
|
343
|
|
|
$otpCredentialProvider = new U2FCredentialProvider($database, $this->getSiteConfiguration()); |
344
|
|
|
|
345
|
|
|
$factorType = 'U2F'; |
346
|
|
|
|
347
|
|
|
$this->deleteCredential($database, $currentUser, $otpCredentialProvider, $factorType); |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
protected function scratch() |
351
|
|
|
{ |
352
|
|
|
$database = $this->getDatabase(); |
353
|
|
|
$currentUser = User::getCurrent($database); |
354
|
|
|
|
355
|
|
|
if (WebRequest::wasPosted()) { |
356
|
|
|
$this->validateCSRFToken(); |
357
|
|
|
|
358
|
|
|
$passwordCredentialProvider = new PasswordCredentialProvider($database, |
359
|
|
|
$this->getSiteConfiguration()); |
360
|
|
|
|
361
|
|
|
$otpCredentialProvider = new ScratchTokenCredentialProvider($database, |
362
|
|
|
$this->getSiteConfiguration()); |
363
|
|
|
|
364
|
|
|
$password = WebRequest::postString('password'); |
365
|
|
|
|
366
|
|
|
$result = $passwordCredentialProvider->authenticate($currentUser, $password); |
367
|
|
|
|
368
|
|
View Code Duplication |
if ($result) { |
|
|
|
|
369
|
|
|
$otpCredentialProvider->setCredential($currentUser, 2, null); |
370
|
|
|
$tokens = $otpCredentialProvider->getTokens(); |
371
|
|
|
$this->assign('tokens', $tokens); |
372
|
|
|
$this->setTemplate('mfa/regenScratchTokens.tpl'); |
373
|
|
|
} |
374
|
|
|
else { |
375
|
|
|
SessionAlert::error('Error refreshing scratch tokens - invalid credentials.'); |
376
|
|
|
$this->redirect('multiFactor'); |
377
|
|
|
} |
378
|
|
|
} |
379
|
|
|
else { |
380
|
|
|
$this->assignCSRFToken(); |
381
|
|
|
$this->setTemplate('mfa/regenScratchAuth.tpl'); |
382
|
|
|
} |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
/** |
386
|
|
|
* @param PdoDatabase $database |
387
|
|
|
* @param User $currentUser |
388
|
|
|
* @param ICredentialProvider $otpCredentialProvider |
389
|
|
|
* @param string $factorType |
390
|
|
|
* |
391
|
|
|
* @throws ApplicationLogicException |
392
|
|
|
*/ |
393
|
|
|
private function deleteCredential( |
394
|
|
|
PdoDatabase $database, |
395
|
|
|
User $currentUser, |
396
|
|
|
ICredentialProvider $otpCredentialProvider, |
397
|
|
|
$factorType |
398
|
|
|
) { |
399
|
|
|
if (WebRequest::wasPosted()) { |
400
|
|
|
$passwordCredentialProvider = new PasswordCredentialProvider($database, |
401
|
|
|
$this->getSiteConfiguration()); |
402
|
|
|
|
403
|
|
|
$this->validateCSRFToken(); |
404
|
|
|
|
405
|
|
|
$password = WebRequest::postString('password'); |
406
|
|
|
$result = $passwordCredentialProvider->authenticate($currentUser, $password); |
407
|
|
|
|
408
|
|
|
if ($result) { |
409
|
|
|
$otpCredentialProvider->deleteCredential($currentUser); |
410
|
|
|
SessionAlert::success('Disabled ' . $factorType . '.'); |
411
|
|
|
$this->redirect('multiFactor'); |
412
|
|
|
} |
413
|
|
|
else { |
414
|
|
|
SessionAlert::error('Error disabling ' . $factorType . ' - invalid credentials.'); |
415
|
|
|
$this->redirect('multiFactor'); |
416
|
|
|
} |
417
|
|
|
} |
418
|
|
|
else { |
419
|
|
|
if (!$otpCredentialProvider->userIsEnrolled($currentUser->getId())) { |
420
|
|
|
// user is not enrolled, we shouldn't have got here. |
421
|
|
|
throw new ApplicationLogicException('User is not enrolled in the selected MFA mechanism'); |
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
$this->assignCSRFToken(); |
425
|
|
|
$this->assign('otpType', $factorType); |
426
|
|
|
$this->setTemplate('mfa/disableOtp.tpl'); |
427
|
|
|
} |
428
|
|
|
} |
429
|
|
|
} |
430
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.