mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-27 07:50:57 +01:00
Bring Duo MFA upstream
Summary: Depends on D20038. Ref T13231. Although I planned to keep this out of the upstream (see T13229) it ended up having enough pieces that I imagine it may need more fixes/updates than we can reasonably manage by copy/pasting stuff around. Until T5055, we don't really have good tools for managing this. Make my life easier by just upstreaming this. Test Plan: See T13231 for a bunch of workflow discussion. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13231 Differential Revision: https://secure.phabricator.com/D20039
This commit is contained in:
parent
d8d4efe89e
commit
9fd8343704
13 changed files with 1076 additions and 8 deletions
|
@ -2228,6 +2228,10 @@ phutil_register_library_map(array(
|
|||
'PhabricatorAuthFactorConfigQuery' => 'applications/auth/query/PhabricatorAuthFactorConfigQuery.php',
|
||||
'PhabricatorAuthFactorProvider' => 'applications/auth/storage/PhabricatorAuthFactorProvider.php',
|
||||
'PhabricatorAuthFactorProviderController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php',
|
||||
'PhabricatorAuthFactorProviderDuoCredentialTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoCredentialTransaction.php',
|
||||
'PhabricatorAuthFactorProviderDuoEnrollTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoEnrollTransaction.php',
|
||||
'PhabricatorAuthFactorProviderDuoHostnameTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoHostnameTransaction.php',
|
||||
'PhabricatorAuthFactorProviderDuoUsernamesTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoUsernamesTransaction.php',
|
||||
'PhabricatorAuthFactorProviderEditController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php',
|
||||
'PhabricatorAuthFactorProviderEditEngine' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php',
|
||||
'PhabricatorAuthFactorProviderEditor' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditor.php',
|
||||
|
@ -2800,6 +2804,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorCountdownTransactionType' => 'applications/countdown/xaction/PhabricatorCountdownTransactionType.php',
|
||||
'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php',
|
||||
'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php',
|
||||
'PhabricatorCredentialEditField' => 'applications/transactions/editfield/PhabricatorCredentialEditField.php',
|
||||
'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php',
|
||||
'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php',
|
||||
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php',
|
||||
|
@ -2986,6 +2991,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorDraftEngine' => 'applications/transactions/draft/PhabricatorDraftEngine.php',
|
||||
'PhabricatorDraftInterface' => 'applications/transactions/draft/PhabricatorDraftInterface.php',
|
||||
'PhabricatorDrydockApplication' => 'applications/drydock/application/PhabricatorDrydockApplication.php',
|
||||
'PhabricatorDuoAuthFactor' => 'applications/auth/factor/PhabricatorDuoAuthFactor.php',
|
||||
'PhabricatorDuoFuture' => 'applications/auth/future/PhabricatorDuoFuture.php',
|
||||
'PhabricatorEdgeChangeRecord' => 'infrastructure/edges/util/PhabricatorEdgeChangeRecord.php',
|
||||
'PhabricatorEdgeChangeRecordTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeChangeRecordTestCase.php',
|
||||
|
@ -7958,6 +7964,10 @@ phutil_register_library_map(array(
|
|||
'PhabricatorEditEngineMFAInterface',
|
||||
),
|
||||
'PhabricatorAuthFactorProviderController' => 'PhabricatorAuthProviderController',
|
||||
'PhabricatorAuthFactorProviderDuoCredentialTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
|
||||
'PhabricatorAuthFactorProviderDuoEnrollTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
|
||||
'PhabricatorAuthFactorProviderDuoHostnameTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
|
||||
'PhabricatorAuthFactorProviderDuoUsernamesTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
|
||||
'PhabricatorAuthFactorProviderEditController' => 'PhabricatorAuthFactorProviderController',
|
||||
'PhabricatorAuthFactorProviderEditEngine' => 'PhabricatorEditEngine',
|
||||
'PhabricatorAuthFactorProviderEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||
|
@ -8633,6 +8643,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorCountdownTransactionType' => 'PhabricatorModularTransactionType',
|
||||
'PhabricatorCountdownView' => 'AphrontView',
|
||||
'PhabricatorCountdownViewController' => 'PhabricatorCountdownController',
|
||||
'PhabricatorCredentialEditField' => 'PhabricatorEditField',
|
||||
'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery',
|
||||
'PhabricatorCustomField' => 'Phobject',
|
||||
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
|
||||
|
@ -8837,6 +8848,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorDraftDAO' => 'PhabricatorLiskDAO',
|
||||
'PhabricatorDraftEngine' => 'Phobject',
|
||||
'PhabricatorDrydockApplication' => 'PhabricatorApplication',
|
||||
'PhabricatorDuoAuthFactor' => 'PhabricatorAuthFactor',
|
||||
'PhabricatorDuoFuture' => 'FutureProxy',
|
||||
'PhabricatorEdgeChangeRecord' => 'Phobject',
|
||||
'PhabricatorEdgeChangeRecordTestCase' => 'PhabricatorTestCase',
|
||||
|
|
|
@ -93,11 +93,12 @@ final class PhabricatorAuthFactorProviderEditEngine
|
|||
}
|
||||
|
||||
protected function buildCustomEditFields($object) {
|
||||
$factor_name = $object->getFactor()->getFactorName();
|
||||
$factor = $object->getFactor();
|
||||
$factor_name = $factor->getFactorName();
|
||||
|
||||
$status_map = PhabricatorAuthFactorProviderStatus::getMap();
|
||||
|
||||
return array(
|
||||
$fields = array(
|
||||
id(new PhabricatorStaticEditField())
|
||||
->setKey('displayType')
|
||||
->setLabel(pht('Factor Type'))
|
||||
|
@ -120,6 +121,13 @@ final class PhabricatorAuthFactorProviderEditEngine
|
|||
->setValue($object->getStatus())
|
||||
->setOptions($status_map),
|
||||
);
|
||||
|
||||
$factor_fields = $factor->newEditEngineFields($this, $object);
|
||||
foreach ($factor_fields as $field) {
|
||||
$fields[] = $field;
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -74,6 +74,12 @@ abstract class PhabricatorAuthFactor extends Phobject {
|
|||
return null;
|
||||
}
|
||||
|
||||
public function newEditEngineFields(
|
||||
PhabricatorEditEngine $engine,
|
||||
PhabricatorAuthFactorProvider $provider) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a factor which depends on the user's contact number?
|
||||
*
|
||||
|
@ -331,6 +337,7 @@ abstract class PhabricatorAuthFactor extends Phobject {
|
|||
|
||||
|
||||
final protected function loadMFASyncToken(
|
||||
PhabricatorAuthFactorProvider $provider,
|
||||
AphrontRequest $request,
|
||||
AphrontFormView $form,
|
||||
PhabricatorUser $user) {
|
||||
|
@ -397,7 +404,9 @@ abstract class PhabricatorAuthFactor extends Phobject {
|
|||
->setTokenCode($sync_key_digest)
|
||||
->setTokenExpires($now + $sync_ttl);
|
||||
|
||||
$properties = $this->newMFASyncTokenProperties($user);
|
||||
$properties = $this->newMFASyncTokenProperties(
|
||||
$provider,
|
||||
$user);
|
||||
|
||||
foreach ($properties as $key => $value) {
|
||||
$sync_token->setTemporaryTokenProperty($key, $value);
|
||||
|
@ -411,7 +420,9 @@ abstract class PhabricatorAuthFactor extends Phobject {
|
|||
return $sync_token;
|
||||
}
|
||||
|
||||
protected function newMFASyncTokenProperties(PhabricatorUser $user) {
|
||||
protected function newMFASyncTokenProperties(
|
||||
PhabricatorAuthFactorProvider $provider,
|
||||
PhabricatorUser $user) {
|
||||
return array();
|
||||
}
|
||||
|
||||
|
|
802
src/applications/auth/factor/PhabricatorDuoAuthFactor.php
Normal file
802
src/applications/auth/factor/PhabricatorDuoAuthFactor.php
Normal file
|
@ -0,0 +1,802 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorDuoAuthFactor
|
||||
extends PhabricatorAuthFactor {
|
||||
|
||||
const PROP_CREDENTIAL = 'duo.credentialPHID';
|
||||
const PROP_ENROLL = 'duo.enroll';
|
||||
const PROP_USERNAMES = 'duo.usernames';
|
||||
const PROP_HOSTNAME = 'duo.hostname';
|
||||
|
||||
public function getFactorKey() {
|
||||
return 'duo';
|
||||
}
|
||||
|
||||
public function getFactorName() {
|
||||
return pht('Duo Security');
|
||||
}
|
||||
|
||||
public function getFactorShortName() {
|
||||
return pht('Duo');
|
||||
}
|
||||
|
||||
public function getFactorCreateHelp() {
|
||||
return pht('Support for Duo push authentication.');
|
||||
}
|
||||
|
||||
public function getFactorDescription() {
|
||||
return pht(
|
||||
'When you need to authenticate, a request will be pushed to the '.
|
||||
'Duo application on your phone.');
|
||||
}
|
||||
|
||||
public function getEnrollDescription(
|
||||
PhabricatorAuthFactorProvider $provider,
|
||||
PhabricatorUser $user) {
|
||||
return pht(
|
||||
'To add a Duo factor, first download and install the Duo application '.
|
||||
'on your phone. Once you have launched the application and are ready '.
|
||||
'to perform setup, click continue.');
|
||||
}
|
||||
|
||||
public function canCreateNewConfiguration(
|
||||
PhabricatorAuthFactorProvider $provider,
|
||||
PhabricatorUser $user) {
|
||||
|
||||
if ($this->loadConfigurationsForProvider($provider, $user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getConfigurationCreateDescription(
|
||||
PhabricatorAuthFactorProvider $provider,
|
||||
PhabricatorUser $user) {
|
||||
|
||||
$messages = array();
|
||||
|
||||
if ($this->loadConfigurationsForProvider($provider, $user)) {
|
||||
$messages[] = id(new PHUIInfoView())
|
||||
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
|
||||
->setErrors(
|
||||
array(
|
||||
pht(
|
||||
'You already have Duo authentication attached to your account '.
|
||||
'for this provider.'),
|
||||
));
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
public function getConfigurationListDetails(
|
||||
PhabricatorAuthFactorConfig $config,
|
||||
PhabricatorAuthFactorProvider $provider,
|
||||
PhabricatorUser $viewer) {
|
||||
|
||||
$duo_user = $config->getAuthFactorConfigProperty('duo.username');
|
||||
|
||||
return pht('Duo Username: %s', $duo_user);
|
||||
}
|
||||
|
||||
|
||||
public function newEditEngineFields(
|
||||
PhabricatorEditEngine $engine,
|
||||
PhabricatorAuthFactorProvider $provider) {
|
||||
|
||||
$viewer = $engine->getViewer();
|
||||
|
||||
$credential_phid = $provider->getAuthFactorProviderProperty(
|
||||
self::PROP_CREDENTIAL);
|
||||
|
||||
$hostname = $provider->getAuthFactorProviderProperty(self::PROP_HOSTNAME);
|
||||
$usernames = $provider->getAuthFactorProviderProperty(self::PROP_USERNAMES);
|
||||
$enroll = $provider->getAuthFactorProviderProperty(self::PROP_ENROLL);
|
||||
|
||||
$credential_type = PassphrasePasswordCredentialType::CREDENTIAL_TYPE;
|
||||
$provides_type = PassphrasePasswordCredentialType::PROVIDES_TYPE;
|
||||
|
||||
$credentials = id(new PassphraseCredentialQuery())
|
||||
->setViewer($viewer)
|
||||
->withIsDestroyed(false)
|
||||
->withProvidesTypes(array($provides_type))
|
||||
->execute();
|
||||
|
||||
$xaction_hostname =
|
||||
PhabricatorAuthFactorProviderDuoHostnameTransaction::TRANSACTIONTYPE;
|
||||
$xaction_credential =
|
||||
PhabricatorAuthFactorProviderDuoCredentialTransaction::TRANSACTIONTYPE;
|
||||
$xaction_usernames =
|
||||
PhabricatorAuthFactorProviderDuoUsernamesTransaction::TRANSACTIONTYPE;
|
||||
$xaction_enroll =
|
||||
PhabricatorAuthFactorProviderDuoEnrollTransaction::TRANSACTIONTYPE;
|
||||
|
||||
return array(
|
||||
id(new PhabricatorTextEditField())
|
||||
->setLabel(pht('Duo API Hostname'))
|
||||
->setKey('duo.hostname')
|
||||
->setValue($hostname)
|
||||
->setTransactionType($xaction_hostname)
|
||||
->setIsRequired(true),
|
||||
id(new PhabricatorCredentialEditField())
|
||||
->setLabel(pht('Duo API Credential'))
|
||||
->setKey('duo.credential')
|
||||
->setValue($credential_phid)
|
||||
->setTransactionType($xaction_credential)
|
||||
->setCredentialType($credential_type)
|
||||
->setCredentials($credentials),
|
||||
id(new PhabricatorSelectEditField())
|
||||
->setLabel(pht('Duo Username'))
|
||||
->setKey('duo.usernames')
|
||||
->setValue($usernames)
|
||||
->setTransactionType($xaction_usernames)
|
||||
->setOptions(
|
||||
array(
|
||||
'username' => pht('Use Phabricator Username'),
|
||||
'email' => pht('Use Primary Email Address'),
|
||||
)),
|
||||
id(new PhabricatorSelectEditField())
|
||||
->setLabel(pht('Create Accounts'))
|
||||
->setKey('duo.enroll')
|
||||
->setValue($enroll)
|
||||
->setTransactionType($xaction_enroll)
|
||||
->setOptions(
|
||||
array(
|
||||
'deny' => pht('Require Existing Duo Account'),
|
||||
'allow' => pht('Create New Duo Account'),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function processAddFactorForm(
|
||||
PhabricatorAuthFactorProvider $provider,
|
||||
AphrontFormView $form,
|
||||
AphrontRequest $request,
|
||||
PhabricatorUser $user) {
|
||||
|
||||
$token = $this->loadMFASyncToken($provider, $request, $form, $user);
|
||||
|
||||
$enroll = $token->getTemporaryTokenProperty('duo.enroll');
|
||||
$duo_id = $token->getTemporaryTokenProperty('duo.user-id');
|
||||
$duo_uri = $token->getTemporaryTokenProperty('duo.uri');
|
||||
$duo_user = $token->getTemporaryTokenProperty('duo.username');
|
||||
|
||||
$is_external = ($enroll === 'external');
|
||||
$is_auto = ($enroll === 'auto');
|
||||
$is_blocked = ($enroll === 'blocked');
|
||||
|
||||
if (!$token->getIsNewTemporaryToken()) {
|
||||
if ($is_auto) {
|
||||
return $this->newDuoConfig($user, $duo_user);
|
||||
} else if ($is_external || $is_blocked) {
|
||||
$parameters = array(
|
||||
'username' => $duo_user,
|
||||
);
|
||||
|
||||
$result = $this->newDuoFuture($provider)
|
||||
->setMethod('preauth', $parameters)
|
||||
->resolve();
|
||||
|
||||
$result_code = $result['response']['result'];
|
||||
switch ($result_code) {
|
||||
case 'auth':
|
||||
case 'allow':
|
||||
return $this->newDuoConfig($user, $duo_user);
|
||||
case 'enroll':
|
||||
if ($is_blocked) {
|
||||
// We'll render an equivalent static control below, so skip
|
||||
// rendering here. We explicitly don't want to give the user
|
||||
// an enroll workflow.
|
||||
break;
|
||||
}
|
||||
|
||||
$duo_uri = $result['response']['enroll_portal_url'];
|
||||
|
||||
$waiting_icon = id(new PHUIIconView())
|
||||
->setIcon('fa-mobile', 'red');
|
||||
|
||||
$waiting_control = id(new PHUIFormTimerControl())
|
||||
->setIcon($waiting_icon)
|
||||
->setError(pht('Not Complete'))
|
||||
->appendChild(
|
||||
pht(
|
||||
'You have not completed Duo enrollment yet. '.
|
||||
'Complete enrollment, then click continue.'));
|
||||
|
||||
$form->appendControl($waiting_control);
|
||||
break;
|
||||
default:
|
||||
case 'deny':
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$parameters = array(
|
||||
'user_id' => $duo_id,
|
||||
'activation_code' => $duo_uri,
|
||||
);
|
||||
|
||||
$future = $this->newDuoFuture($provider)
|
||||
->setMethod('enroll_status', $parameters);
|
||||
|
||||
$result = $future->resolve();
|
||||
$response = $result['response'];
|
||||
|
||||
switch ($response) {
|
||||
case 'success':
|
||||
return $this->newDuoConfig($user, $duo_user);
|
||||
case 'waiting':
|
||||
$waiting_icon = id(new PHUIIconView())
|
||||
->setIcon('fa-mobile', 'red');
|
||||
|
||||
$waiting_control = id(new PHUIFormTimerControl())
|
||||
->setIcon($waiting_icon)
|
||||
->setError(pht('Not Complete'))
|
||||
->appendChild(
|
||||
pht(
|
||||
'You have not activated this enrollment in the Duo '.
|
||||
'application on your phone yet. Complete activation, then '.
|
||||
'click continue.'));
|
||||
|
||||
$form->appendControl($waiting_control);
|
||||
break;
|
||||
case 'invalid':
|
||||
default:
|
||||
throw new Exception(
|
||||
pht(
|
||||
'This Duo enrollment attempt is invalid or has '.
|
||||
'expired ("%s"). Cancel the workflow and try again.',
|
||||
$response));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($is_blocked) {
|
||||
$blocked_icon = id(new PHUIIconView())
|
||||
->setIcon('fa-times', 'red');
|
||||
|
||||
$blocked_control = id(new PHUIFormTimerControl())
|
||||
->setIcon($blocked_icon)
|
||||
->appendChild(
|
||||
pht(
|
||||
'Your Duo account ("%s") has not completed Duo enrollment. '.
|
||||
'Check your email and complete enrollment to continue.',
|
||||
phutil_tag('strong', array(), $duo_user)));
|
||||
|
||||
$form->appendControl($blocked_control);
|
||||
} else if ($is_auto) {
|
||||
$auto_icon = id(new PHUIIconView())
|
||||
->setIcon('fa-check', 'green');
|
||||
|
||||
$auto_control = id(new PHUIFormTimerControl())
|
||||
->setIcon($auto_icon)
|
||||
->appendChild(
|
||||
pht(
|
||||
'Duo account ("%s") is fully enrolled.',
|
||||
phutil_tag('strong', array(), $duo_user)));
|
||||
|
||||
$form->appendControl($auto_control);
|
||||
} else {
|
||||
$duo_button = phutil_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => $duo_uri,
|
||||
'class' => 'button button-grey',
|
||||
'target' => ($is_external ? '_blank' : null),
|
||||
),
|
||||
pht('Enroll Duo Account: %s', $duo_user));
|
||||
|
||||
$duo_button = phutil_tag(
|
||||
'div',
|
||||
array(
|
||||
'class' => 'mfa-form-enroll-button',
|
||||
),
|
||||
$duo_button);
|
||||
|
||||
if ($is_external) {
|
||||
$form->appendRemarkupInstructions(
|
||||
pht(
|
||||
'Complete enrolling your phone with Duo:'));
|
||||
|
||||
$form->appendControl(
|
||||
id(new AphrontFormMarkupControl())
|
||||
->setValue($duo_button));
|
||||
} else {
|
||||
|
||||
$form->appendRemarkupInstructions(
|
||||
pht(
|
||||
'Scan this QR code with the Duo application on your mobile '.
|
||||
'phone:'));
|
||||
|
||||
|
||||
$qr_code = $this->newQRCode($duo_uri);
|
||||
$form->appendChild($qr_code);
|
||||
|
||||
$form->appendRemarkupInstructions(
|
||||
pht(
|
||||
'If you are currently using your phone to view this page, '.
|
||||
'click this button to open the Duo application:'));
|
||||
|
||||
$form->appendControl(
|
||||
id(new AphrontFormMarkupControl())
|
||||
->setValue($duo_button));
|
||||
}
|
||||
|
||||
$form->appendRemarkupInstructions(
|
||||
pht(
|
||||
'Once you have completed setup on your phone, click continue.'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function newMFASyncTokenProperties(
|
||||
PhabricatorAuthFactorProvider $provider,
|
||||
PhabricatorUser $user) {
|
||||
|
||||
$duo_user = $this->getDuoUsername($provider, $user);
|
||||
|
||||
// Duo automatically normalizes usernames to lowercase. Just do that here
|
||||
// so that our value agrees more closely with Duo.
|
||||
$duo_user = phutil_utf8_strtolower($duo_user);
|
||||
|
||||
$parameters = array(
|
||||
'username' => $duo_user,
|
||||
);
|
||||
|
||||
$result = $this->newDuoFuture($provider)
|
||||
->setMethod('preauth', $parameters)
|
||||
->resolve();
|
||||
|
||||
$external_uri = null;
|
||||
$result_code = $result['response']['result'];
|
||||
switch ($result_code) {
|
||||
case 'auth':
|
||||
case 'allow':
|
||||
// If the user already has a Duo account, they don't need to do
|
||||
// anything.
|
||||
return array(
|
||||
'duo.enroll' => 'auto',
|
||||
'duo.username' => $duo_user,
|
||||
);
|
||||
case 'enroll':
|
||||
if (!$this->shouldAllowDuoEnrollment($provider)) {
|
||||
return array(
|
||||
'duo.enroll' => 'blocked',
|
||||
'duo.username' => $duo_user,
|
||||
);
|
||||
}
|
||||
|
||||
$external_uri = $result['response']['enroll_portal_url'];
|
||||
|
||||
// Otherwise, enrollment is permitted so we're going to continue.
|
||||
break;
|
||||
default:
|
||||
case 'deny':
|
||||
return $this->newResult()
|
||||
->setIsError(true)
|
||||
->setErrorMessage(
|
||||
pht('Your account is not permitted to access this system.'));
|
||||
}
|
||||
|
||||
// Duo's "/enroll" API isn't repeatable for the same username. If we're
|
||||
// the first call, great: we can do inline enrollment, which is way more
|
||||
// user friendly. Otherwise, we have to send the user on an adventure.
|
||||
|
||||
$parameters = array(
|
||||
'username' => $duo_user,
|
||||
'valid_secs' => phutil_units('1 hour in seconds'),
|
||||
);
|
||||
|
||||
try {
|
||||
$result = $this->newDuoFuture($provider)
|
||||
->setMethod('enroll', $parameters)
|
||||
->resolve();
|
||||
} catch (HTTPFutureHTTPResponseStatus $ex) {
|
||||
return array(
|
||||
'duo.enroll' => 'external',
|
||||
'duo.username' => $duo_user,
|
||||
'duo.uri' => $external_uri,
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'duo.enroll' => 'inline',
|
||||
'duo.uri' => $result['response']['activation_code'],
|
||||
'duo.username' => $duo_user,
|
||||
'duo.user-id' => $result['response']['user_id'],
|
||||
);
|
||||
}
|
||||
|
||||
protected function newIssuedChallenges(
|
||||
PhabricatorAuthFactorConfig $config,
|
||||
PhabricatorUser $viewer,
|
||||
array $challenges) {
|
||||
|
||||
// If we already issued a valid challenge for this workflow and session,
|
||||
// don't issue a new one.
|
||||
|
||||
$challenge = $this->getChallengeForCurrentContext(
|
||||
$config,
|
||||
$viewer,
|
||||
$challenges);
|
||||
if ($challenge) {
|
||||
return array();
|
||||
}
|
||||
|
||||
if (!$this->hasCSRF($config)) {
|
||||
return $this->newResult()
|
||||
->setIsContinue(true)
|
||||
->setErrorMessage(
|
||||
pht(
|
||||
'An authorization request will be pushed to the Duo '.
|
||||
'application on your phone.'));
|
||||
}
|
||||
|
||||
$provider = $config->getFactorProvider();
|
||||
|
||||
// Otherwise, issue a new challenge.
|
||||
$duo_user = (string)$config->getAuthFactorConfigProperty('duo.username');
|
||||
|
||||
$parameters = array(
|
||||
'username' => $duo_user,
|
||||
);
|
||||
|
||||
$response = $this->newDuoFuture($provider)
|
||||
->setMethod('preauth', $parameters)
|
||||
->resolve();
|
||||
$response = $response['response'];
|
||||
|
||||
$next_step = $response['result'];
|
||||
$status_message = $response['status_msg'];
|
||||
switch ($next_step) {
|
||||
case 'auth':
|
||||
// We're good to go.
|
||||
break;
|
||||
case 'allow':
|
||||
// Duo is telling us to bypass MFA. For now, refuse.
|
||||
return $this->newResult()
|
||||
->setIsError(true)
|
||||
->setErrorMessage(
|
||||
pht(
|
||||
'Duo is not requiring a challenge, which defeats the '.
|
||||
'purpose of MFA. Duo must be configured to challenge you.'));
|
||||
case 'enroll':
|
||||
return $this->newResult()
|
||||
->setIsError(true)
|
||||
->setErrorMessage(
|
||||
pht(
|
||||
'Your Duo account ("%s") requires enrollment. Contact your '.
|
||||
'Duo administrator for help. Duo status message: %s',
|
||||
$duo_user,
|
||||
$status_message));
|
||||
case 'deny':
|
||||
default:
|
||||
return $this->newResult()
|
||||
->setIsError(true)
|
||||
->setErrorMessage(
|
||||
pht(
|
||||
'Duo has denied you access. Duo status message ("%s"): %s',
|
||||
$next_step,
|
||||
$status_message));
|
||||
}
|
||||
|
||||
$has_push = false;
|
||||
$devices = $response['devices'];
|
||||
foreach ($devices as $device) {
|
||||
$capabilities = array_fuse($device['capabilities']);
|
||||
if (isset($capabilities['push'])) {
|
||||
$has_push = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$has_push) {
|
||||
return $this->newResult()
|
||||
->setIsError(true)
|
||||
->setErrorMessage(
|
||||
pht(
|
||||
'This factor has been removed from your device, so Phabricator '.
|
||||
'can not send you a challenge. To continue, an administrator '.
|
||||
'must strip this factor from your account.'));
|
||||
}
|
||||
|
||||
$push_info = array(
|
||||
pht('Domain') => $this->getInstallDisplayName(),
|
||||
);
|
||||
foreach ($push_info as $k => $v) {
|
||||
$push_info[$k] = rawurlencode($k).'='.rawurlencode($v);
|
||||
}
|
||||
$push_info = implode('&', $push_info);
|
||||
|
||||
$parameters = array(
|
||||
'username' => $duo_user,
|
||||
'factor' => 'push',
|
||||
'async' => '1',
|
||||
|
||||
// Duo allows us to specify a device, or to pass "auto" to have it pick
|
||||
// the first one. For now, just let it pick.
|
||||
'device' => 'auto',
|
||||
|
||||
// This is a hard-coded prefix for the word "... request" in the Duo UI,
|
||||
// which defaults to "Login". We could pass richer information from
|
||||
// workflows here, but it's not very flexible anyway.
|
||||
'type' => 'Authentication',
|
||||
|
||||
'display_username' => $viewer->getUsername(),
|
||||
'pushinfo' => $push_info,
|
||||
);
|
||||
|
||||
$result = $this->newDuoFuture($provider)
|
||||
->setMethod('auth', $parameters)
|
||||
->resolve();
|
||||
|
||||
$duo_xaction = $result['response']['txid'];
|
||||
|
||||
// The Duo push timeout is 60 seconds. Set our challenge to expire slightly
|
||||
// more quickly so that we'll re-issue a new challenge before Duo times out.
|
||||
// This should keep users away from a dead-end where they can't respond to
|
||||
// Duo but Phabricator won't issue a new challenge yet.
|
||||
$ttl_seconds = 55;
|
||||
|
||||
return array(
|
||||
$this->newChallenge($config, $viewer)
|
||||
->setChallengeKey($duo_xaction)
|
||||
->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds),
|
||||
);
|
||||
}
|
||||
|
||||
protected function newResultFromIssuedChallenges(
|
||||
PhabricatorAuthFactorConfig $config,
|
||||
PhabricatorUser $viewer,
|
||||
array $challenges) {
|
||||
|
||||
$challenge = $this->getChallengeForCurrentContext(
|
||||
$config,
|
||||
$viewer,
|
||||
$challenges);
|
||||
|
||||
if ($challenge->getIsAnsweredChallenge()) {
|
||||
return $this->newResult()
|
||||
->setAnsweredChallenge($challenge);
|
||||
}
|
||||
|
||||
$provider = $config->getFactorProvider();
|
||||
$duo_xaction = $challenge->getChallengeKey();
|
||||
|
||||
$parameters = array(
|
||||
'txid' => $duo_xaction,
|
||||
);
|
||||
|
||||
// This endpoint always long-polls, so use a timeout to force it to act
|
||||
// more asynchronously.
|
||||
try {
|
||||
$result = $this->newDuoFuture($provider)
|
||||
->setHTTPMethod('GET')
|
||||
->setMethod('auth_status', $parameters)
|
||||
->setTimeout(5)
|
||||
->resolve();
|
||||
|
||||
$state = $result['response']['result'];
|
||||
$status = $result['response']['status'];
|
||||
} catch (HTTPFutureCURLResponseStatus $exception) {
|
||||
if ($exception->isTimeout()) {
|
||||
$state = 'waiting';
|
||||
$status = 'poll';
|
||||
} else {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
$now = PhabricatorTime::getNow();
|
||||
|
||||
switch ($state) {
|
||||
case 'allow':
|
||||
$ttl = PhabricatorTime::getNow()
|
||||
+ phutil_units('15 minutes in seconds');
|
||||
|
||||
$challenge
|
||||
->markChallengeAsAnswered($ttl);
|
||||
|
||||
return $this->newResult()
|
||||
->setAnsweredChallenge($challenge);
|
||||
case 'waiting':
|
||||
// No result yet, we'll render a default state later on.
|
||||
break;
|
||||
default:
|
||||
case 'deny':
|
||||
if ($status === 'timeout') {
|
||||
return $this->newResult()
|
||||
->setIsError(true)
|
||||
->setErrorMessage(
|
||||
pht(
|
||||
'This request has timed out because you took too long to '.
|
||||
'respond.'));
|
||||
} else {
|
||||
$wait_duration = ($challenge->getChallengeTTL() - $now) + 1;
|
||||
|
||||
return $this->newResult()
|
||||
->setIsWait(true)
|
||||
->setErrorMessage(
|
||||
pht(
|
||||
'You denied this request. Wait %s second(s) to try again.',
|
||||
new PhutilNumber($wait_duration)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function renderValidateFactorForm(
|
||||
PhabricatorAuthFactorConfig $config,
|
||||
AphrontFormView $form,
|
||||
PhabricatorUser $viewer,
|
||||
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'))
|
||||
->setCaption(pht('Factor Name: %s', $config->getFactorName()));
|
||||
|
||||
$form->appendChild($control);
|
||||
}
|
||||
|
||||
public function getRequestHasChallengeResponse(
|
||||
PhabricatorAuthFactorConfig $config,
|
||||
AphrontRequest $request) {
|
||||
$value = $this->getChallengeResponseFromRequest($config, $request);
|
||||
return (bool)strlen($value);
|
||||
}
|
||||
|
||||
protected function newResultFromChallengeResponse(
|
||||
PhabricatorAuthFactorConfig $config,
|
||||
PhabricatorUser $viewer,
|
||||
AphrontRequest $request,
|
||||
array $challenges) {
|
||||
|
||||
$challenge = $this->getChallengeForCurrentContext(
|
||||
$config,
|
||||
$viewer,
|
||||
$challenges);
|
||||
|
||||
$code = $this->getChallengeResponseFromRequest(
|
||||
$config,
|
||||
$request);
|
||||
|
||||
$result = $this->newResult()
|
||||
->setValue($code);
|
||||
|
||||
if ($challenge->getIsAnsweredChallenge()) {
|
||||
return $result->setAnsweredChallenge($challenge);
|
||||
}
|
||||
|
||||
if (phutil_hashes_are_identical($code, $challenge->getChallengeKey())) {
|
||||
$ttl = PhabricatorTime::getNow() + phutil_units('15 minutes in seconds');
|
||||
|
||||
$challenge
|
||||
->markChallengeAsAnswered($ttl);
|
||||
|
||||
return $result->setAnsweredChallenge($challenge);
|
||||
}
|
||||
|
||||
if (strlen($code)) {
|
||||
$error_message = pht('Invalid');
|
||||
} else {
|
||||
$error_message = pht('Required');
|
||||
}
|
||||
|
||||
$result->setErrorMessage($error_message);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function newDuoFuture(PhabricatorAuthFactorProvider $provider) {
|
||||
$credential_phid = $provider->getAuthFactorProviderProperty(
|
||||
self::PROP_CREDENTIAL);
|
||||
|
||||
$omnipotent = PhabricatorUser::getOmnipotentUser();
|
||||
|
||||
$credential = id(new PassphraseCredentialQuery())
|
||||
->setViewer($omnipotent)
|
||||
->withPHIDs(array($credential_phid))
|
||||
->needSecrets(true)
|
||||
->executeOne();
|
||||
if (!$credential) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Unable to load Duo API credential ("%s").',
|
||||
$credential_phid));
|
||||
}
|
||||
|
||||
$duo_key = $credential->getUsername();
|
||||
$duo_secret = $credential->getSecret();
|
||||
if (!$duo_secret) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Duo API credential ("%s") has no secret key.',
|
||||
$credential_phid));
|
||||
}
|
||||
|
||||
$duo_host = $provider->getAuthFactorProviderProperty(
|
||||
self::PROP_HOSTNAME);
|
||||
self::requireDuoAPIHostname($duo_host);
|
||||
|
||||
return id(new PhabricatorDuoFuture())
|
||||
->setIntegrationKey($duo_key)
|
||||
->setSecretKey($duo_secret)
|
||||
->setAPIHostname($duo_host)
|
||||
->setTimeout(10)
|
||||
->setHTTPMethod('POST');
|
||||
}
|
||||
|
||||
private function getDuoUsername(
|
||||
PhabricatorAuthFactorProvider $provider,
|
||||
PhabricatorUser $user) {
|
||||
|
||||
$mode = $provider->getAuthFactorProviderProperty(self::PROP_USERNAMES);
|
||||
switch ($mode) {
|
||||
case 'username':
|
||||
return $user->getUsername();
|
||||
case 'email':
|
||||
return $user->loadPrimaryEmailAddress();
|
||||
default:
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Duo username pairing mode ("%s") is not supported.',
|
||||
$mode));
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldAllowDuoEnrollment(
|
||||
PhabricatorAuthFactorProvider $provider) {
|
||||
|
||||
$mode = $provider->getAuthFactorProviderProperty(self::PROP_ENROLL);
|
||||
switch ($mode) {
|
||||
case 'deny':
|
||||
return false;
|
||||
case 'allow':
|
||||
return true;
|
||||
default:
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Duo enrollment mode ("%s") is not supported.',
|
||||
$mode));
|
||||
}
|
||||
}
|
||||
|
||||
private function newDuoConfig(PhabricatorUser $user, $duo_user) {
|
||||
$config_properties = array(
|
||||
'duo.username' => $duo_user,
|
||||
);
|
||||
|
||||
$config = $this->newConfigForUser($user)
|
||||
->setFactorName(pht('Duo (%s)', $duo_user))
|
||||
->setProperties($config_properties);
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
public static function requireDuoAPIHostname($hostname) {
|
||||
if (preg_match('/\.duosecurity\.com\z/', $hostname)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Duo API hostname ("%s") is invalid, hostname must be '.
|
||||
'"*.duosecurity.com".',
|
||||
$hostname));
|
||||
}
|
||||
|
||||
}
|
|
@ -140,7 +140,7 @@ final class PhabricatorSMSAuthFactor
|
|||
AphrontRequest $request,
|
||||
PhabricatorUser $user) {
|
||||
|
||||
$token = $this->loadMFASyncToken($request, $form, $user);
|
||||
$token = $this->loadMFASyncToken($provider, $request, $form, $user);
|
||||
$code = $request->getStr('sms.code');
|
||||
|
||||
$e_code = true;
|
||||
|
@ -364,7 +364,10 @@ final class PhabricatorSMSAuthFactor
|
|||
return head($contact_numbers);
|
||||
}
|
||||
|
||||
protected function newMFASyncTokenProperties(PhabricatorUser $user) {
|
||||
protected function newMFASyncTokenProperties(
|
||||
PhabricatorAuthFactorProvider $providerr,
|
||||
PhabricatorUser $user) {
|
||||
|
||||
$sms_code = $this->newSMSChallengeCode();
|
||||
|
||||
$envelope = new PhutilOpaqueEnvelope($sms_code);
|
||||
|
|
|
@ -58,6 +58,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
|
|||
PhabricatorUser $user) {
|
||||
|
||||
$sync_token = $this->loadMFASyncToken(
|
||||
$provider,
|
||||
$request,
|
||||
$form,
|
||||
$user);
|
||||
|
@ -440,7 +441,9 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
|
|||
return null;
|
||||
}
|
||||
|
||||
protected function newMFASyncTokenProperties(PhabricatorUser $user) {
|
||||
protected function newMFASyncTokenProperties(
|
||||
PhabricatorAuthFactorProvider $providerr,
|
||||
PhabricatorUser $user) {
|
||||
return array(
|
||||
'secret' => self::generateNewTOTPKey(),
|
||||
);
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAuthFactorProviderDuoCredentialTransaction
|
||||
extends PhabricatorAuthFactorProviderTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'duo.credential';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
$key = PhabricatorDuoAuthFactor::PROP_CREDENTIAL;
|
||||
return $object->getAuthFactorProviderProperty($key);
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$key = PhabricatorDuoAuthFactor::PROP_CREDENTIAL;
|
||||
$object->setAuthFactorProviderProperty($key, $value);
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
return pht(
|
||||
'%s changed the credential for this provider from %s to %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderOldHandle(),
|
||||
$this->renderNewHandle());
|
||||
}
|
||||
|
||||
public function validateTransactions($object, array $xactions) {
|
||||
$actor = $this->getActor();
|
||||
$errors = array();
|
||||
|
||||
$old_value = $this->generateOldValue($object);
|
||||
if ($this->isEmptyTextTransaction($old_value, $xactions)) {
|
||||
$errors[] = $this->newRequiredError(
|
||||
pht('Duo providers must have an API credential.'));
|
||||
}
|
||||
|
||||
foreach ($xactions as $xaction) {
|
||||
$new_value = $xaction->getNewValue();
|
||||
|
||||
if (!strlen($new_value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($new_value === $old_value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$credential = id(new PassphraseCredentialQuery())
|
||||
->setViewer($actor)
|
||||
->withIsDestroyed(false)
|
||||
->withPHIDs(array($new_value))
|
||||
->executeOne();
|
||||
if (!$credential) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht(
|
||||
'Credential ("%s") is not valid.',
|
||||
$new_value),
|
||||
$xaction);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAuthFactorProviderDuoEnrollTransaction
|
||||
extends PhabricatorAuthFactorProviderTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'duo.enroll';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
$key = PhabricatorDuoAuthFactor::PROP_ENROLL;
|
||||
return $object->getAuthFactorProviderProperty($key);
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$key = PhabricatorDuoAuthFactor::PROP_ENROLL;
|
||||
$object->setAuthFactorProviderProperty($key, $value);
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
return pht(
|
||||
'%s changed the enrollment policy for this provider from %s to %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderOldValue(),
|
||||
$this->renderNewValue());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAuthFactorProviderDuoHostnameTransaction
|
||||
extends PhabricatorAuthFactorProviderTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'duo.hostname';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
$key = PhabricatorDuoAuthFactor::PROP_HOSTNAME;
|
||||
return $object->getAuthFactorProviderProperty($key);
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$key = PhabricatorDuoAuthFactor::PROP_HOSTNAME;
|
||||
$object->setAuthFactorProviderProperty($key, $value);
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
return pht(
|
||||
'%s changed the hostname for this provider from %s to %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderOldValue(),
|
||||
$this->renderNewValue());
|
||||
}
|
||||
|
||||
public function validateTransactions($object, array $xactions) {
|
||||
$errors = array();
|
||||
|
||||
$old_value = $this->generateOldValue($object);
|
||||
if ($this->isEmptyTextTransaction($old_value, $xactions)) {
|
||||
$errors[] = $this->newRequiredError(
|
||||
pht('Duo providers must have an API hostname.'));
|
||||
}
|
||||
|
||||
foreach ($xactions as $xaction) {
|
||||
$new_value = $xaction->getNewValue();
|
||||
|
||||
if (!strlen($new_value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($new_value === $old_value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
PhabricatorDuoAuthFactor::requireDuoAPIHostname($new_value);
|
||||
} catch (Exception $ex) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
$ex->getMessage(),
|
||||
$xaction);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAuthFactorProviderDuoUsernamesTransaction
|
||||
extends PhabricatorAuthFactorProviderTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'duo.usernames';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
$key = PhabricatorDuoAuthFactor::PROP_USERNAMES;
|
||||
return $object->getAuthFactorProviderProperty($key);
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$key = PhabricatorDuoAuthFactor::PROP_USERNAMES;
|
||||
$object->setAuthFactorProviderProperty($key, $value);
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
return pht(
|
||||
'%s changed the username policy for this provider from %s to %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderOldValue(),
|
||||
$this->renderNewValue());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorCredentialEditField
|
||||
extends PhabricatorEditField {
|
||||
|
||||
private $credentialType;
|
||||
private $credentials;
|
||||
|
||||
public function setCredentialType($credential_type) {
|
||||
$this->credentialType = $credential_type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCredentialType() {
|
||||
return $this->credentialType;
|
||||
}
|
||||
|
||||
public function setCredentials(array $credentials) {
|
||||
$this->credentials = $credentials;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCredentials() {
|
||||
return $this->credentials;
|
||||
}
|
||||
|
||||
protected function newControl() {
|
||||
$control = id(new PassphraseCredentialControl())
|
||||
->setCredentialType($this->getCredentialType())
|
||||
->setOptions($this->getCredentials());
|
||||
|
||||
return $control;
|
||||
}
|
||||
|
||||
protected function newHTTPParameterType() {
|
||||
return new AphrontPHIDHTTPParameterType();
|
||||
}
|
||||
|
||||
protected function newConduitParameterType() {
|
||||
return new ConduitPHIDParameterType();
|
||||
}
|
||||
|
||||
}
|
|
@ -28,7 +28,6 @@ final class PhabricatorSpaceEditField
|
|||
return new ConduitPHIDParameterType();
|
||||
}
|
||||
|
||||
|
||||
public function shouldReadValueFromRequest() {
|
||||
return $this->getPolicyField()->shouldReadValueFromRequest();
|
||||
}
|
||||
|
|
|
@ -109,6 +109,17 @@ an authorization code to enter into the prompt.
|
|||
details, see: <https://phurl.io/u/sms>.
|
||||
|
||||
|
||||
Factor: Duo
|
||||
===========
|
||||
|
||||
This factor supports integration with [[ https://duo.com/ | Duo Security ]], a
|
||||
third-party authentication service popular with enterprises that have a lot of
|
||||
policies to enforce.
|
||||
|
||||
To use Duo, you'll install the Duo application on your phone. When you try
|
||||
to take a sensitive action, you'll be asked to confirm it in the application.
|
||||
|
||||
|
||||
Administration: Configuration
|
||||
=============================
|
||||
|
||||
|
|
Loading…
Reference in a new issue