Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/auth/factor/PhabricatorAuthFactor.php
12256 views
1
<?php
2
3
abstract class PhabricatorAuthFactor extends Phobject {
4
5
abstract public function getFactorName();
6
abstract public function getFactorShortName();
7
abstract public function getFactorKey();
8
abstract public function getFactorCreateHelp();
9
abstract public function getFactorDescription();
10
abstract public function processAddFactorForm(
11
PhabricatorAuthFactorProvider $provider,
12
AphrontFormView $form,
13
AphrontRequest $request,
14
PhabricatorUser $user);
15
16
abstract public function renderValidateFactorForm(
17
PhabricatorAuthFactorConfig $config,
18
AphrontFormView $form,
19
PhabricatorUser $viewer,
20
PhabricatorAuthFactorResult $validation_result);
21
22
public function getParameterName(
23
PhabricatorAuthFactorConfig $config,
24
$name) {
25
return 'authfactor.'.$config->getID().'.'.$name;
26
}
27
28
public static function getAllFactors() {
29
return id(new PhutilClassMapQuery())
30
->setAncestorClass(__CLASS__)
31
->setUniqueMethod('getFactorKey')
32
->execute();
33
}
34
35
protected function newConfigForUser(PhabricatorUser $user) {
36
return id(new PhabricatorAuthFactorConfig())
37
->setUserPHID($user->getPHID())
38
->setFactorSecret('');
39
}
40
41
protected function newResult() {
42
return new PhabricatorAuthFactorResult();
43
}
44
45
public function newIconView() {
46
return id(new PHUIIconView())
47
->setIcon('fa-mobile');
48
}
49
50
public function canCreateNewProvider() {
51
return true;
52
}
53
54
public function getProviderCreateDescription() {
55
return null;
56
}
57
58
public function canCreateNewConfiguration(
59
PhabricatorAuthFactorProvider $provider,
60
PhabricatorUser $user) {
61
return true;
62
}
63
64
public function getConfigurationCreateDescription(
65
PhabricatorAuthFactorProvider $provider,
66
PhabricatorUser $user) {
67
return null;
68
}
69
70
public function getConfigurationListDetails(
71
PhabricatorAuthFactorConfig $config,
72
PhabricatorAuthFactorProvider $provider,
73
PhabricatorUser $viewer) {
74
return null;
75
}
76
77
public function newEditEngineFields(
78
PhabricatorEditEngine $engine,
79
PhabricatorAuthFactorProvider $provider) {
80
return array();
81
}
82
83
public function newChallengeStatusView(
84
PhabricatorAuthFactorConfig $config,
85
PhabricatorAuthFactorProvider $provider,
86
PhabricatorUser $viewer,
87
PhabricatorAuthChallenge $challenge) {
88
return null;
89
}
90
91
/**
92
* Is this a factor which depends on the user's contact number?
93
*
94
* If a user has a "contact number" factor configured, they can not modify
95
* or switch their primary contact number.
96
*
97
* @return bool True if this factor should lock contact numbers.
98
*/
99
public function isContactNumberFactor() {
100
return false;
101
}
102
103
abstract public function getEnrollDescription(
104
PhabricatorAuthFactorProvider $provider,
105
PhabricatorUser $user);
106
107
public function getEnrollButtonText(
108
PhabricatorAuthFactorProvider $provider,
109
PhabricatorUser $user) {
110
return pht('Continue');
111
}
112
113
public function getFactorOrder() {
114
return 1000;
115
}
116
117
final public function newSortVector() {
118
return id(new PhutilSortVector())
119
->addInt($this->canCreateNewProvider() ? 0 : 1)
120
->addInt($this->getFactorOrder())
121
->addString($this->getFactorName());
122
}
123
124
protected function newChallenge(
125
PhabricatorAuthFactorConfig $config,
126
PhabricatorUser $viewer) {
127
128
$engine = $config->getSessionEngine();
129
130
return PhabricatorAuthChallenge::initializeNewChallenge()
131
->setUserPHID($viewer->getPHID())
132
->setSessionPHID($viewer->getSession()->getPHID())
133
->setFactorPHID($config->getPHID())
134
->setIsNewChallenge(true)
135
->setWorkflowKey($engine->getWorkflowKey());
136
}
137
138
abstract public function getRequestHasChallengeResponse(
139
PhabricatorAuthFactorConfig $config,
140
AphrontRequest $response);
141
142
final public function getNewIssuedChallenges(
143
PhabricatorAuthFactorConfig $config,
144
PhabricatorUser $viewer,
145
array $challenges) {
146
assert_instances_of($challenges, 'PhabricatorAuthChallenge');
147
148
$now = PhabricatorTime::getNow();
149
150
// Factor implementations may need to perform writes in order to issue
151
// challenges, particularly push factors like SMS.
152
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
153
154
$new_challenges = $this->newIssuedChallenges(
155
$config,
156
$viewer,
157
$challenges);
158
159
if ($this->isAuthResult($new_challenges)) {
160
unset($unguarded);
161
return $new_challenges;
162
}
163
164
assert_instances_of($new_challenges, 'PhabricatorAuthChallenge');
165
166
foreach ($new_challenges as $new_challenge) {
167
$ttl = $new_challenge->getChallengeTTL();
168
if (!$ttl) {
169
throw new Exception(
170
pht('Newly issued MFA challenges must have a valid TTL!'));
171
}
172
173
if ($ttl < $now) {
174
throw new Exception(
175
pht(
176
'Newly issued MFA challenges must have a future TTL. This '.
177
'factor issued a bad TTL ("%s"). (Did you use a relative '.
178
'time instead of an epoch?)',
179
$ttl));
180
}
181
}
182
183
foreach ($new_challenges as $challenge) {
184
$challenge->save();
185
}
186
187
unset($unguarded);
188
189
return $new_challenges;
190
}
191
192
abstract protected function newIssuedChallenges(
193
PhabricatorAuthFactorConfig $config,
194
PhabricatorUser $viewer,
195
array $challenges);
196
197
final public function getResultFromIssuedChallenges(
198
PhabricatorAuthFactorConfig $config,
199
PhabricatorUser $viewer,
200
array $challenges) {
201
assert_instances_of($challenges, 'PhabricatorAuthChallenge');
202
203
$result = $this->newResultFromIssuedChallenges(
204
$config,
205
$viewer,
206
$challenges);
207
208
if ($result === null) {
209
return $result;
210
}
211
212
if (!$this->isAuthResult($result)) {
213
throw new Exception(
214
pht(
215
'Expected "newResultFromIssuedChallenges()" to return null or '.
216
'an object of class "%s"; got something else (in "%s").',
217
'PhabricatorAuthFactorResult',
218
get_class($this)));
219
}
220
221
return $result;
222
}
223
224
final public function getResultForPrompt(
225
PhabricatorAuthFactorConfig $config,
226
PhabricatorUser $viewer,
227
AphrontRequest $request,
228
array $challenges) {
229
assert_instances_of($challenges, 'PhabricatorAuthChallenge');
230
231
$result = $this->newResultForPrompt(
232
$config,
233
$viewer,
234
$request,
235
$challenges);
236
237
if (!$this->isAuthResult($result)) {
238
throw new Exception(
239
pht(
240
'Expected "newResultForPrompt()" to return an object of class "%s", '.
241
'but it returned something else ("%s"; in "%s").',
242
'PhabricatorAuthFactorResult',
243
phutil_describe_type($result),
244
get_class($this)));
245
}
246
247
return $result;
248
}
249
250
protected function newResultForPrompt(
251
PhabricatorAuthFactorConfig $config,
252
PhabricatorUser $viewer,
253
AphrontRequest $request,
254
array $challenges) {
255
return $this->newResult();
256
}
257
258
abstract protected function newResultFromIssuedChallenges(
259
PhabricatorAuthFactorConfig $config,
260
PhabricatorUser $viewer,
261
array $challenges);
262
263
final public function getResultFromChallengeResponse(
264
PhabricatorAuthFactorConfig $config,
265
PhabricatorUser $viewer,
266
AphrontRequest $request,
267
array $challenges) {
268
assert_instances_of($challenges, 'PhabricatorAuthChallenge');
269
270
$result = $this->newResultFromChallengeResponse(
271
$config,
272
$viewer,
273
$request,
274
$challenges);
275
276
if (!$this->isAuthResult($result)) {
277
throw new Exception(
278
pht(
279
'Expected "newResultFromChallengeResponse()" to return an object '.
280
'of class "%s"; got something else (in "%s").',
281
'PhabricatorAuthFactorResult',
282
get_class($this)));
283
}
284
285
return $result;
286
}
287
288
abstract protected function newResultFromChallengeResponse(
289
PhabricatorAuthFactorConfig $config,
290
PhabricatorUser $viewer,
291
AphrontRequest $request,
292
array $challenges);
293
294
final protected function newAutomaticControl(
295
PhabricatorAuthFactorResult $result) {
296
297
$is_error = $result->getIsError();
298
if ($is_error) {
299
return $this->newErrorControl($result);
300
}
301
302
$is_continue = $result->getIsContinue();
303
if ($is_continue) {
304
return $this->newContinueControl($result);
305
}
306
307
$is_answered = (bool)$result->getAnsweredChallenge();
308
if ($is_answered) {
309
return $this->newAnsweredControl($result);
310
}
311
312
$is_wait = $result->getIsWait();
313
if ($is_wait) {
314
return $this->newWaitControl($result);
315
}
316
317
return null;
318
}
319
320
private function newWaitControl(
321
PhabricatorAuthFactorResult $result) {
322
323
$error = $result->getErrorMessage();
324
325
$icon = $result->getIcon();
326
if (!$icon) {
327
$icon = id(new PHUIIconView())
328
->setIcon('fa-clock-o', 'red');
329
}
330
331
return id(new PHUIFormTimerControl())
332
->setIcon($icon)
333
->appendChild($error)
334
->setError(pht('Wait'));
335
}
336
337
private function newAnsweredControl(
338
PhabricatorAuthFactorResult $result) {
339
340
$icon = $result->getIcon();
341
if (!$icon) {
342
$icon = id(new PHUIIconView())
343
->setIcon('fa-check-circle-o', 'green');
344
}
345
346
return id(new PHUIFormTimerControl())
347
->setIcon($icon)
348
->appendChild(
349
pht('You responded to this challenge correctly.'));
350
}
351
352
private function newErrorControl(
353
PhabricatorAuthFactorResult $result) {
354
355
$error = $result->getErrorMessage();
356
357
$icon = $result->getIcon();
358
if (!$icon) {
359
$icon = id(new PHUIIconView())
360
->setIcon('fa-times', 'red');
361
}
362
363
return id(new PHUIFormTimerControl())
364
->setIcon($icon)
365
->appendChild($error)
366
->setError(pht('Error'));
367
}
368
369
private function newContinueControl(
370
PhabricatorAuthFactorResult $result) {
371
372
$error = $result->getErrorMessage();
373
374
$icon = $result->getIcon();
375
if (!$icon) {
376
$icon = id(new PHUIIconView())
377
->setIcon('fa-commenting', 'green');
378
}
379
380
$control = id(new PHUIFormTimerControl())
381
->setIcon($icon)
382
->appendChild($error);
383
384
$status_challenge = $result->getStatusChallenge();
385
if ($status_challenge) {
386
$id = $status_challenge->getID();
387
$uri = "/auth/mfa/challenge/status/{$id}/";
388
$control->setUpdateURI($uri);
389
}
390
391
return $control;
392
}
393
394
395
396
/* -( Synchronizing New Factors )------------------------------------------ */
397
398
399
final protected function loadMFASyncToken(
400
PhabricatorAuthFactorProvider $provider,
401
AphrontRequest $request,
402
AphrontFormView $form,
403
PhabricatorUser $user) {
404
405
// If the form included a synchronization key, load the corresponding
406
// token. The user must synchronize to a key we generated because this
407
// raises the barrier to theoretical attacks where an attacker might
408
// provide a known key for factors like TOTP.
409
410
// (We store and verify the hash of the key, not the key itself, to limit
411
// how useful the data in the table is to an attacker.)
412
413
$sync_type = PhabricatorAuthMFASyncTemporaryTokenType::TOKENTYPE;
414
$sync_token = null;
415
416
$sync_key = $request->getStr($this->getMFASyncTokenFormKey());
417
if (phutil_nonempty_string($sync_key)) {
418
$sync_key_digest = PhabricatorHash::digestWithNamedKey(
419
$sync_key,
420
PhabricatorAuthMFASyncTemporaryTokenType::DIGEST_KEY);
421
422
$sync_token = id(new PhabricatorAuthTemporaryTokenQuery())
423
->setViewer($user)
424
->withTokenResources(array($user->getPHID()))
425
->withTokenTypes(array($sync_type))
426
->withExpired(false)
427
->withTokenCodes(array($sync_key_digest))
428
->executeOne();
429
}
430
431
if (!$sync_token) {
432
433
// Don't generate a new sync token if there are too many outstanding
434
// tokens already. This is mostly relevant for push factors like SMS,
435
// where generating a token has the side effect of sending a user a
436
// message.
437
438
$outstanding_limit = 10;
439
$outstanding_tokens = id(new PhabricatorAuthTemporaryTokenQuery())
440
->setViewer($user)
441
->withTokenResources(array($user->getPHID()))
442
->withTokenTypes(array($sync_type))
443
->withExpired(false)
444
->execute();
445
if (count($outstanding_tokens) > $outstanding_limit) {
446
throw new Exception(
447
pht(
448
'Your account has too many outstanding, incomplete MFA '.
449
'synchronization attempts. Wait an hour and try again.'));
450
}
451
452
$now = PhabricatorTime::getNow();
453
454
$sync_key = Filesystem::readRandomCharacters(32);
455
$sync_key_digest = PhabricatorHash::digestWithNamedKey(
456
$sync_key,
457
PhabricatorAuthMFASyncTemporaryTokenType::DIGEST_KEY);
458
$sync_ttl = $this->getMFASyncTokenTTL();
459
460
$sync_token = id(new PhabricatorAuthTemporaryToken())
461
->setIsNewTemporaryToken(true)
462
->setTokenResource($user->getPHID())
463
->setTokenType($sync_type)
464
->setTokenCode($sync_key_digest)
465
->setTokenExpires($now + $sync_ttl);
466
467
$properties = $this->newMFASyncTokenProperties(
468
$provider,
469
$user);
470
471
if ($this->isAuthResult($properties)) {
472
return $properties;
473
}
474
475
foreach ($properties as $key => $value) {
476
$sync_token->setTemporaryTokenProperty($key, $value);
477
}
478
479
$sync_token->save();
480
}
481
482
$form->addHiddenInput($this->getMFASyncTokenFormKey(), $sync_key);
483
484
return $sync_token;
485
}
486
487
protected function newMFASyncTokenProperties(
488
PhabricatorAuthFactorProvider $provider,
489
PhabricatorUser $user) {
490
return array();
491
}
492
493
private function getMFASyncTokenFormKey() {
494
return 'sync.key';
495
}
496
497
private function getMFASyncTokenTTL() {
498
return phutil_units('1 hour in seconds');
499
}
500
501
final protected function getChallengeForCurrentContext(
502
PhabricatorAuthFactorConfig $config,
503
PhabricatorUser $viewer,
504
array $challenges) {
505
506
$session_phid = $viewer->getSession()->getPHID();
507
$engine = $config->getSessionEngine();
508
$workflow_key = $engine->getWorkflowKey();
509
510
foreach ($challenges as $challenge) {
511
if ($challenge->getSessionPHID() !== $session_phid) {
512
continue;
513
}
514
515
if ($challenge->getWorkflowKey() !== $workflow_key) {
516
continue;
517
}
518
519
if ($challenge->getIsCompleted()) {
520
continue;
521
}
522
523
if ($challenge->getIsReusedChallenge()) {
524
continue;
525
}
526
527
return $challenge;
528
}
529
530
return null;
531
}
532
533
534
/**
535
* @phutil-external-symbol class QRcode
536
*/
537
final protected function newQRCode($uri) {
538
$root = dirname(phutil_get_library_root('phabricator'));
539
require_once $root.'/externals/phpqrcode/phpqrcode.php';
540
541
$lines = QRcode::text($uri);
542
543
$total_width = 240;
544
$cell_size = floor($total_width / count($lines));
545
546
$rows = array();
547
foreach ($lines as $line) {
548
$cells = array();
549
for ($ii = 0; $ii < strlen($line); $ii++) {
550
if ($line[$ii] == '1') {
551
$color = '#000';
552
} else {
553
$color = '#fff';
554
}
555
556
$cells[] = phutil_tag(
557
'td',
558
array(
559
'width' => $cell_size,
560
'height' => $cell_size,
561
'style' => 'background: '.$color,
562
),
563
'');
564
}
565
$rows[] = phutil_tag('tr', array(), $cells);
566
}
567
568
return phutil_tag(
569
'table',
570
array(
571
'style' => 'margin: 24px auto;',
572
),
573
$rows);
574
}
575
576
final protected function getInstallDisplayName() {
577
$uri = PhabricatorEnv::getURI('/');
578
$uri = new PhutilURI($uri);
579
return $uri->getDomain();
580
}
581
582
final protected function getChallengeResponseParameterName(
583
PhabricatorAuthFactorConfig $config) {
584
return $this->getParameterName($config, 'mfa.response');
585
}
586
587
final protected function getChallengeResponseFromRequest(
588
PhabricatorAuthFactorConfig $config,
589
AphrontRequest $request) {
590
591
$name = $this->getChallengeResponseParameterName($config);
592
593
$value = $request->getStr($name);
594
$value = (string)$value;
595
$value = trim($value);
596
597
return $value;
598
}
599
600
final protected function hasCSRF(PhabricatorAuthFactorConfig $config) {
601
$engine = $config->getSessionEngine();
602
$request = $engine->getRequest();
603
604
if (!$request->isHTTPPost()) {
605
return false;
606
}
607
608
return $request->validateCSRF();
609
}
610
611
final protected function loadConfigurationsForProvider(
612
PhabricatorAuthFactorProvider $provider,
613
PhabricatorUser $user) {
614
615
return id(new PhabricatorAuthFactorConfigQuery())
616
->setViewer($user)
617
->withUserPHIDs(array($user->getPHID()))
618
->withFactorProviderPHIDs(array($provider->getPHID()))
619
->execute();
620
}
621
622
final protected function isAuthResult($object) {
623
return ($object instanceof PhabricatorAuthFactorResult);
624
}
625
626
}
627
628