Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / auth / factor / PhabricatorDuoAuthFactor.php
bloba84337a764aa265c0fdb1308ff0e5fcb2b9a9aed
1 <?php
3 final class PhabricatorDuoAuthFactor
4 extends PhabricatorAuthFactor {
6 const PROP_CREDENTIAL = 'duo.credentialPHID';
7 const PROP_ENROLL = 'duo.enroll';
8 const PROP_USERNAMES = 'duo.usernames';
9 const PROP_HOSTNAME = 'duo.hostname';
11 public function getFactorKey() {
12 return 'duo';
15 public function getFactorName() {
16 return pht('Duo Security');
19 public function getFactorShortName() {
20 return pht('Duo');
23 public function getFactorCreateHelp() {
24 return pht('Support for Duo push authentication.');
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.');
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.');
42 public function canCreateNewConfiguration(
43 PhabricatorAuthFactorProvider $provider,
44 PhabricatorUser $user) {
46 if ($this->loadConfigurationsForProvider($provider, $user)) {
47 return false;
50 return true;
53 public function getConfigurationCreateDescription(
54 PhabricatorAuthFactorProvider $provider,
55 PhabricatorUser $user) {
57 $messages = array();
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 ));
70 return $messages;
73 public function getConfigurationListDetails(
74 PhabricatorAuthFactorConfig $config,
75 PhabricatorAuthFactorProvider $provider,
76 PhabricatorUser $viewer) {
78 $duo_user = $config->getAuthFactorConfigProperty('duo.username');
80 return pht('Duo Username: %s', $duo_user);
84 public function newEditEngineFields(
85 PhabricatorEditEngine $engine,
86 PhabricatorAuthFactorProvider $provider) {
88 $viewer = $engine->getViewer();
90 $credential_phid = $provider->getAuthFactorProviderProperty(
91 self::PROP_CREDENTIAL);
93 $hostname = $provider->getAuthFactorProviderProperty(self::PROP_HOSTNAME);
94 $usernames = $provider->getAuthFactorProviderProperty(self::PROP_USERNAMES);
95 $enroll = $provider->getAuthFactorProviderProperty(self::PROP_ENROLL);
97 $credential_type = PassphrasePasswordCredentialType::CREDENTIAL_TYPE;
98 $provides_type = PassphrasePasswordCredentialType::PROVIDES_TYPE;
100 $credentials = id(new PassphraseCredentialQuery())
101 ->setViewer($viewer)
102 ->withIsDestroyed(false)
103 ->withProvidesTypes(array($provides_type))
104 ->execute();
106 $xaction_hostname =
107 PhabricatorAuthFactorProviderDuoHostnameTransaction::TRANSACTIONTYPE;
108 $xaction_credential =
109 PhabricatorAuthFactorProviderDuoCredentialTransaction::TRANSACTIONTYPE;
110 $xaction_usernames =
111 PhabricatorAuthFactorProviderDuoUsernamesTransaction::TRANSACTIONTYPE;
112 $xaction_enroll =
113 PhabricatorAuthFactorProviderDuoEnrollTransaction::TRANSACTIONTYPE;
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('Use Phabricator Username'),
137 'email' => pht('Use Primary Email Address'),
139 id(new PhabricatorSelectEditField())
140 ->setLabel(pht('Create Accounts'))
141 ->setKey('duo.enroll')
142 ->setValue($enroll)
143 ->setTransactionType($xaction_enroll)
144 ->setOptions(
145 array(
146 'deny' => pht('Require Existing Duo Account'),
147 'allow' => pht('Create New Duo Account'),
153 public function processAddFactorForm(
154 PhabricatorAuthFactorProvider $provider,
155 AphrontFormView $form,
156 AphrontRequest $request,
157 PhabricatorUser $user) {
159 $token = $this->loadMFASyncToken($provider, $request, $form, $user);
160 if ($this->isAuthResult($token)) {
161 $form->appendChild($this->newAutomaticControl($token));
162 return;
165 $enroll = $token->getTemporaryTokenProperty('duo.enroll');
166 $duo_id = $token->getTemporaryTokenProperty('duo.user-id');
167 $duo_uri = $token->getTemporaryTokenProperty('duo.uri');
168 $duo_user = $token->getTemporaryTokenProperty('duo.username');
170 $is_external = ($enroll === 'external');
171 $is_auto = ($enroll === 'auto');
172 $is_blocked = ($enroll === 'blocked');
174 if (!$token->getIsNewTemporaryToken()) {
175 if ($is_auto) {
176 return $this->newDuoConfig($user, $duo_user);
177 } else if ($is_external || $is_blocked) {
178 $parameters = array(
179 'username' => $duo_user,
182 $result = $this->newDuoFuture($provider)
183 ->setMethod('preauth', $parameters)
184 ->resolve();
186 $result_code = $result['response']['result'];
187 switch ($result_code) {
188 case 'auth':
189 case 'allow':
190 return $this->newDuoConfig($user, $duo_user);
191 case 'enroll':
192 if ($is_blocked) {
193 // We'll render an equivalent static control below, so skip
194 // rendering here. We explicitly don't want to give the user
195 // an enroll workflow.
196 break;
199 $duo_uri = $result['response']['enroll_portal_url'];
201 $waiting_icon = id(new PHUIIconView())
202 ->setIcon('fa-mobile', 'red');
204 $waiting_control = id(new PHUIFormTimerControl())
205 ->setIcon($waiting_icon)
206 ->setError(pht('Not Complete'))
207 ->appendChild(
208 pht(
209 'You have not completed Duo enrollment yet. '.
210 'Complete enrollment, then click continue.'));
212 $form->appendControl($waiting_control);
213 break;
214 default:
215 case 'deny':
216 break;
218 } else {
219 $parameters = array(
220 'user_id' => $duo_id,
221 'activation_code' => $duo_uri,
224 $future = $this->newDuoFuture($provider)
225 ->setMethod('enroll_status', $parameters);
227 $result = $future->resolve();
228 $response = $result['response'];
230 switch ($response) {
231 case 'success':
232 return $this->newDuoConfig($user, $duo_user);
233 case 'waiting':
234 $waiting_icon = id(new PHUIIconView())
235 ->setIcon('fa-mobile', 'red');
237 $waiting_control = id(new PHUIFormTimerControl())
238 ->setIcon($waiting_icon)
239 ->setError(pht('Not Complete'))
240 ->appendChild(
241 pht(
242 'You have not activated this enrollment in the Duo '.
243 'application on your phone yet. Complete activation, then '.
244 'click continue.'));
246 $form->appendControl($waiting_control);
247 break;
248 case 'invalid':
249 default:
250 throw new Exception(
251 pht(
252 'This Duo enrollment attempt is invalid or has '.
253 'expired ("%s"). Cancel the workflow and try again.',
254 $response));
259 if ($is_blocked) {
260 $blocked_icon = id(new PHUIIconView())
261 ->setIcon('fa-times', 'red');
263 $blocked_control = id(new PHUIFormTimerControl())
264 ->setIcon($blocked_icon)
265 ->appendChild(
266 pht(
267 'Your Duo account ("%s") has not completed Duo enrollment. '.
268 'Check your email and complete enrollment to continue.',
269 phutil_tag('strong', array(), $duo_user)));
271 $form->appendControl($blocked_control);
272 } else if ($is_auto) {
273 $auto_icon = id(new PHUIIconView())
274 ->setIcon('fa-check', 'green');
276 $auto_control = id(new PHUIFormTimerControl())
277 ->setIcon($auto_icon)
278 ->appendChild(
279 pht(
280 'Duo account ("%s") is fully enrolled.',
281 phutil_tag('strong', array(), $duo_user)));
283 $form->appendControl($auto_control);
284 } else {
285 $duo_button = phutil_tag(
286 'a',
287 array(
288 'href' => $duo_uri,
289 'class' => 'button button-grey',
290 'target' => ($is_external ? '_blank' : null),
292 pht('Enroll Duo Account: %s', $duo_user));
294 $duo_button = phutil_tag(
295 'div',
296 array(
297 'class' => 'mfa-form-enroll-button',
299 $duo_button);
301 if ($is_external) {
302 $form->appendRemarkupInstructions(
303 pht(
304 'Complete enrolling your phone with Duo:'));
306 $form->appendControl(
307 id(new AphrontFormMarkupControl())
308 ->setValue($duo_button));
309 } else {
311 $form->appendRemarkupInstructions(
312 pht(
313 'Scan this QR code with the Duo application on your mobile '.
314 'phone:'));
317 $qr_code = $this->newQRCode($duo_uri);
318 $form->appendChild($qr_code);
320 $form->appendRemarkupInstructions(
321 pht(
322 'If you are currently using your phone to view this page, '.
323 'click this button to open the Duo application:'));
325 $form->appendControl(
326 id(new AphrontFormMarkupControl())
327 ->setValue($duo_button));
330 $form->appendRemarkupInstructions(
331 pht(
332 'Once you have completed setup on your phone, click continue.'));
337 protected function newMFASyncTokenProperties(
338 PhabricatorAuthFactorProvider $provider,
339 PhabricatorUser $user) {
341 $duo_user = $this->getDuoUsername($provider, $user);
343 // Duo automatically normalizes usernames to lowercase. Just do that here
344 // so that our value agrees more closely with Duo.
345 $duo_user = phutil_utf8_strtolower($duo_user);
347 $parameters = array(
348 'username' => $duo_user,
351 $result = $this->newDuoFuture($provider)
352 ->setMethod('preauth', $parameters)
353 ->resolve();
355 $external_uri = null;
356 $result_code = $result['response']['result'];
357 $status_message = $result['response']['status_msg'];
358 switch ($result_code) {
359 case 'auth':
360 case 'allow':
361 // If the user already has a Duo account, they don't need to do
362 // anything.
363 return array(
364 'duo.enroll' => 'auto',
365 'duo.username' => $duo_user,
367 case 'enroll':
368 if (!$this->shouldAllowDuoEnrollment($provider)) {
369 return array(
370 'duo.enroll' => 'blocked',
371 'duo.username' => $duo_user,
375 $external_uri = $result['response']['enroll_portal_url'];
377 // Otherwise, enrollment is permitted so we're going to continue.
378 break;
379 default:
380 case 'deny':
381 return $this->newResult()
382 ->setIsError(true)
383 ->setErrorMessage(
384 pht(
385 'Your Duo account ("%s") is not permitted to access this '.
386 'system. Contact your Duo administrator for help. '.
387 'The Duo preauth API responded with status message ("%s"): %s',
388 $duo_user,
389 $result_code,
390 $status_message));
393 // Duo's "/enroll" API isn't repeatable for the same username. If we're
394 // the first call, great: we can do inline enrollment, which is way more
395 // user friendly. Otherwise, we have to send the user on an adventure.
397 $parameters = array(
398 'username' => $duo_user,
399 'valid_secs' => phutil_units('1 hour in seconds'),
402 try {
403 $result = $this->newDuoFuture($provider)
404 ->setMethod('enroll', $parameters)
405 ->resolve();
406 } catch (HTTPFutureHTTPResponseStatus $ex) {
407 return array(
408 'duo.enroll' => 'external',
409 'duo.username' => $duo_user,
410 'duo.uri' => $external_uri,
414 return array(
415 'duo.enroll' => 'inline',
416 'duo.uri' => $result['response']['activation_code'],
417 'duo.username' => $duo_user,
418 'duo.user-id' => $result['response']['user_id'],
422 protected function newIssuedChallenges(
423 PhabricatorAuthFactorConfig $config,
424 PhabricatorUser $viewer,
425 array $challenges) {
427 // If we already issued a valid challenge for this workflow and session,
428 // don't issue a new one.
430 $challenge = $this->getChallengeForCurrentContext(
431 $config,
432 $viewer,
433 $challenges);
434 if ($challenge) {
435 return array();
438 if (!$this->hasCSRF($config)) {
439 return $this->newResult()
440 ->setIsContinue(true)
441 ->setErrorMessage(
442 pht(
443 'An authorization request will be pushed to the Duo '.
444 'application on your phone.'));
447 $provider = $config->getFactorProvider();
449 // Otherwise, issue a new challenge.
450 $duo_user = (string)$config->getAuthFactorConfigProperty('duo.username');
452 $parameters = array(
453 'username' => $duo_user,
456 $response = $this->newDuoFuture($provider)
457 ->setMethod('preauth', $parameters)
458 ->resolve();
459 $response = $response['response'];
461 $next_step = $response['result'];
462 $status_message = $response['status_msg'];
463 switch ($next_step) {
464 case 'auth':
465 // We're good to go.
466 break;
467 case 'allow':
468 // Duo is telling us to bypass MFA. For now, refuse.
469 return $this->newResult()
470 ->setIsError(true)
471 ->setErrorMessage(
472 pht(
473 'Duo is not requiring a challenge, which defeats the '.
474 'purpose of MFA. Duo must be configured to challenge you.'));
475 case 'enroll':
476 return $this->newResult()
477 ->setIsError(true)
478 ->setErrorMessage(
479 pht(
480 'Your Duo account ("%s") requires enrollment. Contact your '.
481 'Duo administrator for help. Duo status message: %s',
482 $duo_user,
483 $status_message));
484 case 'deny':
485 default:
486 return $this->newResult()
487 ->setIsError(true)
488 ->setErrorMessage(
489 pht(
490 'Your Duo account ("%s") is not permitted to access this '.
491 'system. Contact your Duo administrator for help. The Duo '.
492 'preauth API responded with status message ("%s"): %s',
493 $duo_user,
494 $next_step,
495 $status_message));
498 $has_push = false;
499 $devices = $response['devices'];
500 foreach ($devices as $device) {
501 $capabilities = array_fuse($device['capabilities']);
502 if (isset($capabilities['push'])) {
503 $has_push = true;
504 break;
508 if (!$has_push) {
509 return $this->newResult()
510 ->setIsError(true)
511 ->setErrorMessage(
512 pht(
513 'This factor has been removed from your device, so Phabricator '.
514 'can not send you a challenge. To continue, an administrator '.
515 'must strip this factor from your account.'));
518 $push_info = array(
519 pht('Domain') => $this->getInstallDisplayName(),
521 $push_info = phutil_build_http_querystring($push_info);
523 $parameters = array(
524 'username' => $duo_user,
525 'factor' => 'push',
526 'async' => '1',
528 // Duo allows us to specify a device, or to pass "auto" to have it pick
529 // the first one. For now, just let it pick.
530 'device' => 'auto',
532 // This is a hard-coded prefix for the word "... request" in the Duo UI,
533 // which defaults to "Login". We could pass richer information from
534 // workflows here, but it's not very flexible anyway.
535 'type' => 'Authentication',
537 'display_username' => $viewer->getUsername(),
538 'pushinfo' => $push_info,
541 $result = $this->newDuoFuture($provider)
542 ->setMethod('auth', $parameters)
543 ->resolve();
545 $duo_xaction = $result['response']['txid'];
547 // The Duo push timeout is 60 seconds. Set our challenge to expire slightly
548 // more quickly so that we'll re-issue a new challenge before Duo times out.
549 // This should keep users away from a dead-end where they can't respond to
550 // Duo but Phabricator won't issue a new challenge yet.
551 $ttl_seconds = 55;
553 return array(
554 $this->newChallenge($config, $viewer)
555 ->setChallengeKey($duo_xaction)
556 ->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds),
560 protected function newResultFromIssuedChallenges(
561 PhabricatorAuthFactorConfig $config,
562 PhabricatorUser $viewer,
563 array $challenges) {
565 $challenge = $this->getChallengeForCurrentContext(
566 $config,
567 $viewer,
568 $challenges);
570 if ($challenge->getIsAnsweredChallenge()) {
571 return $this->newResult()
572 ->setAnsweredChallenge($challenge);
575 $provider = $config->getFactorProvider();
576 $duo_xaction = $challenge->getChallengeKey();
578 $parameters = array(
579 'txid' => $duo_xaction,
582 // This endpoint always long-polls, so use a timeout to force it to act
583 // more asynchronously.
584 try {
585 $result = $this->newDuoFuture($provider)
586 ->setHTTPMethod('GET')
587 ->setMethod('auth_status', $parameters)
588 ->setTimeout(3)
589 ->resolve();
591 $state = $result['response']['result'];
592 $status = $result['response']['status'];
593 } catch (HTTPFutureCURLResponseStatus $exception) {
594 if ($exception->isTimeout()) {
595 $state = 'waiting';
596 $status = 'poll';
597 } else {
598 throw $exception;
602 $now = PhabricatorTime::getNow();
604 switch ($state) {
605 case 'allow':
606 $ttl = PhabricatorTime::getNow()
607 + phutil_units('15 minutes in seconds');
609 $challenge
610 ->markChallengeAsAnswered($ttl);
612 return $this->newResult()
613 ->setAnsweredChallenge($challenge);
614 case 'waiting':
615 // If we didn't just issue this challenge, give the user a stronger
616 // hint that they need to follow the instructions.
617 if (!$challenge->getIsNewChallenge()) {
618 return $this->newResult()
619 ->setIsContinue(true)
620 ->setIcon(
621 id(new PHUIIconView())
622 ->setIcon('fa-exclamation-triangle', 'yellow'))
623 ->setErrorMessage(
624 pht(
625 'You must approve the challenge which was sent to your '.
626 'phone. Open the Duo application and confirm the challenge, '.
627 'then continue.'));
630 // Otherwise, we'll construct a default message later on.
631 break;
632 default:
633 case 'deny':
634 if ($status === 'timeout') {
635 return $this->newResult()
636 ->setIsError(true)
637 ->setErrorMessage(
638 pht(
639 'This request has timed out because you took too long to '.
640 'respond.'));
641 } else {
642 $wait_duration = ($challenge->getChallengeTTL() - $now) + 1;
644 return $this->newResult()
645 ->setIsWait(true)
646 ->setErrorMessage(
647 pht(
648 'You denied this request. Wait %s second(s) to try again.',
649 new PhutilNumber($wait_duration)));
651 break;
654 return null;
657 public function renderValidateFactorForm(
658 PhabricatorAuthFactorConfig $config,
659 AphrontFormView $form,
660 PhabricatorUser $viewer,
661 PhabricatorAuthFactorResult $result) {
663 $control = $this->newAutomaticControl($result);
665 $control
666 ->setLabel(pht('Duo'))
667 ->setCaption(pht('Factor Name: %s', $config->getFactorName()));
669 $form->appendChild($control);
672 public function getRequestHasChallengeResponse(
673 PhabricatorAuthFactorConfig $config,
674 AphrontRequest $request) {
675 return false;
678 protected function newResultFromChallengeResponse(
679 PhabricatorAuthFactorConfig $config,
680 PhabricatorUser $viewer,
681 AphrontRequest $request,
682 array $challenges) {
684 return $this->getResultForPrompt(
685 $config,
686 $viewer,
687 $request,
688 $challenges);
691 protected function newResultForPrompt(
692 PhabricatorAuthFactorConfig $config,
693 PhabricatorUser $viewer,
694 AphrontRequest $request,
695 array $challenges) {
697 $result = $this->newResult()
698 ->setIsContinue(true)
699 ->setErrorMessage(
700 pht(
701 'A challenge has been sent to your phone. Open the Duo '.
702 'application and confirm the challenge, then continue.'));
704 $challenge = $this->getChallengeForCurrentContext(
705 $config,
706 $viewer,
707 $challenges);
708 if ($challenge) {
709 $result
710 ->setStatusChallenge($challenge)
711 ->setIcon(
712 id(new PHUIIconView())
713 ->setIcon('fa-refresh', 'green ph-spin'));
716 return $result;
719 private function newDuoFuture(PhabricatorAuthFactorProvider $provider) {
720 $credential_phid = $provider->getAuthFactorProviderProperty(
721 self::PROP_CREDENTIAL);
723 $omnipotent = PhabricatorUser::getOmnipotentUser();
725 $credential = id(new PassphraseCredentialQuery())
726 ->setViewer($omnipotent)
727 ->withPHIDs(array($credential_phid))
728 ->needSecrets(true)
729 ->executeOne();
730 if (!$credential) {
731 throw new Exception(
732 pht(
733 'Unable to load Duo API credential ("%s").',
734 $credential_phid));
737 $duo_key = $credential->getUsername();
738 $duo_secret = $credential->getSecret();
739 if (!$duo_secret) {
740 throw new Exception(
741 pht(
742 'Duo API credential ("%s") has no secret key.',
743 $credential_phid));
746 $duo_host = $provider->getAuthFactorProviderProperty(
747 self::PROP_HOSTNAME);
748 self::requireDuoAPIHostname($duo_host);
750 return id(new PhabricatorDuoFuture())
751 ->setIntegrationKey($duo_key)
752 ->setSecretKey($duo_secret)
753 ->setAPIHostname($duo_host)
754 ->setTimeout(10)
755 ->setHTTPMethod('POST');
758 private function getDuoUsername(
759 PhabricatorAuthFactorProvider $provider,
760 PhabricatorUser $user) {
762 $mode = $provider->getAuthFactorProviderProperty(self::PROP_USERNAMES);
763 switch ($mode) {
764 case 'username':
765 return $user->getUsername();
766 case 'email':
767 return $user->loadPrimaryEmailAddress();
768 default:
769 throw new Exception(
770 pht(
771 'Duo username pairing mode ("%s") is not supported.',
772 $mode));
776 private function shouldAllowDuoEnrollment(
777 PhabricatorAuthFactorProvider $provider) {
779 $mode = $provider->getAuthFactorProviderProperty(self::PROP_ENROLL);
780 switch ($mode) {
781 case 'deny':
782 return false;
783 case 'allow':
784 return true;
785 default:
786 throw new Exception(
787 pht(
788 'Duo enrollment mode ("%s") is not supported.',
789 $mode));
793 private function newDuoConfig(PhabricatorUser $user, $duo_user) {
794 $config_properties = array(
795 'duo.username' => $duo_user,
798 $config = $this->newConfigForUser($user)
799 ->setFactorName(pht('Duo (%s)', $duo_user))
800 ->setProperties($config_properties);
802 return $config;
805 public static function requireDuoAPIHostname($hostname) {
806 if (preg_match('/\.duosecurity\.com\z/', $hostname)) {
807 return;
810 throw new Exception(
811 pht(
812 'Duo API hostname ("%s") is invalid, hostname must be '.
813 '"*.duosecurity.com".',
814 $hostname));
817 public function newChallengeStatusView(
818 PhabricatorAuthFactorConfig $config,
819 PhabricatorAuthFactorProvider $provider,
820 PhabricatorUser $viewer,
821 PhabricatorAuthChallenge $challenge) {
823 $duo_xaction = $challenge->getChallengeKey();
825 $parameters = array(
826 'txid' => $duo_xaction,
829 $default_result = id(new PhabricatorAuthChallengeUpdate())
830 ->setRetry(true);
832 try {
833 $result = $this->newDuoFuture($provider)
834 ->setHTTPMethod('GET')
835 ->setMethod('auth_status', $parameters)
836 ->setTimeout(5)
837 ->resolve();
839 $state = $result['response']['result'];
840 } catch (HTTPFutureCURLResponseStatus $exception) {
841 // If we failed or timed out, retry. Usually, this is a timeout.
842 return id(new PhabricatorAuthChallengeUpdate())
843 ->setRetry(true);
846 // For now, don't update the view for anything but an "Allow". Updates
847 // here are just about providing more visual feedback for user convenience.
848 if ($state !== 'allow') {
849 return id(new PhabricatorAuthChallengeUpdate())
850 ->setRetry(false);
853 $icon = id(new PHUIIconView())
854 ->setIcon('fa-check-circle-o', 'green');
856 $view = id(new PHUIFormTimerControl())
857 ->setIcon($icon)
858 ->appendChild(pht('You responded to this challenge correctly.'))
859 ->newTimerView();
861 return id(new PhabricatorAuthChallengeUpdate())
862 ->setState('allow')
863 ->setRetry(false)
864 ->setMarkup($view);