Remove all "FileHasObject" edge reads and writes
[phabricator.git] / src / applications / auth / factor / PhabricatorDuoAuthFactor.php
blob65f0aa5e4bf8cdfc4d7c3e10662d787833d964a9
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(
137 'Use %s Username',
138 PlatformSymbols::getPlatformServerName()),
139 'email' => pht('Use Primary Email Address'),
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'),
155 public function processAddFactorForm(
156 PhabricatorAuthFactorProvider $provider,
157 AphrontFormView $form,
158 AphrontRequest $request,
159 PhabricatorUser $user) {
161 $token = $this->loadMFASyncToken($provider, $request, $form, $user);
162 if ($this->isAuthResult($token)) {
163 $form->appendChild($this->newAutomaticControl($token));
164 return;
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');
172 $is_external = ($enroll === 'external');
173 $is_auto = ($enroll === 'auto');
174 $is_blocked = ($enroll === 'blocked');
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,
184 $result = $this->newDuoFuture($provider)
185 ->setMethod('preauth', $parameters)
186 ->resolve();
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;
201 $duo_uri = $result['response']['enroll_portal_url'];
203 $waiting_icon = id(new PHUIIconView())
204 ->setIcon('fa-mobile', 'red');
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.'));
214 $form->appendControl($waiting_control);
215 break;
216 default:
217 case 'deny':
218 break;
220 } else {
221 $parameters = array(
222 'user_id' => $duo_id,
223 'activation_code' => $duo_uri,
226 $future = $this->newDuoFuture($provider)
227 ->setMethod('enroll_status', $parameters);
229 $result = $future->resolve();
230 $response = $result['response'];
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');
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.'));
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));
261 if ($is_blocked) {
262 $blocked_icon = id(new PHUIIconView())
263 ->setIcon('fa-times', 'red');
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)));
273 $form->appendControl($blocked_control);
274 } else if ($is_auto) {
275 $auto_icon = id(new PHUIIconView())
276 ->setIcon('fa-check', 'green');
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)));
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),
294 pht('Enroll Duo Account: %s', $duo_user));
296 $duo_button = phutil_tag(
297 'div',
298 array(
299 'class' => 'mfa-form-enroll-button',
301 $duo_button);
303 if ($is_external) {
304 $form->appendRemarkupInstructions(
305 pht(
306 'Complete enrolling your phone with Duo:'));
308 $form->appendControl(
309 id(new AphrontFormMarkupControl())
310 ->setValue($duo_button));
311 } else {
313 $form->appendRemarkupInstructions(
314 pht(
315 'Scan this QR code with the Duo application on your mobile '.
316 'phone:'));
319 $qr_code = $this->newQRCode($duo_uri);
320 $form->appendChild($qr_code);
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:'));
327 $form->appendControl(
328 id(new AphrontFormMarkupControl())
329 ->setValue($duo_button));
332 $form->appendRemarkupInstructions(
333 pht(
334 'Once you have completed setup on your phone, click continue.'));
339 protected function newMFASyncTokenProperties(
340 PhabricatorAuthFactorProvider $provider,
341 PhabricatorUser $user) {
343 $duo_user = $this->getDuoUsername($provider, $user);
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);
349 $parameters = array(
350 'username' => $duo_user,
353 $result = $this->newDuoFuture($provider)
354 ->setMethod('preauth', $parameters)
355 ->resolve();
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,
369 case 'enroll':
370 if (!$this->shouldAllowDuoEnrollment($provider)) {
371 return array(
372 'duo.enroll' => 'blocked',
373 'duo.username' => $duo_user,
377 $external_uri = $result['response']['enroll_portal_url'];
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));
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.
399 $parameters = array(
400 'username' => $duo_user,
401 'valid_secs' => phutil_units('1 hour in seconds'),
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,
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'],
424 protected function newIssuedChallenges(
425 PhabricatorAuthFactorConfig $config,
426 PhabricatorUser $viewer,
427 array $challenges) {
429 // If we already issued a valid challenge for this workflow and session,
430 // don't issue a new one.
432 $challenge = $this->getChallengeForCurrentContext(
433 $config,
434 $viewer,
435 $challenges);
436 if ($challenge) {
437 return array();
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.'));
449 $provider = $config->getFactorProvider();
451 // Otherwise, issue a new challenge.
452 $duo_user = (string)$config->getAuthFactorConfigProperty('duo.username');
454 $parameters = array(
455 'username' => $duo_user,
458 $response = $this->newDuoFuture($provider)
459 ->setMethod('preauth', $parameters)
460 ->resolve();
461 $response = $response['response'];
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));
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;
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.'));
520 $push_info = array(
521 pht('Domain') => $this->getInstallDisplayName(),
523 $push_info = phutil_build_http_querystring($push_info);
525 $parameters = array(
526 'username' => $duo_user,
527 'factor' => 'push',
528 'async' => '1',
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',
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',
539 'display_username' => $viewer->getUsername(),
540 'pushinfo' => $push_info,
543 $result = $this->newDuoFuture($provider)
544 ->setMethod('auth', $parameters)
545 ->resolve();
547 $duo_xaction = $result['response']['txid'];
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;
555 return array(
556 $this->newChallenge($config, $viewer)
557 ->setChallengeKey($duo_xaction)
558 ->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds),
562 protected function newResultFromIssuedChallenges(
563 PhabricatorAuthFactorConfig $config,
564 PhabricatorUser $viewer,
565 array $challenges) {
567 $challenge = $this->getChallengeForCurrentContext(
568 $config,
569 $viewer,
570 $challenges);
572 if ($challenge->getIsAnsweredChallenge()) {
573 return $this->newResult()
574 ->setAnsweredChallenge($challenge);
577 $provider = $config->getFactorProvider();
578 $duo_xaction = $challenge->getChallengeKey();
580 $parameters = array(
581 'txid' => $duo_xaction,
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();
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;
604 $now = PhabricatorTime::getNow();
606 switch ($state) {
607 case 'allow':
608 $ttl = PhabricatorTime::getNow()
609 + phutil_units('15 minutes in seconds');
611 $challenge
612 ->markChallengeAsAnswered($ttl);
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.'));
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;
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)));
653 break;
656 return null;
659 public function renderValidateFactorForm(
660 PhabricatorAuthFactorConfig $config,
661 AphrontFormView $form,
662 PhabricatorUser $viewer,
663 PhabricatorAuthFactorResult $result) {
665 $control = $this->newAutomaticControl($result);
667 $control
668 ->setLabel(pht('Duo'))
669 ->setCaption(pht('Factor Name: %s', $config->getFactorName()));
671 $form->appendChild($control);
674 public function getRequestHasChallengeResponse(
675 PhabricatorAuthFactorConfig $config,
676 AphrontRequest $request) {
677 return false;
680 protected function newResultFromChallengeResponse(
681 PhabricatorAuthFactorConfig $config,
682 PhabricatorUser $viewer,
683 AphrontRequest $request,
684 array $challenges) {
686 return $this->getResultForPrompt(
687 $config,
688 $viewer,
689 $request,
690 $challenges);
693 protected function newResultForPrompt(
694 PhabricatorAuthFactorConfig $config,
695 PhabricatorUser $viewer,
696 AphrontRequest $request,
697 array $challenges) {
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.'));
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'));
718 return $result;
721 private function newDuoFuture(PhabricatorAuthFactorProvider $provider) {
722 $credential_phid = $provider->getAuthFactorProviderProperty(
723 self::PROP_CREDENTIAL);
725 $omnipotent = PhabricatorUser::getOmnipotentUser();
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));
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));
748 $duo_host = $provider->getAuthFactorProviderProperty(
749 self::PROP_HOSTNAME);
750 self::requireDuoAPIHostname($duo_host);
752 return id(new PhabricatorDuoFuture())
753 ->setIntegrationKey($duo_key)
754 ->setSecretKey($duo_secret)
755 ->setAPIHostname($duo_host)
756 ->setTimeout(10)
757 ->setHTTPMethod('POST');
760 private function getDuoUsername(
761 PhabricatorAuthFactorProvider $provider,
762 PhabricatorUser $user) {
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));
778 private function shouldAllowDuoEnrollment(
779 PhabricatorAuthFactorProvider $provider) {
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));
795 private function newDuoConfig(PhabricatorUser $user, $duo_user) {
796 $config_properties = array(
797 'duo.username' => $duo_user,
800 $config = $this->newConfigForUser($user)
801 ->setFactorName(pht('Duo (%s)', $duo_user))
802 ->setProperties($config_properties);
804 return $config;
807 public static function requireDuoAPIHostname($hostname) {
808 if (preg_match('/\.duosecurity\.com\z/', $hostname)) {
809 return;
812 throw new Exception(
813 pht(
814 'Duo API hostname ("%s") is invalid, hostname must be '.
815 '"*.duosecurity.com".',
816 $hostname));
819 public function newChallengeStatusView(
820 PhabricatorAuthFactorConfig $config,
821 PhabricatorAuthFactorProvider $provider,
822 PhabricatorUser $viewer,
823 PhabricatorAuthChallenge $challenge) {
825 $duo_xaction = $challenge->getChallengeKey();
827 $parameters = array(
828 'txid' => $duo_xaction,
831 $default_result = id(new PhabricatorAuthChallengeUpdate())
832 ->setRetry(true);
834 try {
835 $result = $this->newDuoFuture($provider)
836 ->setHTTPMethod('GET')
837 ->setMethod('auth_status', $parameters)
838 ->setTimeout(5)
839 ->resolve();
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);
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);
855 $icon = id(new PHUIIconView())
856 ->setIcon('fa-check-circle-o', 'green');
858 $view = id(new PHUIFormTimerControl())
859 ->setIcon($icon)
860 ->appendChild(pht('You responded to this challenge correctly.'))
861 ->newTimerView();
863 return id(new PhabricatorAuthChallengeUpdate())
864 ->setState('allow')
865 ->setRetry(false)
866 ->setMarkup($view);