@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.

When users confirm Duo MFA in the mobile app, live-update the UI

Summary: Ref T13249. Poll for Duo updates in the background so we can automatically update the UI when the user clicks the mobile phone app button.

Test Plan: Hit a Duo gate, clicked "Approve" in the mobile app, saw the UI update immediately.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13249

Differential Revision: https://secure.phabricator.com/D20169

+284 -30
+10 -3
resources/celerity/map.php
··· 9 9 'names' => array( 10 10 'conpherence.pkg.css' => '3c8a0668', 11 11 'conpherence.pkg.js' => '020aebcf', 12 - 'core.pkg.css' => '7a73ffc5', 12 + 'core.pkg.css' => 'e0f5d66f', 13 13 'core.pkg.js' => '5c737607', 14 14 'differential.pkg.css' => 'b8df73d4', 15 15 'differential.pkg.js' => '67c9ea4c', ··· 151 151 'rsrc/css/phui/phui-document.css' => '52b748a5', 152 152 'rsrc/css/phui/phui-feed-story.css' => 'a0c05029', 153 153 'rsrc/css/phui/phui-fontkit.css' => '9b714a5e', 154 - 'rsrc/css/phui/phui-form-view.css' => '0807e7ac', 154 + 'rsrc/css/phui/phui-form-view.css' => '01b796c0', 155 155 'rsrc/css/phui/phui-form.css' => '159e2d9c', 156 156 'rsrc/css/phui/phui-head-thing.css' => 'd7f293df', 157 157 'rsrc/css/phui/phui-header-view.css' => '93cea4ec', ··· 502 502 'rsrc/js/phui/behavior-phui-selectable-list.js' => 'b26a41e4', 503 503 'rsrc/js/phui/behavior-phui-submenu.js' => 'b5e9bff9', 504 504 'rsrc/js/phui/behavior-phui-tab-group.js' => '242aa08b', 505 + 'rsrc/js/phui/behavior-phui-timer-control.js' => 'f84bcbf4', 505 506 'rsrc/js/phuix/PHUIXActionListView.js' => 'c68f183f', 506 507 'rsrc/js/phuix/PHUIXActionView.js' => 'aaa08f3b', 507 508 'rsrc/js/phuix/PHUIXAutocomplete.js' => '58cc4ab8', ··· 650 651 'javelin-behavior-phui-selectable-list' => 'b26a41e4', 651 652 'javelin-behavior-phui-submenu' => 'b5e9bff9', 652 653 'javelin-behavior-phui-tab-group' => '242aa08b', 654 + 'javelin-behavior-phui-timer-control' => 'f84bcbf4', 653 655 'javelin-behavior-phuix-example' => 'c2c500a7', 654 656 'javelin-behavior-policy-control' => '0eaa33a9', 655 657 'javelin-behavior-policy-rule-editor' => '9347f172', ··· 817 819 'phui-font-icon-base-css' => 'd7994e06', 818 820 'phui-fontkit-css' => '9b714a5e', 819 821 'phui-form-css' => '159e2d9c', 820 - 'phui-form-view-css' => '0807e7ac', 822 + 'phui-form-view-css' => '01b796c0', 821 823 'phui-head-thing-view-css' => 'd7f293df', 822 824 'phui-header-view-css' => '93cea4ec', 823 825 'phui-hovercard' => '074f0783', ··· 2107 2109 'javelin-dom', 2108 2110 ), 2109 2111 'f5c78ae3' => array( 2112 + 'javelin-behavior', 2113 + 'javelin-stratcom', 2114 + 'javelin-dom', 2115 + ), 2116 + 'f84bcbf4' => array( 2110 2117 'javelin-behavior', 2111 2118 'javelin-stratcom', 2112 2119 'javelin-dom',
+4
src/__phutil_library_map__.php
··· 2195 2195 'PhabricatorAuthChallengeGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php', 2196 2196 'PhabricatorAuthChallengePHIDType' => 'applications/auth/phid/PhabricatorAuthChallengePHIDType.php', 2197 2197 'PhabricatorAuthChallengeQuery' => 'applications/auth/query/PhabricatorAuthChallengeQuery.php', 2198 + 'PhabricatorAuthChallengeStatusController' => 'applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php', 2199 + 'PhabricatorAuthChallengeUpdate' => 'applications/auth/view/PhabricatorAuthChallengeUpdate.php', 2198 2200 'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php', 2199 2201 'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php', 2200 2202 'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php', ··· 7925 7927 'PhabricatorAuthChallengeGarbageCollector' => 'PhabricatorGarbageCollector', 7926 7928 'PhabricatorAuthChallengePHIDType' => 'PhabricatorPHIDType', 7927 7929 'PhabricatorAuthChallengeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 7930 + 'PhabricatorAuthChallengeStatusController' => 'PhabricatorAuthController', 7931 + 'PhabricatorAuthChallengeUpdate' => 'Phobject', 7928 7932 'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction', 7929 7933 'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod', 7930 7934 'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker',
+2
src/applications/auth/application/PhabricatorAuthApplication.php
··· 97 97 'PhabricatorAuthFactorProviderViewController', 98 98 'message/(?P<id>[1-9]\d*)/' => 99 99 'PhabricatorAuthFactorProviderMessageController', 100 + 'challenge/status/(?P<id>[1-9]\d*)/' => 101 + 'PhabricatorAuthChallengeStatusController', 100 102 ), 101 103 102 104 'message/' => array(
+40
src/applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthChallengeStatusController 4 + extends PhabricatorAuthController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $this->getViewer(); 8 + $id = $request->getURIData('id'); 9 + $now = PhabricatorTime::getNow(); 10 + 11 + $result = new PhabricatorAuthChallengeUpdate(); 12 + 13 + $challenge = id(new PhabricatorAuthChallengeQuery()) 14 + ->setViewer($viewer) 15 + ->withIDs(array($id)) 16 + ->withUserPHIDs(array($viewer->getPHID())) 17 + ->withChallengeTTLBetween($now, null) 18 + ->executeOne(); 19 + if ($challenge) { 20 + $config = id(new PhabricatorAuthFactorConfigQuery()) 21 + ->setViewer($viewer) 22 + ->withPHIDs(array($challenge->getFactorPHID())) 23 + ->executeOne(); 24 + if ($config) { 25 + $provider = $config->getFactorProvider(); 26 + $factor = $provider->getFactor(); 27 + 28 + $result = $factor->newChallengeStatusView( 29 + $config, 30 + $provider, 31 + $viewer, 32 + $challenge); 33 + } 34 + } 35 + 36 + return id(new AphrontAjaxResponse()) 37 + ->setContent($result->newContent()); 38 + } 39 + 40 + }
+18 -5
src/applications/auth/factor/PhabricatorAuthFactor.php
··· 80 80 return array(); 81 81 } 82 82 83 + public function newChallengeStatusView( 84 + PhabricatorAuthFactorConfig $config, 85 + PhabricatorAuthFactorProvider $provider, 86 + PhabricatorUser $viewer, 87 + PhabricatorAuthChallenge $challenge) { 88 + return null; 89 + } 90 + 83 91 /** 84 92 * Is this a factor which depends on the user's contact number? 85 93 * ··· 210 218 get_class($this))); 211 219 } 212 220 213 - $result->setIssuedChallenges($challenges); 214 - 215 221 return $result; 216 222 } 217 223 ··· 241 247 'PhabricatorAuthFactorResult', 242 248 get_class($this))); 243 249 } 244 - 245 - $result->setIssuedChallenges($challenges); 246 250 247 251 return $result; 248 252 } ··· 339 343 ->setIcon('fa-commenting', 'green'); 340 344 } 341 345 342 - return id(new PHUIFormTimerControl()) 346 + $control = id(new PHUIFormTimerControl()) 343 347 ->setIcon($icon) 344 348 ->appendChild($error); 349 + 350 + $status_challenge = $result->getStatusChallenge(); 351 + if ($status_challenge) { 352 + $id = $status_challenge->getID(); 353 + $uri = "/auth/mfa/challenge/status/{$id}/"; 354 + $control->setUpdateURI($uri); 355 + } 356 + 357 + return $control; 345 358 } 346 359 347 360
+10 -10
src/applications/auth/factor/PhabricatorAuthFactorResult.php
··· 11 11 private $value; 12 12 private $issuedChallenges = array(); 13 13 private $icon; 14 + private $statusChallenge; 14 15 15 16 public function setAnsweredChallenge(PhabricatorAuthChallenge $challenge) { 16 17 if (!$challenge->getIsAnsweredChallenge()) { ··· 32 33 33 34 public function getAnsweredChallenge() { 34 35 return $this->answeredChallenge; 36 + } 37 + 38 + public function setStatusChallenge(PhabricatorAuthChallenge $challenge) { 39 + $this->statusChallenge = $challenge; 40 + return $this; 41 + } 42 + 43 + public function getStatusChallenge() { 44 + return $this->statusChallenge; 35 45 } 36 46 37 47 public function getIsValid() { ··· 81 91 82 92 public function getValue() { 83 93 return $this->value; 84 - } 85 - 86 - public function setIssuedChallenges(array $issued_challenges) { 87 - assert_instances_of($issued_challenges, 'PhabricatorAuthChallenge'); 88 - $this->issuedChallenges = $issued_challenges; 89 - return $this; 90 - } 91 - 92 - public function getIssuedChallenges() { 93 - return $this->issuedChallenges; 94 94 } 95 95 96 96 public function setIcon(PHUIIconView $icon) {
+72 -11
src/applications/auth/factor/PhabricatorDuoAuthFactor.php
··· 585 585 $result = $this->newDuoFuture($provider) 586 586 ->setHTTPMethod('GET') 587 587 ->setMethod('auth_status', $parameters) 588 - ->setTimeout(5) 588 + ->setTimeout(3) 589 589 ->resolve(); 590 590 591 591 $state = $result['response']['result']; ··· 661 661 PhabricatorAuthFactorResult $result) { 662 662 663 663 $control = $this->newAutomaticControl($result); 664 - if (!$control) { 665 - $result = $this->newResult() 666 - ->setIsContinue(true) 667 - ->setErrorMessage( 668 - pht( 669 - 'A challenge has been sent to your phone. Open the Duo '. 670 - 'application and confirm the challenge, then continue.')); 671 - $control = $this->newAutomaticControl($result); 672 - } 673 664 674 665 $control 675 666 ->setLabel(pht('Duo')) ··· 689 680 PhabricatorUser $viewer, 690 681 AphrontRequest $request, 691 682 array $challenges) { 692 - return $this->newResult(); 683 + 684 + $result = $this->newResult() 685 + ->setIsContinue(true) 686 + ->setErrorMessage( 687 + pht( 688 + 'A challenge has been sent to your phone. Open the Duo '. 689 + 'application and confirm the challenge, then continue.')); 690 + 691 + $challenge = $this->getChallengeForCurrentContext( 692 + $config, 693 + $viewer, 694 + $challenges); 695 + if ($challenge) { 696 + $result 697 + ->setStatusChallenge($challenge) 698 + ->setIcon( 699 + id(new PHUIIconView()) 700 + ->setIcon('fa-refresh', 'green ph-spin')); 701 + } 702 + 703 + return $result; 693 704 } 694 705 695 706 private function newDuoFuture(PhabricatorAuthFactorProvider $provider) { ··· 788 799 'Duo API hostname ("%s") is invalid, hostname must be '. 789 800 '"*.duosecurity.com".', 790 801 $hostname)); 802 + } 803 + 804 + public function newChallengeStatusView( 805 + PhabricatorAuthFactorConfig $config, 806 + PhabricatorAuthFactorProvider $provider, 807 + PhabricatorUser $viewer, 808 + PhabricatorAuthChallenge $challenge) { 809 + 810 + $duo_xaction = $challenge->getChallengeKey(); 811 + 812 + $parameters = array( 813 + 'txid' => $duo_xaction, 814 + ); 815 + 816 + $default_result = id(new PhabricatorAuthChallengeUpdate()) 817 + ->setRetry(true); 818 + 819 + try { 820 + $result = $this->newDuoFuture($provider) 821 + ->setHTTPMethod('GET') 822 + ->setMethod('auth_status', $parameters) 823 + ->setTimeout(5) 824 + ->resolve(); 825 + 826 + $state = $result['response']['result']; 827 + } catch (HTTPFutureCURLResponseStatus $exception) { 828 + // If we failed or timed out, retry. Usually, this is a timeout. 829 + return id(new PhabricatorAuthChallengeUpdate()) 830 + ->setRetry(true); 831 + } 832 + 833 + // For now, don't update the view for anything but an "Allow". Updates 834 + // here are just about providing more visual feedback for user convenience. 835 + if ($state !== 'allow') { 836 + return id(new PhabricatorAuthChallengeUpdate()) 837 + ->setRetry(false); 838 + } 839 + 840 + $icon = id(new PHUIIconView()) 841 + ->setIcon('fa-check-circle-o', 'green'); 842 + 843 + $view = id(new PHUIFormTimerControl()) 844 + ->setIcon($icon) 845 + ->appendChild(pht('You responded to this challenge correctly.')) 846 + ->newTimerView(); 847 + 848 + return id(new PhabricatorAuthChallengeUpdate()) 849 + ->setState('allow') 850 + ->setRetry(false) 851 + ->setMarkup($view); 791 852 } 792 853 793 854 }
+44
src/applications/auth/view/PhabricatorAuthChallengeUpdate.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthChallengeUpdate 4 + extends Phobject { 5 + 6 + private $retry = false; 7 + private $state; 8 + private $markup; 9 + 10 + public function setRetry($retry) { 11 + $this->retry = $retry; 12 + return $this; 13 + } 14 + 15 + public function getRetry() { 16 + return $this->retry; 17 + } 18 + 19 + public function setState($state) { 20 + $this->state = $state; 21 + return $this; 22 + } 23 + 24 + public function getState() { 25 + return $this->state; 26 + } 27 + 28 + public function setMarkup($markup) { 29 + $this->markup = $markup; 30 + return $this; 31 + } 32 + 33 + public function getMarkup() { 34 + return $this->markup; 35 + } 36 + 37 + public function newContent() { 38 + return array( 39 + 'retry' => $this->getRetry(), 40 + 'state' => $this->getState(), 41 + 'markup' => $this->getMarkup(), 42 + ); 43 + } 44 + }
+29 -1
src/view/form/control/PHUIFormTimerControl.php
··· 3 3 final class PHUIFormTimerControl extends AphrontFormControl { 4 4 5 5 private $icon; 6 + private $updateURI; 6 7 7 8 public function setIcon(PHUIIconView $icon) { 8 9 $this->icon = $icon; ··· 13 14 return $this->icon; 14 15 } 15 16 17 + public function setUpdateURI($update_uri) { 18 + $this->updateURI = $update_uri; 19 + return $this; 20 + } 21 + 22 + public function getUpdateURI() { 23 + return $this->updateURI; 24 + } 25 + 16 26 protected function getCustomControlClass() { 17 27 return 'phui-form-timer'; 18 28 } 19 29 20 30 protected function renderInput() { 31 + return $this->newTimerView(); 32 + } 33 + 34 + public function newTimerView() { 21 35 $icon_cell = phutil_tag( 22 36 'td', 23 37 array( ··· 34 48 35 49 $row = phutil_tag('tr', array(), array($icon_cell, $content_cell)); 36 50 37 - return phutil_tag('table', array(), $row); 51 + $node_id = null; 52 + 53 + $update_uri = $this->getUpdateURI(); 54 + if ($update_uri) { 55 + $node_id = celerity_generate_unique_node_id(); 56 + 57 + Javelin::initBehavior( 58 + 'phui-timer-control', 59 + array( 60 + 'nodeID' => $node_id, 61 + 'uri' => $update_uri, 62 + )); 63 + } 64 + 65 + return phutil_tag('table', array('id' => $node_id), $row); 38 66 } 39 67 40 68 }
+14
webroot/rsrc/css/phui/phui-form-view.css
··· 578 578 .mfa-form-enroll-button { 579 579 text-align: center; 580 580 } 581 + 582 + .phui-form-timer-updated { 583 + animation: phui-form-timer-fade-in 2s linear; 584 + } 585 + 586 + 587 + @keyframes phui-form-timer-fade-in { 588 + 0% { 589 + background-color: {$lightyellow}; 590 + } 591 + 100% { 592 + background-color: transparent; 593 + } 594 + }
+41
webroot/rsrc/js/phui/behavior-phui-timer-control.js
··· 1 + /** 2 + * @provides javelin-behavior-phui-timer-control 3 + * @requires javelin-behavior 4 + * javelin-stratcom 5 + * javelin-dom 6 + */ 7 + 8 + JX.behavior('phui-timer-control', function(config) { 9 + var node = JX.$(config.nodeID); 10 + var uri = config.uri; 11 + var state = null; 12 + 13 + function onupdate(result) { 14 + var markup = result.markup; 15 + if (markup) { 16 + var new_node = JX.$H(markup).getFragment().firstChild; 17 + JX.DOM.replace(node, new_node); 18 + node = new_node; 19 + 20 + // If the overall state has changed from the previous display state, 21 + // animate the control to draw the user's attention to the state change. 22 + if (result.state !== state) { 23 + state = result.state; 24 + JX.DOM.alterClass(node, 'phui-form-timer-updated', true); 25 + } 26 + } 27 + 28 + var retry = result.retry; 29 + if (retry) { 30 + setTimeout(update, 1000); 31 + } 32 + } 33 + 34 + function update() { 35 + new JX.Request(uri, onupdate) 36 + .setTimeout(10000) 37 + .send(); 38 + } 39 + 40 + update(); 41 + });