Page MenuHomePhabricator

D20169.id48183.diff
No OneTemporary

D20169.id48183.diff

diff --git a/resources/celerity/map.php b/resources/celerity/map.php
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -9,7 +9,7 @@
'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 @@
'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 @@
'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 @@
'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 @@
'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 @@
'javelin-stratcom',
'javelin-dom',
),
+ 'f84bcbf4' => array(
+ 'javelin-behavior',
+ 'javelin-stratcom',
+ 'javelin-dom',
+ ),
'f8c4e135' => array(
'javelin-install',
'javelin-dom',
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -2195,6 +2195,8 @@
'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 @@
'PhabricatorAuthChallengeGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorAuthChallengePHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthChallengeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorAuthChallengeStatusController' => 'PhabricatorAuthController',
+ 'PhabricatorAuthChallengeUpdate' => 'Phobject',
'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction',
'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod',
'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker',
diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php
--- a/src/applications/auth/application/PhabricatorAuthApplication.php
+++ b/src/applications/auth/application/PhabricatorAuthApplication.php
@@ -97,6 +97,8 @@
'PhabricatorAuthFactorProviderViewController',
'message/(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthFactorProviderMessageController',
+ 'challenge/status/(?P<id>[1-9]\d*)/' =>
+ 'PhabricatorAuthChallengeStatusController',
),
'message/' => array(
diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php b/src/applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php
@@ -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());
+ }
+
+}
diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php
--- a/src/applications/auth/factor/PhabricatorAuthFactor.php
+++ b/src/applications/auth/factor/PhabricatorAuthFactor.php
@@ -80,6 +80,14 @@
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 @@
get_class($this)));
}
- $result->setIssuedChallenges($challenges);
-
return $result;
}
@@ -242,8 +248,6 @@
get_class($this)));
}
- $result->setIssuedChallenges($challenges);
-
return $result;
}
@@ -339,9 +343,18 @@
->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;
}
diff --git a/src/applications/auth/factor/PhabricatorAuthFactorResult.php b/src/applications/auth/factor/PhabricatorAuthFactorResult.php
--- a/src/applications/auth/factor/PhabricatorAuthFactorResult.php
+++ b/src/applications/auth/factor/PhabricatorAuthFactorResult.php
@@ -11,6 +11,7 @@
private $value;
private $issuedChallenges = array();
private $icon;
+ private $statusChallenge;
public function setAnsweredChallenge(PhabricatorAuthChallenge $challenge) {
if (!$challenge->getIsAnsweredChallenge()) {
@@ -34,6 +35,15 @@
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 @@
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;
diff --git a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php
--- a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php
+++ b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php
@@ -585,7 +585,7 @@
$result = $this->newDuoFuture($provider)
->setHTTPMethod('GET')
->setMethod('auth_status', $parameters)
- ->setTimeout(5)
+ ->setTimeout(3)
->resolve();
$state = $result['response']['result'];
@@ -661,15 +661,6 @@
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 @@
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 @@
$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);
+ }
+
}
diff --git a/src/applications/auth/view/PhabricatorAuthChallengeUpdate.php b/src/applications/auth/view/PhabricatorAuthChallengeUpdate.php
new file mode 100644
--- /dev/null
+++ b/src/applications/auth/view/PhabricatorAuthChallengeUpdate.php
@@ -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(),
+ );
+ }
+}
diff --git a/src/view/form/control/PHUIFormTimerControl.php b/src/view/form/control/PHUIFormTimerControl.php
--- a/src/view/form/control/PHUIFormTimerControl.php
+++ b/src/view/form/control/PHUIFormTimerControl.php
@@ -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 @@
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 @@
$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);
}
}
diff --git a/webroot/rsrc/css/phui/phui-form-view.css b/webroot/rsrc/css/phui/phui-form-view.css
--- a/webroot/rsrc/css/phui/phui-form-view.css
+++ b/webroot/rsrc/css/phui/phui-form-view.css
@@ -578,3 +578,17 @@
.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;
+ }
+}
diff --git a/webroot/rsrc/js/phui/behavior-phui-timer-control.js b/webroot/rsrc/js/phui/behavior-phui-timer-control.js
new file mode 100644
--- /dev/null
+++ b/webroot/rsrc/js/phui/behavior-phui-timer-control.js
@@ -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();
+});

File Metadata

Mime Type
text/plain
Expires
Mon, Mar 17, 7:33 PM (5 d, 10 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7662362
Default Alt Text
D20169.id48183.diff (16 KB)

Event Timeline