mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-22 13:30:55 +01:00
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
This commit is contained in:
parent
454a762562
commit
2ca316d652
11 changed files with 284 additions and 30 deletions
|
@ -9,7 +9,7 @@ return array(
|
|||
'names' => array(
|
||||
'conpherence.pkg.css' => '3c8a0668',
|
||||
'conpherence.pkg.js' => '020aebcf',
|
||||
'core.pkg.css' => '7a73ffc5',
|
||||
'core.pkg.css' => 'e0f5d66f',
|
||||
'core.pkg.js' => '5c737607',
|
||||
'differential.pkg.css' => 'b8df73d4',
|
||||
'differential.pkg.js' => '67c9ea4c',
|
||||
|
@ -151,7 +151,7 @@ return array(
|
|||
'rsrc/css/phui/phui-document.css' => '52b748a5',
|
||||
'rsrc/css/phui/phui-feed-story.css' => 'a0c05029',
|
||||
'rsrc/css/phui/phui-fontkit.css' => '9b714a5e',
|
||||
'rsrc/css/phui/phui-form-view.css' => '0807e7ac',
|
||||
'rsrc/css/phui/phui-form-view.css' => '01b796c0',
|
||||
'rsrc/css/phui/phui-form.css' => '159e2d9c',
|
||||
'rsrc/css/phui/phui-head-thing.css' => 'd7f293df',
|
||||
'rsrc/css/phui/phui-header-view.css' => '93cea4ec',
|
||||
|
@ -502,6 +502,7 @@ return array(
|
|||
'rsrc/js/phui/behavior-phui-selectable-list.js' => 'b26a41e4',
|
||||
'rsrc/js/phui/behavior-phui-submenu.js' => 'b5e9bff9',
|
||||
'rsrc/js/phui/behavior-phui-tab-group.js' => '242aa08b',
|
||||
'rsrc/js/phui/behavior-phui-timer-control.js' => 'f84bcbf4',
|
||||
'rsrc/js/phuix/PHUIXActionListView.js' => 'c68f183f',
|
||||
'rsrc/js/phuix/PHUIXActionView.js' => 'aaa08f3b',
|
||||
'rsrc/js/phuix/PHUIXAutocomplete.js' => '58cc4ab8',
|
||||
|
@ -650,6 +651,7 @@ return array(
|
|||
'javelin-behavior-phui-selectable-list' => 'b26a41e4',
|
||||
'javelin-behavior-phui-submenu' => 'b5e9bff9',
|
||||
'javelin-behavior-phui-tab-group' => '242aa08b',
|
||||
'javelin-behavior-phui-timer-control' => 'f84bcbf4',
|
||||
'javelin-behavior-phuix-example' => 'c2c500a7',
|
||||
'javelin-behavior-policy-control' => '0eaa33a9',
|
||||
'javelin-behavior-policy-rule-editor' => '9347f172',
|
||||
|
@ -817,7 +819,7 @@ return array(
|
|||
'phui-font-icon-base-css' => 'd7994e06',
|
||||
'phui-fontkit-css' => '9b714a5e',
|
||||
'phui-form-css' => '159e2d9c',
|
||||
'phui-form-view-css' => '0807e7ac',
|
||||
'phui-form-view-css' => '01b796c0',
|
||||
'phui-head-thing-view-css' => 'd7f293df',
|
||||
'phui-header-view-css' => '93cea4ec',
|
||||
'phui-hovercard' => '074f0783',
|
||||
|
@ -2111,6 +2113,11 @@ return array(
|
|||
'javelin-stratcom',
|
||||
'javelin-dom',
|
||||
),
|
||||
'f84bcbf4' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-stratcom',
|
||||
'javelin-dom',
|
||||
),
|
||||
'f8c4e135' => array(
|
||||
'javelin-install',
|
||||
'javelin-dom',
|
||||
|
|
|
@ -2195,6 +2195,8 @@ phutil_register_library_map(array(
|
|||
'PhabricatorAuthChallengeGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php',
|
||||
'PhabricatorAuthChallengePHIDType' => 'applications/auth/phid/PhabricatorAuthChallengePHIDType.php',
|
||||
'PhabricatorAuthChallengeQuery' => 'applications/auth/query/PhabricatorAuthChallengeQuery.php',
|
||||
'PhabricatorAuthChallengeStatusController' => 'applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php',
|
||||
'PhabricatorAuthChallengeUpdate' => 'applications/auth/view/PhabricatorAuthChallengeUpdate.php',
|
||||
'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php',
|
||||
'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php',
|
||||
'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php',
|
||||
|
@ -7925,6 +7927,8 @@ phutil_register_library_map(array(
|
|||
'PhabricatorAuthChallengeGarbageCollector' => 'PhabricatorGarbageCollector',
|
||||
'PhabricatorAuthChallengePHIDType' => 'PhabricatorPHIDType',
|
||||
'PhabricatorAuthChallengeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'PhabricatorAuthChallengeStatusController' => 'PhabricatorAuthController',
|
||||
'PhabricatorAuthChallengeUpdate' => 'Phobject',
|
||||
'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction',
|
||||
'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod',
|
||||
'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker',
|
||||
|
|
|
@ -97,6 +97,8 @@ final class PhabricatorAuthApplication extends PhabricatorApplication {
|
|||
'PhabricatorAuthFactorProviderViewController',
|
||||
'message/(?P<id>[1-9]\d*)/' =>
|
||||
'PhabricatorAuthFactorProviderMessageController',
|
||||
'challenge/status/(?P<id>[1-9]\d*)/' =>
|
||||
'PhabricatorAuthChallengeStatusController',
|
||||
),
|
||||
|
||||
'message/' => array(
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAuthChallengeStatusController
|
||||
extends PhabricatorAuthController {
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$viewer = $this->getViewer();
|
||||
$id = $request->getURIData('id');
|
||||
$now = PhabricatorTime::getNow();
|
||||
|
||||
$result = new PhabricatorAuthChallengeUpdate();
|
||||
|
||||
$challenge = id(new PhabricatorAuthChallengeQuery())
|
||||
->setViewer($viewer)
|
||||
->withIDs(array($id))
|
||||
->withUserPHIDs(array($viewer->getPHID()))
|
||||
->withChallengeTTLBetween($now, null)
|
||||
->executeOne();
|
||||
if ($challenge) {
|
||||
$config = id(new PhabricatorAuthFactorConfigQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs(array($challenge->getFactorPHID()))
|
||||
->executeOne();
|
||||
if ($config) {
|
||||
$provider = $config->getFactorProvider();
|
||||
$factor = $provider->getFactor();
|
||||
|
||||
$result = $factor->newChallengeStatusView(
|
||||
$config,
|
||||
$provider,
|
||||
$viewer,
|
||||
$challenge);
|
||||
}
|
||||
}
|
||||
|
||||
return id(new AphrontAjaxResponse())
|
||||
->setContent($result->newContent());
|
||||
}
|
||||
|
||||
}
|
|
@ -80,6 +80,14 @@ abstract class PhabricatorAuthFactor extends Phobject {
|
|||
return array();
|
||||
}
|
||||
|
||||
public function newChallengeStatusView(
|
||||
PhabricatorAuthFactorConfig $config,
|
||||
PhabricatorAuthFactorProvider $provider,
|
||||
PhabricatorUser $viewer,
|
||||
PhabricatorAuthChallenge $challenge) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a factor which depends on the user's contact number?
|
||||
*
|
||||
|
@ -210,8 +218,6 @@ abstract class PhabricatorAuthFactor extends Phobject {
|
|||
get_class($this)));
|
||||
}
|
||||
|
||||
$result->setIssuedChallenges($challenges);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
@ -242,8 +248,6 @@ abstract class PhabricatorAuthFactor extends Phobject {
|
|||
get_class($this)));
|
||||
}
|
||||
|
||||
$result->setIssuedChallenges($challenges);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
@ -339,9 +343,18 @@ abstract class PhabricatorAuthFactor extends Phobject {
|
|||
->setIcon('fa-commenting', 'green');
|
||||
}
|
||||
|
||||
return id(new PHUIFormTimerControl())
|
||||
$control = id(new PHUIFormTimerControl())
|
||||
->setIcon($icon)
|
||||
->appendChild($error);
|
||||
|
||||
$status_challenge = $result->getStatusChallenge();
|
||||
if ($status_challenge) {
|
||||
$id = $status_challenge->getID();
|
||||
$uri = "/auth/mfa/challenge/status/{$id}/";
|
||||
$control->setUpdateURI($uri);
|
||||
}
|
||||
|
||||
return $control;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ final class PhabricatorAuthFactorResult
|
|||
private $value;
|
||||
private $issuedChallenges = array();
|
||||
private $icon;
|
||||
private $statusChallenge;
|
||||
|
||||
public function setAnsweredChallenge(PhabricatorAuthChallenge $challenge) {
|
||||
if (!$challenge->getIsAnsweredChallenge()) {
|
||||
|
@ -34,6 +35,15 @@ final class PhabricatorAuthFactorResult
|
|||
return $this->answeredChallenge;
|
||||
}
|
||||
|
||||
public function setStatusChallenge(PhabricatorAuthChallenge $challenge) {
|
||||
$this->statusChallenge = $challenge;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatusChallenge() {
|
||||
return $this->statusChallenge;
|
||||
}
|
||||
|
||||
public function getIsValid() {
|
||||
return (bool)$this->getAnsweredChallenge();
|
||||
}
|
||||
|
@ -83,16 +93,6 @@ final class PhabricatorAuthFactorResult
|
|||
return $this->value;
|
||||
}
|
||||
|
||||
public function setIssuedChallenges(array $issued_challenges) {
|
||||
assert_instances_of($issued_challenges, 'PhabricatorAuthChallenge');
|
||||
$this->issuedChallenges = $issued_challenges;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIssuedChallenges() {
|
||||
return $this->issuedChallenges;
|
||||
}
|
||||
|
||||
public function setIcon(PHUIIconView $icon) {
|
||||
$this->icon = $icon;
|
||||
return $this;
|
||||
|
|
|
@ -585,7 +585,7 @@ final class PhabricatorDuoAuthFactor
|
|||
$result = $this->newDuoFuture($provider)
|
||||
->setHTTPMethod('GET')
|
||||
->setMethod('auth_status', $parameters)
|
||||
->setTimeout(5)
|
||||
->setTimeout(3)
|
||||
->resolve();
|
||||
|
||||
$state = $result['response']['result'];
|
||||
|
@ -661,15 +661,6 @@ final class PhabricatorDuoAuthFactor
|
|||
PhabricatorAuthFactorResult $result) {
|
||||
|
||||
$control = $this->newAutomaticControl($result);
|
||||
if (!$control) {
|
||||
$result = $this->newResult()
|
||||
->setIsContinue(true)
|
||||
->setErrorMessage(
|
||||
pht(
|
||||
'A challenge has been sent to your phone. Open the Duo '.
|
||||
'application and confirm the challenge, then continue.'));
|
||||
$control = $this->newAutomaticControl($result);
|
||||
}
|
||||
|
||||
$control
|
||||
->setLabel(pht('Duo'))
|
||||
|
@ -689,7 +680,27 @@ final class PhabricatorDuoAuthFactor
|
|||
PhabricatorUser $viewer,
|
||||
AphrontRequest $request,
|
||||
array $challenges) {
|
||||
return $this->newResult();
|
||||
|
||||
$result = $this->newResult()
|
||||
->setIsContinue(true)
|
||||
->setErrorMessage(
|
||||
pht(
|
||||
'A challenge has been sent to your phone. Open the Duo '.
|
||||
'application and confirm the challenge, then continue.'));
|
||||
|
||||
$challenge = $this->getChallengeForCurrentContext(
|
||||
$config,
|
||||
$viewer,
|
||||
$challenges);
|
||||
if ($challenge) {
|
||||
$result
|
||||
->setStatusChallenge($challenge)
|
||||
->setIcon(
|
||||
id(new PHUIIconView())
|
||||
->setIcon('fa-refresh', 'green ph-spin'));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function newDuoFuture(PhabricatorAuthFactorProvider $provider) {
|
||||
|
@ -790,4 +801,54 @@ final class PhabricatorDuoAuthFactor
|
|||
$hostname));
|
||||
}
|
||||
|
||||
public function newChallengeStatusView(
|
||||
PhabricatorAuthFactorConfig $config,
|
||||
PhabricatorAuthFactorProvider $provider,
|
||||
PhabricatorUser $viewer,
|
||||
PhabricatorAuthChallenge $challenge) {
|
||||
|
||||
$duo_xaction = $challenge->getChallengeKey();
|
||||
|
||||
$parameters = array(
|
||||
'txid' => $duo_xaction,
|
||||
);
|
||||
|
||||
$default_result = id(new PhabricatorAuthChallengeUpdate())
|
||||
->setRetry(true);
|
||||
|
||||
try {
|
||||
$result = $this->newDuoFuture($provider)
|
||||
->setHTTPMethod('GET')
|
||||
->setMethod('auth_status', $parameters)
|
||||
->setTimeout(5)
|
||||
->resolve();
|
||||
|
||||
$state = $result['response']['result'];
|
||||
} catch (HTTPFutureCURLResponseStatus $exception) {
|
||||
// If we failed or timed out, retry. Usually, this is a timeout.
|
||||
return id(new PhabricatorAuthChallengeUpdate())
|
||||
->setRetry(true);
|
||||
}
|
||||
|
||||
// For now, don't update the view for anything but an "Allow". Updates
|
||||
// here are just about providing more visual feedback for user convenience.
|
||||
if ($state !== 'allow') {
|
||||
return id(new PhabricatorAuthChallengeUpdate())
|
||||
->setRetry(false);
|
||||
}
|
||||
|
||||
$icon = id(new PHUIIconView())
|
||||
->setIcon('fa-check-circle-o', 'green');
|
||||
|
||||
$view = id(new PHUIFormTimerControl())
|
||||
->setIcon($icon)
|
||||
->appendChild(pht('You responded to this challenge correctly.'))
|
||||
->newTimerView();
|
||||
|
||||
return id(new PhabricatorAuthChallengeUpdate())
|
||||
->setState('allow')
|
||||
->setRetry(false)
|
||||
->setMarkup($view);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAuthChallengeUpdate
|
||||
extends Phobject {
|
||||
|
||||
private $retry = false;
|
||||
private $state;
|
||||
private $markup;
|
||||
|
||||
public function setRetry($retry) {
|
||||
$this->retry = $retry;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRetry() {
|
||||
return $this->retry;
|
||||
}
|
||||
|
||||
public function setState($state) {
|
||||
$this->state = $state;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getState() {
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
public function setMarkup($markup) {
|
||||
$this->markup = $markup;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMarkup() {
|
||||
return $this->markup;
|
||||
}
|
||||
|
||||
public function newContent() {
|
||||
return array(
|
||||
'retry' => $this->getRetry(),
|
||||
'state' => $this->getState(),
|
||||
'markup' => $this->getMarkup(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
final class PHUIFormTimerControl extends AphrontFormControl {
|
||||
|
||||
private $icon;
|
||||
private $updateURI;
|
||||
|
||||
public function setIcon(PHUIIconView $icon) {
|
||||
$this->icon = $icon;
|
||||
|
@ -13,11 +14,24 @@ final class PHUIFormTimerControl extends AphrontFormControl {
|
|||
return $this->icon;
|
||||
}
|
||||
|
||||
public function setUpdateURI($update_uri) {
|
||||
$this->updateURI = $update_uri;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUpdateURI() {
|
||||
return $this->updateURI;
|
||||
}
|
||||
|
||||
protected function getCustomControlClass() {
|
||||
return 'phui-form-timer';
|
||||
}
|
||||
|
||||
protected function renderInput() {
|
||||
return $this->newTimerView();
|
||||
}
|
||||
|
||||
public function newTimerView() {
|
||||
$icon_cell = phutil_tag(
|
||||
'td',
|
||||
array(
|
||||
|
@ -34,7 +48,21 @@ final class PHUIFormTimerControl extends AphrontFormControl {
|
|||
|
||||
$row = phutil_tag('tr', array(), array($icon_cell, $content_cell));
|
||||
|
||||
return phutil_tag('table', array(), $row);
|
||||
$node_id = null;
|
||||
|
||||
$update_uri = $this->getUpdateURI();
|
||||
if ($update_uri) {
|
||||
$node_id = celerity_generate_unique_node_id();
|
||||
|
||||
Javelin::initBehavior(
|
||||
'phui-timer-control',
|
||||
array(
|
||||
'nodeID' => $node_id,
|
||||
'uri' => $update_uri,
|
||||
));
|
||||
}
|
||||
|
||||
return phutil_tag('table', array('id' => $node_id), $row);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -578,3 +578,17 @@ properly, and submit values. */
|
|||
.mfa-form-enroll-button {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.phui-form-timer-updated {
|
||||
animation: phui-form-timer-fade-in 2s linear;
|
||||
}
|
||||
|
||||
|
||||
@keyframes phui-form-timer-fade-in {
|
||||
0% {
|
||||
background-color: {$lightyellow};
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
|
41
webroot/rsrc/js/phui/behavior-phui-timer-control.js
Normal file
41
webroot/rsrc/js/phui/behavior-phui-timer-control.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* @provides javelin-behavior-phui-timer-control
|
||||
* @requires javelin-behavior
|
||||
* javelin-stratcom
|
||||
* javelin-dom
|
||||
*/
|
||||
|
||||
JX.behavior('phui-timer-control', function(config) {
|
||||
var node = JX.$(config.nodeID);
|
||||
var uri = config.uri;
|
||||
var state = null;
|
||||
|
||||
function onupdate(result) {
|
||||
var markup = result.markup;
|
||||
if (markup) {
|
||||
var new_node = JX.$H(markup).getFragment().firstChild;
|
||||
JX.DOM.replace(node, new_node);
|
||||
node = new_node;
|
||||
|
||||
// If the overall state has changed from the previous display state,
|
||||
// animate the control to draw the user's attention to the state change.
|
||||
if (result.state !== state) {
|
||||
state = result.state;
|
||||
JX.DOM.alterClass(node, 'phui-form-timer-updated', true);
|
||||
}
|
||||
}
|
||||
|
||||
var retry = result.retry;
|
||||
if (retry) {
|
||||
setTimeout(update, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
new JX.Request(uri, onupdate)
|
||||
.setTimeout(10000)
|
||||
.send();
|
||||
}
|
||||
|
||||
update();
|
||||
});
|
Loading…
Reference in a new issue