@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 868 lines 26 kB view raw
1<?php 2 3final 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 } 654 655 return null; 656 } 657 658 public function renderValidateFactorForm( 659 PhabricatorAuthFactorConfig $config, 660 AphrontFormView $form, 661 PhabricatorUser $viewer, 662 PhabricatorAuthFactorResult $result) { 663 664 $control = $this->newAutomaticControl($result); 665 666 $control 667 ->setLabel(pht('Duo')) 668 ->setCaption(pht('Factor Name: %s', $config->getFactorName())); 669 670 $form->appendChild($control); 671 } 672 673 public function getRequestHasChallengeResponse( 674 PhabricatorAuthFactorConfig $config, 675 AphrontRequest $request) { 676 return false; 677 } 678 679 protected function newResultFromChallengeResponse( 680 PhabricatorAuthFactorConfig $config, 681 PhabricatorUser $viewer, 682 AphrontRequest $request, 683 array $challenges) { 684 685 return $this->getResultForPrompt( 686 $config, 687 $viewer, 688 $request, 689 $challenges); 690 } 691 692 protected function newResultForPrompt( 693 PhabricatorAuthFactorConfig $config, 694 PhabricatorUser $viewer, 695 AphrontRequest $request, 696 array $challenges) { 697 698 $result = $this->newResult() 699 ->setIsContinue(true) 700 ->setErrorMessage( 701 pht( 702 'A challenge has been sent to your phone. Open the Duo '. 703 'application and confirm the challenge, then continue.')); 704 705 $challenge = $this->getChallengeForCurrentContext( 706 $config, 707 $viewer, 708 $challenges); 709 if ($challenge) { 710 $result 711 ->setStatusChallenge($challenge) 712 ->setIcon( 713 id(new PHUIIconView()) 714 ->setIcon('fa-refresh', 'green ph-spin')); 715 } 716 717 return $result; 718 } 719 720 private function newDuoFuture(PhabricatorAuthFactorProvider $provider) { 721 $credential_phid = $provider->getAuthFactorProviderProperty( 722 self::PROP_CREDENTIAL); 723 724 $omnipotent = PhabricatorUser::getOmnipotentUser(); 725 726 $credential = id(new PassphraseCredentialQuery()) 727 ->setViewer($omnipotent) 728 ->withPHIDs(array($credential_phid)) 729 ->needSecrets(true) 730 ->executeOne(); 731 if (!$credential) { 732 throw new Exception( 733 pht( 734 'Unable to load Duo API credential ("%s").', 735 $credential_phid)); 736 } 737 738 $duo_key = $credential->getUsername(); 739 $duo_secret = $credential->getSecret(); 740 if (!$duo_secret) { 741 throw new Exception( 742 pht( 743 'Duo API credential ("%s") has no secret key.', 744 $credential_phid)); 745 } 746 747 $duo_host = $provider->getAuthFactorProviderProperty( 748 self::PROP_HOSTNAME); 749 self::requireDuoAPIHostname($duo_host); 750 751 return id(new PhabricatorDuoFuture()) 752 ->setIntegrationKey($duo_key) 753 ->setSecretKey($duo_secret) 754 ->setAPIHostname($duo_host) 755 ->setTimeout(10) 756 ->setHTTPMethod('POST'); 757 } 758 759 private function getDuoUsername( 760 PhabricatorAuthFactorProvider $provider, 761 PhabricatorUser $user) { 762 763 $mode = $provider->getAuthFactorProviderProperty(self::PROP_USERNAMES); 764 switch ($mode) { 765 case 'username': 766 return $user->getUsername(); 767 case 'email': 768 return $user->loadPrimaryEmailAddress(); 769 default: 770 throw new Exception( 771 pht( 772 'Duo username pairing mode ("%s") is not supported.', 773 $mode)); 774 } 775 } 776 777 private function shouldAllowDuoEnrollment( 778 PhabricatorAuthFactorProvider $provider) { 779 780 $mode = $provider->getAuthFactorProviderProperty(self::PROP_ENROLL); 781 switch ($mode) { 782 case 'deny': 783 return false; 784 case 'allow': 785 return true; 786 default: 787 throw new Exception( 788 pht( 789 'Duo enrollment mode ("%s") is not supported.', 790 $mode)); 791 } 792 } 793 794 private function newDuoConfig(PhabricatorUser $user, $duo_user) { 795 $config_properties = array( 796 'duo.username' => $duo_user, 797 ); 798 799 $config = $this->newConfigForUser($user) 800 ->setFactorName(pht('Duo (%s)', $duo_user)) 801 ->setProperties($config_properties); 802 803 return $config; 804 } 805 806 public static function requireDuoAPIHostname($hostname) { 807 if (preg_match('/\.duosecurity\.com\z/', $hostname)) { 808 return; 809 } 810 811 throw new Exception( 812 pht( 813 'Duo API hostname ("%s") is invalid, hostname must be '. 814 '"*.duosecurity.com".', 815 $hostname)); 816 } 817 818 public function newChallengeStatusView( 819 PhabricatorAuthFactorConfig $config, 820 PhabricatorAuthFactorProvider $provider, 821 PhabricatorUser $viewer, 822 PhabricatorAuthChallenge $challenge) { 823 824 $duo_xaction = $challenge->getChallengeKey(); 825 826 $parameters = array( 827 'txid' => $duo_xaction, 828 ); 829 830 $default_result = id(new PhabricatorAuthChallengeUpdate()) 831 ->setRetry(true); 832 833 try { 834 $result = $this->newDuoFuture($provider) 835 ->setHTTPMethod('GET') 836 ->setMethod('auth_status', $parameters) 837 ->setTimeout(5) 838 ->resolve(); 839 840 $state = $result['response']['result']; 841 } catch (HTTPFutureCURLResponseStatus $exception) { 842 // If we failed or timed out, retry. Usually, this is a timeout. 843 return id(new PhabricatorAuthChallengeUpdate()) 844 ->setRetry(true); 845 } 846 847 // For now, don't update the view for anything but an "Allow". Updates 848 // here are just about providing more visual feedback for user convenience. 849 if ($state !== 'allow') { 850 return id(new PhabricatorAuthChallengeUpdate()) 851 ->setRetry(false); 852 } 853 854 $icon = id(new PHUIIconView()) 855 ->setIcon('fa-check-circle-o', 'green'); 856 857 $view = id(new PHUIFormTimerControl()) 858 ->setIcon($icon) 859 ->appendChild(pht('You responded to this challenge correctly.')) 860 ->newTimerView(); 861 862 return id(new PhabricatorAuthChallengeUpdate()) 863 ->setState('allow') 864 ->setRetry(false) 865 ->setMarkup($view); 866 } 867 868}