mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-26 16:52:41 +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',
|
'PhabricatorAuthFactorConfigQuery' => 'applications/auth/query/PhabricatorAuthFactorConfigQuery.php',
|
||||||
'PhabricatorAuthFactorProvider' => 'applications/auth/storage/PhabricatorAuthFactorProvider.php',
|
'PhabricatorAuthFactorProvider' => 'applications/auth/storage/PhabricatorAuthFactorProvider.php',
|
||||||
'PhabricatorAuthFactorProviderController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.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',
|
'PhabricatorAuthFactorProviderEditController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php',
|
||||||
'PhabricatorAuthFactorProviderEditEngine' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php',
|
'PhabricatorAuthFactorProviderEditEngine' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php',
|
||||||
'PhabricatorAuthFactorProviderEditor' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditor.php',
|
'PhabricatorAuthFactorProviderEditor' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditor.php',
|
||||||
|
@ -2800,6 +2804,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorCountdownTransactionType' => 'applications/countdown/xaction/PhabricatorCountdownTransactionType.php',
|
'PhabricatorCountdownTransactionType' => 'applications/countdown/xaction/PhabricatorCountdownTransactionType.php',
|
||||||
'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php',
|
'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php',
|
||||||
'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php',
|
'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php',
|
||||||
|
'PhabricatorCredentialEditField' => 'applications/transactions/editfield/PhabricatorCredentialEditField.php',
|
||||||
'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php',
|
'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php',
|
||||||
'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php',
|
'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php',
|
||||||
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php',
|
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php',
|
||||||
|
@ -2986,6 +2991,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorDraftEngine' => 'applications/transactions/draft/PhabricatorDraftEngine.php',
|
'PhabricatorDraftEngine' => 'applications/transactions/draft/PhabricatorDraftEngine.php',
|
||||||
'PhabricatorDraftInterface' => 'applications/transactions/draft/PhabricatorDraftInterface.php',
|
'PhabricatorDraftInterface' => 'applications/transactions/draft/PhabricatorDraftInterface.php',
|
||||||
'PhabricatorDrydockApplication' => 'applications/drydock/application/PhabricatorDrydockApplication.php',
|
'PhabricatorDrydockApplication' => 'applications/drydock/application/PhabricatorDrydockApplication.php',
|
||||||
|
'PhabricatorDuoAuthFactor' => 'applications/auth/factor/PhabricatorDuoAuthFactor.php',
|
||||||
'PhabricatorDuoFuture' => 'applications/auth/future/PhabricatorDuoFuture.php',
|
'PhabricatorDuoFuture' => 'applications/auth/future/PhabricatorDuoFuture.php',
|
||||||
'PhabricatorEdgeChangeRecord' => 'infrastructure/edges/util/PhabricatorEdgeChangeRecord.php',
|
'PhabricatorEdgeChangeRecord' => 'infrastructure/edges/util/PhabricatorEdgeChangeRecord.php',
|
||||||
'PhabricatorEdgeChangeRecordTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeChangeRecordTestCase.php',
|
'PhabricatorEdgeChangeRecordTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeChangeRecordTestCase.php',
|
||||||
|
@ -7958,6 +7964,10 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorEditEngineMFAInterface',
|
'PhabricatorEditEngineMFAInterface',
|
||||||
),
|
),
|
||||||
'PhabricatorAuthFactorProviderController' => 'PhabricatorAuthProviderController',
|
'PhabricatorAuthFactorProviderController' => 'PhabricatorAuthProviderController',
|
||||||
|
'PhabricatorAuthFactorProviderDuoCredentialTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
|
||||||
|
'PhabricatorAuthFactorProviderDuoEnrollTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
|
||||||
|
'PhabricatorAuthFactorProviderDuoHostnameTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
|
||||||
|
'PhabricatorAuthFactorProviderDuoUsernamesTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
|
||||||
'PhabricatorAuthFactorProviderEditController' => 'PhabricatorAuthFactorProviderController',
|
'PhabricatorAuthFactorProviderEditController' => 'PhabricatorAuthFactorProviderController',
|
||||||
'PhabricatorAuthFactorProviderEditEngine' => 'PhabricatorEditEngine',
|
'PhabricatorAuthFactorProviderEditEngine' => 'PhabricatorEditEngine',
|
||||||
'PhabricatorAuthFactorProviderEditor' => 'PhabricatorApplicationTransactionEditor',
|
'PhabricatorAuthFactorProviderEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||||
|
@ -8633,6 +8643,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorCountdownTransactionType' => 'PhabricatorModularTransactionType',
|
'PhabricatorCountdownTransactionType' => 'PhabricatorModularTransactionType',
|
||||||
'PhabricatorCountdownView' => 'AphrontView',
|
'PhabricatorCountdownView' => 'AphrontView',
|
||||||
'PhabricatorCountdownViewController' => 'PhabricatorCountdownController',
|
'PhabricatorCountdownViewController' => 'PhabricatorCountdownController',
|
||||||
|
'PhabricatorCredentialEditField' => 'PhabricatorEditField',
|
||||||
'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery',
|
'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery',
|
||||||
'PhabricatorCustomField' => 'Phobject',
|
'PhabricatorCustomField' => 'Phobject',
|
||||||
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
|
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
|
||||||
|
@ -8837,6 +8848,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorDraftDAO' => 'PhabricatorLiskDAO',
|
'PhabricatorDraftDAO' => 'PhabricatorLiskDAO',
|
||||||
'PhabricatorDraftEngine' => 'Phobject',
|
'PhabricatorDraftEngine' => 'Phobject',
|
||||||
'PhabricatorDrydockApplication' => 'PhabricatorApplication',
|
'PhabricatorDrydockApplication' => 'PhabricatorApplication',
|
||||||
|
'PhabricatorDuoAuthFactor' => 'PhabricatorAuthFactor',
|
||||||
'PhabricatorDuoFuture' => 'FutureProxy',
|
'PhabricatorDuoFuture' => 'FutureProxy',
|
||||||
'PhabricatorEdgeChangeRecord' => 'Phobject',
|
'PhabricatorEdgeChangeRecord' => 'Phobject',
|
||||||
'PhabricatorEdgeChangeRecordTestCase' => 'PhabricatorTestCase',
|
'PhabricatorEdgeChangeRecordTestCase' => 'PhabricatorTestCase',
|
||||||
|
|
|
@ -93,11 +93,12 @@ final class PhabricatorAuthFactorProviderEditEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function buildCustomEditFields($object) {
|
protected function buildCustomEditFields($object) {
|
||||||
$factor_name = $object->getFactor()->getFactorName();
|
$factor = $object->getFactor();
|
||||||
|
$factor_name = $factor->getFactorName();
|
||||||
|
|
||||||
$status_map = PhabricatorAuthFactorProviderStatus::getMap();
|
$status_map = PhabricatorAuthFactorProviderStatus::getMap();
|
||||||
|
|
||||||
return array(
|
$fields = array(
|
||||||
id(new PhabricatorStaticEditField())
|
id(new PhabricatorStaticEditField())
|
||||||
->setKey('displayType')
|
->setKey('displayType')
|
||||||
->setLabel(pht('Factor Type'))
|
->setLabel(pht('Factor Type'))
|
||||||
|
@ -120,6 +121,13 @@ final class PhabricatorAuthFactorProviderEditEngine
|
||||||
->setValue($object->getStatus())
|
->setValue($object->getStatus())
|
||||||
->setOptions($status_map),
|
->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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function newEditEngineFields(
|
||||||
|
PhabricatorEditEngine $engine,
|
||||||
|
PhabricatorAuthFactorProvider $provider) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is this a factor which depends on the user's contact number?
|
* 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(
|
final protected function loadMFASyncToken(
|
||||||
|
PhabricatorAuthFactorProvider $provider,
|
||||||
AphrontRequest $request,
|
AphrontRequest $request,
|
||||||
AphrontFormView $form,
|
AphrontFormView $form,
|
||||||
PhabricatorUser $user) {
|
PhabricatorUser $user) {
|
||||||
|
@ -397,7 +404,9 @@ abstract class PhabricatorAuthFactor extends Phobject {
|
||||||
->setTokenCode($sync_key_digest)
|
->setTokenCode($sync_key_digest)
|
||||||
->setTokenExpires($now + $sync_ttl);
|
->setTokenExpires($now + $sync_ttl);
|
||||||
|
|
||||||
$properties = $this->newMFASyncTokenProperties($user);
|
$properties = $this->newMFASyncTokenProperties(
|
||||||
|
$provider,
|
||||||
|
$user);
|
||||||
|
|
||||||
foreach ($properties as $key => $value) {
|
foreach ($properties as $key => $value) {
|
||||||
$sync_token->setTemporaryTokenProperty($key, $value);
|
$sync_token->setTemporaryTokenProperty($key, $value);
|
||||||
|
@ -411,7 +420,9 @@ abstract class PhabricatorAuthFactor extends Phobject {
|
||||||
return $sync_token;
|
return $sync_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function newMFASyncTokenProperties(PhabricatorUser $user) {
|
protected function newMFASyncTokenProperties(
|
||||||
|
PhabricatorAuthFactorProvider $provider,
|
||||||
|
PhabricatorUser $user) {
|
||||||
return array();
|
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,
|
AphrontRequest $request,
|
||||||
PhabricatorUser $user) {
|
PhabricatorUser $user) {
|
||||||
|
|
||||||
$token = $this->loadMFASyncToken($request, $form, $user);
|
$token = $this->loadMFASyncToken($provider, $request, $form, $user);
|
||||||
$code = $request->getStr('sms.code');
|
$code = $request->getStr('sms.code');
|
||||||
|
|
||||||
$e_code = true;
|
$e_code = true;
|
||||||
|
@ -364,7 +364,10 @@ final class PhabricatorSMSAuthFactor
|
||||||
return head($contact_numbers);
|
return head($contact_numbers);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function newMFASyncTokenProperties(PhabricatorUser $user) {
|
protected function newMFASyncTokenProperties(
|
||||||
|
PhabricatorAuthFactorProvider $providerr,
|
||||||
|
PhabricatorUser $user) {
|
||||||
|
|
||||||
$sms_code = $this->newSMSChallengeCode();
|
$sms_code = $this->newSMSChallengeCode();
|
||||||
|
|
||||||
$envelope = new PhutilOpaqueEnvelope($sms_code);
|
$envelope = new PhutilOpaqueEnvelope($sms_code);
|
||||||
|
|
|
@ -58,6 +58,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
|
||||||
PhabricatorUser $user) {
|
PhabricatorUser $user) {
|
||||||
|
|
||||||
$sync_token = $this->loadMFASyncToken(
|
$sync_token = $this->loadMFASyncToken(
|
||||||
|
$provider,
|
||||||
$request,
|
$request,
|
||||||
$form,
|
$form,
|
||||||
$user);
|
$user);
|
||||||
|
@ -440,7 +441,9 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function newMFASyncTokenProperties(PhabricatorUser $user) {
|
protected function newMFASyncTokenProperties(
|
||||||
|
PhabricatorAuthFactorProvider $providerr,
|
||||||
|
PhabricatorUser $user) {
|
||||||
return array(
|
return array(
|
||||||
'secret' => self::generateNewTOTPKey(),
|
'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();
|
return new ConduitPHIDParameterType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function shouldReadValueFromRequest() {
|
public function shouldReadValueFromRequest() {
|
||||||
return $this->getPolicyField()->shouldReadValueFromRequest();
|
return $this->getPolicyField()->shouldReadValueFromRequest();
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,6 +109,17 @@ an authorization code to enter into the prompt.
|
||||||
details, see: <https://phurl.io/u/sms>.
|
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
|
Administration: Configuration
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue