Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15397320
D20169.id48183.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
16 KB
Referenced Files
None
Subscribers
None
D20169.id48183.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D20169: When users confirm Duo MFA in the mobile app, live-update the UI
Attached
Detach File
Event Timeline
Log In to Comment