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:
		@@ -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
 | 
			
		||||
=============================
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user