Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/auth/factor/PhabricatorDuoAuthFactor.php
12256 views
1
<?php
2
3
final class PhabricatorDuoAuthFactor
4
extends PhabricatorAuthFactor {
5
6
const PROP_CREDENTIAL = 'duo.credentialPHID';
7
const PROP_ENROLL = 'duo.enroll';
8
const PROP_USERNAMES = 'duo.usernames';
9
const PROP_HOSTNAME = 'duo.hostname';
10
11
public function getFactorKey() {
12
return 'duo';
13
}
14
15
public function getFactorName() {
16
return pht('Duo Security');
17
}
18
19
public function getFactorShortName() {
20
return pht('Duo');
21
}
22
23
public function getFactorCreateHelp() {
24
return pht('Support for Duo push authentication.');
25
}
26
27
public function getFactorDescription() {
28
return pht(
29
'When you need to authenticate, a request will be pushed to the '.
30
'Duo application on your phone.');
31
}
32
33
public function getEnrollDescription(
34
PhabricatorAuthFactorProvider $provider,
35
PhabricatorUser $user) {
36
return pht(
37
'To add a Duo factor, first download and install the Duo application '.
38
'on your phone. Once you have launched the application and are ready '.
39
'to perform setup, click continue.');
40
}
41
42
public function canCreateNewConfiguration(
43
PhabricatorAuthFactorProvider $provider,
44
PhabricatorUser $user) {
45
46
if ($this->loadConfigurationsForProvider($provider, $user)) {
47
return false;
48
}
49
50
return true;
51
}
52
53
public function getConfigurationCreateDescription(
54
PhabricatorAuthFactorProvider $provider,
55
PhabricatorUser $user) {
56
57
$messages = array();
58
59
if ($this->loadConfigurationsForProvider($provider, $user)) {
60
$messages[] = id(new PHUIInfoView())
61
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
62
->setErrors(
63
array(
64
pht(
65
'You already have Duo authentication attached to your account '.
66
'for this provider.'),
67
));
68
}
69
70
return $messages;
71
}
72
73
public function getConfigurationListDetails(
74
PhabricatorAuthFactorConfig $config,
75
PhabricatorAuthFactorProvider $provider,
76
PhabricatorUser $viewer) {
77
78
$duo_user = $config->getAuthFactorConfigProperty('duo.username');
79
80
return pht('Duo Username: %s', $duo_user);
81
}
82
83
84
public function newEditEngineFields(
85
PhabricatorEditEngine $engine,
86
PhabricatorAuthFactorProvider $provider) {
87
88
$viewer = $engine->getViewer();
89
90
$credential_phid = $provider->getAuthFactorProviderProperty(
91
self::PROP_CREDENTIAL);
92
93
$hostname = $provider->getAuthFactorProviderProperty(self::PROP_HOSTNAME);
94
$usernames = $provider->getAuthFactorProviderProperty(self::PROP_USERNAMES);
95
$enroll = $provider->getAuthFactorProviderProperty(self::PROP_ENROLL);
96
97
$credential_type = PassphrasePasswordCredentialType::CREDENTIAL_TYPE;
98
$provides_type = PassphrasePasswordCredentialType::PROVIDES_TYPE;
99
100
$credentials = id(new PassphraseCredentialQuery())
101
->setViewer($viewer)
102
->withIsDestroyed(false)
103
->withProvidesTypes(array($provides_type))
104
->execute();
105
106
$xaction_hostname =
107
PhabricatorAuthFactorProviderDuoHostnameTransaction::TRANSACTIONTYPE;
108
$xaction_credential =
109
PhabricatorAuthFactorProviderDuoCredentialTransaction::TRANSACTIONTYPE;
110
$xaction_usernames =
111
PhabricatorAuthFactorProviderDuoUsernamesTransaction::TRANSACTIONTYPE;
112
$xaction_enroll =
113
PhabricatorAuthFactorProviderDuoEnrollTransaction::TRANSACTIONTYPE;
114
115
return array(
116
id(new PhabricatorTextEditField())
117
->setLabel(pht('Duo API Hostname'))
118
->setKey('duo.hostname')
119
->setValue($hostname)
120
->setTransactionType($xaction_hostname)
121
->setIsRequired(true),
122
id(new PhabricatorCredentialEditField())
123
->setLabel(pht('Duo API Credential'))
124
->setKey('duo.credential')
125
->setValue($credential_phid)
126
->setTransactionType($xaction_credential)
127
->setCredentialType($credential_type)
128
->setCredentials($credentials),
129
id(new PhabricatorSelectEditField())
130
->setLabel(pht('Duo Username'))
131
->setKey('duo.usernames')
132
->setValue($usernames)
133
->setTransactionType($xaction_usernames)
134
->setOptions(
135
array(
136
'username' => pht(
137
'Use %s Username',
138
PlatformSymbols::getPlatformServerName()),
139
'email' => pht('Use Primary Email Address'),
140
)),
141
id(new PhabricatorSelectEditField())
142
->setLabel(pht('Create Accounts'))
143
->setKey('duo.enroll')
144
->setValue($enroll)
145
->setTransactionType($xaction_enroll)
146
->setOptions(
147
array(
148
'deny' => pht('Require Existing Duo Account'),
149
'allow' => pht('Create New Duo Account'),
150
)),
151
);
152
}
153
154
155
public function processAddFactorForm(
156
PhabricatorAuthFactorProvider $provider,
157
AphrontFormView $form,
158
AphrontRequest $request,
159
PhabricatorUser $user) {
160
161
$token = $this->loadMFASyncToken($provider, $request, $form, $user);
162
if ($this->isAuthResult($token)) {
163
$form->appendChild($this->newAutomaticControl($token));
164
return;
165
}
166
167
$enroll = $token->getTemporaryTokenProperty('duo.enroll');
168
$duo_id = $token->getTemporaryTokenProperty('duo.user-id');
169
$duo_uri = $token->getTemporaryTokenProperty('duo.uri');
170
$duo_user = $token->getTemporaryTokenProperty('duo.username');
171
172
$is_external = ($enroll === 'external');
173
$is_auto = ($enroll === 'auto');
174
$is_blocked = ($enroll === 'blocked');
175
176
if (!$token->getIsNewTemporaryToken()) {
177
if ($is_auto) {
178
return $this->newDuoConfig($user, $duo_user);
179
} else if ($is_external || $is_blocked) {
180
$parameters = array(
181
'username' => $duo_user,
182
);
183
184
$result = $this->newDuoFuture($provider)
185
->setMethod('preauth', $parameters)
186
->resolve();
187
188
$result_code = $result['response']['result'];
189
switch ($result_code) {
190
case 'auth':
191
case 'allow':
192
return $this->newDuoConfig($user, $duo_user);
193
case 'enroll':
194
if ($is_blocked) {
195
// We'll render an equivalent static control below, so skip
196
// rendering here. We explicitly don't want to give the user
197
// an enroll workflow.
198
break;
199
}
200
201
$duo_uri = $result['response']['enroll_portal_url'];
202
203
$waiting_icon = id(new PHUIIconView())
204
->setIcon('fa-mobile', 'red');
205
206
$waiting_control = id(new PHUIFormTimerControl())
207
->setIcon($waiting_icon)
208
->setError(pht('Not Complete'))
209
->appendChild(
210
pht(
211
'You have not completed Duo enrollment yet. '.
212
'Complete enrollment, then click continue.'));
213
214
$form->appendControl($waiting_control);
215
break;
216
default:
217
case 'deny':
218
break;
219
}
220
} else {
221
$parameters = array(
222
'user_id' => $duo_id,
223
'activation_code' => $duo_uri,
224
);
225
226
$future = $this->newDuoFuture($provider)
227
->setMethod('enroll_status', $parameters);
228
229
$result = $future->resolve();
230
$response = $result['response'];
231
232
switch ($response) {
233
case 'success':
234
return $this->newDuoConfig($user, $duo_user);
235
case 'waiting':
236
$waiting_icon = id(new PHUIIconView())
237
->setIcon('fa-mobile', 'red');
238
239
$waiting_control = id(new PHUIFormTimerControl())
240
->setIcon($waiting_icon)
241
->setError(pht('Not Complete'))
242
->appendChild(
243
pht(
244
'You have not activated this enrollment in the Duo '.
245
'application on your phone yet. Complete activation, then '.
246
'click continue.'));
247
248
$form->appendControl($waiting_control);
249
break;
250
case 'invalid':
251
default:
252
throw new Exception(
253
pht(
254
'This Duo enrollment attempt is invalid or has '.
255
'expired ("%s"). Cancel the workflow and try again.',
256
$response));
257
}
258
}
259
}
260
261
if ($is_blocked) {
262
$blocked_icon = id(new PHUIIconView())
263
->setIcon('fa-times', 'red');
264
265
$blocked_control = id(new PHUIFormTimerControl())
266
->setIcon($blocked_icon)
267
->appendChild(
268
pht(
269
'Your Duo account ("%s") has not completed Duo enrollment. '.
270
'Check your email and complete enrollment to continue.',
271
phutil_tag('strong', array(), $duo_user)));
272
273
$form->appendControl($blocked_control);
274
} else if ($is_auto) {
275
$auto_icon = id(new PHUIIconView())
276
->setIcon('fa-check', 'green');
277
278
$auto_control = id(new PHUIFormTimerControl())
279
->setIcon($auto_icon)
280
->appendChild(
281
pht(
282
'Duo account ("%s") is fully enrolled.',
283
phutil_tag('strong', array(), $duo_user)));
284
285
$form->appendControl($auto_control);
286
} else {
287
$duo_button = phutil_tag(
288
'a',
289
array(
290
'href' => $duo_uri,
291
'class' => 'button button-grey',
292
'target' => ($is_external ? '_blank' : null),
293
),
294
pht('Enroll Duo Account: %s', $duo_user));
295
296
$duo_button = phutil_tag(
297
'div',
298
array(
299
'class' => 'mfa-form-enroll-button',
300
),
301
$duo_button);
302
303
if ($is_external) {
304
$form->appendRemarkupInstructions(
305
pht(
306
'Complete enrolling your phone with Duo:'));
307
308
$form->appendControl(
309
id(new AphrontFormMarkupControl())
310
->setValue($duo_button));
311
} else {
312
313
$form->appendRemarkupInstructions(
314
pht(
315
'Scan this QR code with the Duo application on your mobile '.
316
'phone:'));
317
318
319
$qr_code = $this->newQRCode($duo_uri);
320
$form->appendChild($qr_code);
321
322
$form->appendRemarkupInstructions(
323
pht(
324
'If you are currently using your phone to view this page, '.
325
'click this button to open the Duo application:'));
326
327
$form->appendControl(
328
id(new AphrontFormMarkupControl())
329
->setValue($duo_button));
330
}
331
332
$form->appendRemarkupInstructions(
333
pht(
334
'Once you have completed setup on your phone, click continue.'));
335
}
336
}
337
338
339
protected function newMFASyncTokenProperties(
340
PhabricatorAuthFactorProvider $provider,
341
PhabricatorUser $user) {
342
343
$duo_user = $this->getDuoUsername($provider, $user);
344
345
// Duo automatically normalizes usernames to lowercase. Just do that here
346
// so that our value agrees more closely with Duo.
347
$duo_user = phutil_utf8_strtolower($duo_user);
348
349
$parameters = array(
350
'username' => $duo_user,
351
);
352
353
$result = $this->newDuoFuture($provider)
354
->setMethod('preauth', $parameters)
355
->resolve();
356
357
$external_uri = null;
358
$result_code = $result['response']['result'];
359
$status_message = $result['response']['status_msg'];
360
switch ($result_code) {
361
case 'auth':
362
case 'allow':
363
// If the user already has a Duo account, they don't need to do
364
// anything.
365
return array(
366
'duo.enroll' => 'auto',
367
'duo.username' => $duo_user,
368
);
369
case 'enroll':
370
if (!$this->shouldAllowDuoEnrollment($provider)) {
371
return array(
372
'duo.enroll' => 'blocked',
373
'duo.username' => $duo_user,
374
);
375
}
376
377
$external_uri = $result['response']['enroll_portal_url'];
378
379
// Otherwise, enrollment is permitted so we're going to continue.
380
break;
381
default:
382
case 'deny':
383
return $this->newResult()
384
->setIsError(true)
385
->setErrorMessage(
386
pht(
387
'Your Duo account ("%s") is not permitted to access this '.
388
'system. Contact your Duo administrator for help. '.
389
'The Duo preauth API responded with status message ("%s"): %s',
390
$duo_user,
391
$result_code,
392
$status_message));
393
}
394
395
// Duo's "/enroll" API isn't repeatable for the same username. If we're
396
// the first call, great: we can do inline enrollment, which is way more
397
// user friendly. Otherwise, we have to send the user on an adventure.
398
399
$parameters = array(
400
'username' => $duo_user,
401
'valid_secs' => phutil_units('1 hour in seconds'),
402
);
403
404
try {
405
$result = $this->newDuoFuture($provider)
406
->setMethod('enroll', $parameters)
407
->resolve();
408
} catch (HTTPFutureHTTPResponseStatus $ex) {
409
return array(
410
'duo.enroll' => 'external',
411
'duo.username' => $duo_user,
412
'duo.uri' => $external_uri,
413
);
414
}
415
416
return array(
417
'duo.enroll' => 'inline',
418
'duo.uri' => $result['response']['activation_code'],
419
'duo.username' => $duo_user,
420
'duo.user-id' => $result['response']['user_id'],
421
);
422
}
423
424
protected function newIssuedChallenges(
425
PhabricatorAuthFactorConfig $config,
426
PhabricatorUser $viewer,
427
array $challenges) {
428
429
// If we already issued a valid challenge for this workflow and session,
430
// don't issue a new one.
431
432
$challenge = $this->getChallengeForCurrentContext(
433
$config,
434
$viewer,
435
$challenges);
436
if ($challenge) {
437
return array();
438
}
439
440
if (!$this->hasCSRF($config)) {
441
return $this->newResult()
442
->setIsContinue(true)
443
->setErrorMessage(
444
pht(
445
'An authorization request will be pushed to the Duo '.
446
'application on your phone.'));
447
}
448
449
$provider = $config->getFactorProvider();
450
451
// Otherwise, issue a new challenge.
452
$duo_user = (string)$config->getAuthFactorConfigProperty('duo.username');
453
454
$parameters = array(
455
'username' => $duo_user,
456
);
457
458
$response = $this->newDuoFuture($provider)
459
->setMethod('preauth', $parameters)
460
->resolve();
461
$response = $response['response'];
462
463
$next_step = $response['result'];
464
$status_message = $response['status_msg'];
465
switch ($next_step) {
466
case 'auth':
467
// We're good to go.
468
break;
469
case 'allow':
470
// Duo is telling us to bypass MFA. For now, refuse.
471
return $this->newResult()
472
->setIsError(true)
473
->setErrorMessage(
474
pht(
475
'Duo is not requiring a challenge, which defeats the '.
476
'purpose of MFA. Duo must be configured to challenge you.'));
477
case 'enroll':
478
return $this->newResult()
479
->setIsError(true)
480
->setErrorMessage(
481
pht(
482
'Your Duo account ("%s") requires enrollment. Contact your '.
483
'Duo administrator for help. Duo status message: %s',
484
$duo_user,
485
$status_message));
486
case 'deny':
487
default:
488
return $this->newResult()
489
->setIsError(true)
490
->setErrorMessage(
491
pht(
492
'Your Duo account ("%s") is not permitted to access this '.
493
'system. Contact your Duo administrator for help. The Duo '.
494
'preauth API responded with status message ("%s"): %s',
495
$duo_user,
496
$next_step,
497
$status_message));
498
}
499
500
$has_push = false;
501
$devices = $response['devices'];
502
foreach ($devices as $device) {
503
$capabilities = array_fuse($device['capabilities']);
504
if (isset($capabilities['push'])) {
505
$has_push = true;
506
break;
507
}
508
}
509
510
if (!$has_push) {
511
return $this->newResult()
512
->setIsError(true)
513
->setErrorMessage(
514
pht(
515
'This factor has been removed from your device, so this server '.
516
'can not send you a challenge. To continue, an administrator '.
517
'must strip this factor from your account.'));
518
}
519
520
$push_info = array(
521
pht('Domain') => $this->getInstallDisplayName(),
522
);
523
$push_info = phutil_build_http_querystring($push_info);
524
525
$parameters = array(
526
'username' => $duo_user,
527
'factor' => 'push',
528
'async' => '1',
529
530
// Duo allows us to specify a device, or to pass "auto" to have it pick
531
// the first one. For now, just let it pick.
532
'device' => 'auto',
533
534
// This is a hard-coded prefix for the word "... request" in the Duo UI,
535
// which defaults to "Login". We could pass richer information from
536
// workflows here, but it's not very flexible anyway.
537
'type' => 'Authentication',
538
539
'display_username' => $viewer->getUsername(),
540
'pushinfo' => $push_info,
541
);
542
543
$result = $this->newDuoFuture($provider)
544
->setMethod('auth', $parameters)
545
->resolve();
546
547
$duo_xaction = $result['response']['txid'];
548
549
// The Duo push timeout is 60 seconds. Set our challenge to expire slightly
550
// more quickly so that we'll re-issue a new challenge before Duo times out.
551
// This should keep users away from a dead-end where they can't respond to
552
// Duo but we won't issue a new challenge yet.
553
$ttl_seconds = 55;
554
555
return array(
556
$this->newChallenge($config, $viewer)
557
->setChallengeKey($duo_xaction)
558
->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds),
559
);
560
}
561
562
protected function newResultFromIssuedChallenges(
563
PhabricatorAuthFactorConfig $config,
564
PhabricatorUser $viewer,
565
array $challenges) {
566
567
$challenge = $this->getChallengeForCurrentContext(
568
$config,
569
$viewer,
570
$challenges);
571
572
if ($challenge->getIsAnsweredChallenge()) {
573
return $this->newResult()
574
->setAnsweredChallenge($challenge);
575
}
576
577
$provider = $config->getFactorProvider();
578
$duo_xaction = $challenge->getChallengeKey();
579
580
$parameters = array(
581
'txid' => $duo_xaction,
582
);
583
584
// This endpoint always long-polls, so use a timeout to force it to act
585
// more asynchronously.
586
try {
587
$result = $this->newDuoFuture($provider)
588
->setHTTPMethod('GET')
589
->setMethod('auth_status', $parameters)
590
->setTimeout(3)
591
->resolve();
592
593
$state = $result['response']['result'];
594
$status = $result['response']['status'];
595
} catch (HTTPFutureCURLResponseStatus $exception) {
596
if ($exception->isTimeout()) {
597
$state = 'waiting';
598
$status = 'poll';
599
} else {
600
throw $exception;
601
}
602
}
603
604
$now = PhabricatorTime::getNow();
605
606
switch ($state) {
607
case 'allow':
608
$ttl = PhabricatorTime::getNow()
609
+ phutil_units('15 minutes in seconds');
610
611
$challenge
612
->markChallengeAsAnswered($ttl);
613
614
return $this->newResult()
615
->setAnsweredChallenge($challenge);
616
case 'waiting':
617
// If we didn't just issue this challenge, give the user a stronger
618
// hint that they need to follow the instructions.
619
if (!$challenge->getIsNewChallenge()) {
620
return $this->newResult()
621
->setIsContinue(true)
622
->setIcon(
623
id(new PHUIIconView())
624
->setIcon('fa-exclamation-triangle', 'yellow'))
625
->setErrorMessage(
626
pht(
627
'You must approve the challenge which was sent to your '.
628
'phone. Open the Duo application and confirm the challenge, '.
629
'then continue.'));
630
}
631
632
// Otherwise, we'll construct a default message later on.
633
break;
634
default:
635
case 'deny':
636
if ($status === 'timeout') {
637
return $this->newResult()
638
->setIsError(true)
639
->setErrorMessage(
640
pht(
641
'This request has timed out because you took too long to '.
642
'respond.'));
643
} else {
644
$wait_duration = ($challenge->getChallengeTTL() - $now) + 1;
645
646
return $this->newResult()
647
->setIsWait(true)
648
->setErrorMessage(
649
pht(
650
'You denied this request. Wait %s second(s) to try again.',
651
new PhutilNumber($wait_duration)));
652
}
653
break;
654
}
655
656
return null;
657
}
658
659
public function renderValidateFactorForm(
660
PhabricatorAuthFactorConfig $config,
661
AphrontFormView $form,
662
PhabricatorUser $viewer,
663
PhabricatorAuthFactorResult $result) {
664
665
$control = $this->newAutomaticControl($result);
666
667
$control
668
->setLabel(pht('Duo'))
669
->setCaption(pht('Factor Name: %s', $config->getFactorName()));
670
671
$form->appendChild($control);
672
}
673
674
public function getRequestHasChallengeResponse(
675
PhabricatorAuthFactorConfig $config,
676
AphrontRequest $request) {
677
return false;
678
}
679
680
protected function newResultFromChallengeResponse(
681
PhabricatorAuthFactorConfig $config,
682
PhabricatorUser $viewer,
683
AphrontRequest $request,
684
array $challenges) {
685
686
return $this->getResultForPrompt(
687
$config,
688
$viewer,
689
$request,
690
$challenges);
691
}
692
693
protected function newResultForPrompt(
694
PhabricatorAuthFactorConfig $config,
695
PhabricatorUser $viewer,
696
AphrontRequest $request,
697
array $challenges) {
698
699
$result = $this->newResult()
700
->setIsContinue(true)
701
->setErrorMessage(
702
pht(
703
'A challenge has been sent to your phone. Open the Duo '.
704
'application and confirm the challenge, then continue.'));
705
706
$challenge = $this->getChallengeForCurrentContext(
707
$config,
708
$viewer,
709
$challenges);
710
if ($challenge) {
711
$result
712
->setStatusChallenge($challenge)
713
->setIcon(
714
id(new PHUIIconView())
715
->setIcon('fa-refresh', 'green ph-spin'));
716
}
717
718
return $result;
719
}
720
721
private function newDuoFuture(PhabricatorAuthFactorProvider $provider) {
722
$credential_phid = $provider->getAuthFactorProviderProperty(
723
self::PROP_CREDENTIAL);
724
725
$omnipotent = PhabricatorUser::getOmnipotentUser();
726
727
$credential = id(new PassphraseCredentialQuery())
728
->setViewer($omnipotent)
729
->withPHIDs(array($credential_phid))
730
->needSecrets(true)
731
->executeOne();
732
if (!$credential) {
733
throw new Exception(
734
pht(
735
'Unable to load Duo API credential ("%s").',
736
$credential_phid));
737
}
738
739
$duo_key = $credential->getUsername();
740
$duo_secret = $credential->getSecret();
741
if (!$duo_secret) {
742
throw new Exception(
743
pht(
744
'Duo API credential ("%s") has no secret key.',
745
$credential_phid));
746
}
747
748
$duo_host = $provider->getAuthFactorProviderProperty(
749
self::PROP_HOSTNAME);
750
self::requireDuoAPIHostname($duo_host);
751
752
return id(new PhabricatorDuoFuture())
753
->setIntegrationKey($duo_key)
754
->setSecretKey($duo_secret)
755
->setAPIHostname($duo_host)
756
->setTimeout(10)
757
->setHTTPMethod('POST');
758
}
759
760
private function getDuoUsername(
761
PhabricatorAuthFactorProvider $provider,
762
PhabricatorUser $user) {
763
764
$mode = $provider->getAuthFactorProviderProperty(self::PROP_USERNAMES);
765
switch ($mode) {
766
case 'username':
767
return $user->getUsername();
768
case 'email':
769
return $user->loadPrimaryEmailAddress();
770
default:
771
throw new Exception(
772
pht(
773
'Duo username pairing mode ("%s") is not supported.',
774
$mode));
775
}
776
}
777
778
private function shouldAllowDuoEnrollment(
779
PhabricatorAuthFactorProvider $provider) {
780
781
$mode = $provider->getAuthFactorProviderProperty(self::PROP_ENROLL);
782
switch ($mode) {
783
case 'deny':
784
return false;
785
case 'allow':
786
return true;
787
default:
788
throw new Exception(
789
pht(
790
'Duo enrollment mode ("%s") is not supported.',
791
$mode));
792
}
793
}
794
795
private function newDuoConfig(PhabricatorUser $user, $duo_user) {
796
$config_properties = array(
797
'duo.username' => $duo_user,
798
);
799
800
$config = $this->newConfigForUser($user)
801
->setFactorName(pht('Duo (%s)', $duo_user))
802
->setProperties($config_properties);
803
804
return $config;
805
}
806
807
public static function requireDuoAPIHostname($hostname) {
808
if (preg_match('/\.duosecurity\.com\z/', $hostname)) {
809
return;
810
}
811
812
throw new Exception(
813
pht(
814
'Duo API hostname ("%s") is invalid, hostname must be '.
815
'"*.duosecurity.com".',
816
$hostname));
817
}
818
819
public function newChallengeStatusView(
820
PhabricatorAuthFactorConfig $config,
821
PhabricatorAuthFactorProvider $provider,
822
PhabricatorUser $viewer,
823
PhabricatorAuthChallenge $challenge) {
824
825
$duo_xaction = $challenge->getChallengeKey();
826
827
$parameters = array(
828
'txid' => $duo_xaction,
829
);
830
831
$default_result = id(new PhabricatorAuthChallengeUpdate())
832
->setRetry(true);
833
834
try {
835
$result = $this->newDuoFuture($provider)
836
->setHTTPMethod('GET')
837
->setMethod('auth_status', $parameters)
838
->setTimeout(5)
839
->resolve();
840
841
$state = $result['response']['result'];
842
} catch (HTTPFutureCURLResponseStatus $exception) {
843
// If we failed or timed out, retry. Usually, this is a timeout.
844
return id(new PhabricatorAuthChallengeUpdate())
845
->setRetry(true);
846
}
847
848
// For now, don't update the view for anything but an "Allow". Updates
849
// here are just about providing more visual feedback for user convenience.
850
if ($state !== 'allow') {
851
return id(new PhabricatorAuthChallengeUpdate())
852
->setRetry(false);
853
}
854
855
$icon = id(new PHUIIconView())
856
->setIcon('fa-check-circle-o', 'green');
857
858
$view = id(new PHUIFormTimerControl())
859
->setIcon($icon)
860
->appendChild(pht('You responded to this challenge correctly.'))
861
->newTimerView();
862
863
return id(new PhabricatorAuthChallengeUpdate())
864
->setState('allow')
865
->setRetry(false)
866
->setMarkup($view);
867
}
868
869
}
870
871