1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-23 07:12:41 +01:00

Require MFA implementations to return a formal result object when validating factors

Summary:
Ref T13222. See PHI873. Currently, MFA implementations return this weird sort of ad-hoc dictionary from validation, which is later used to render form/control stuff.

I want to make this more formal to handle token reuse / session binding cases, and let MFA factors share more code around challenges. Formalize this into a proper object instead of an ad-hoc bundle of properties.

Test Plan:
  - Answered a TOTP MFA prompt wrong (nothing, bad value).
  - Answered a TOTP MFA prompt properly.
  - Added new TOTP MFA, survived enrollment.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13222

Differential Revision: https://secure.phabricator.com/D19885
This commit is contained in:
epriestley 2018-12-13 12:34:12 -08:00
parent 54b952df5d
commit c731508d74
6 changed files with 82 additions and 29 deletions

View file

@ -2198,6 +2198,7 @@ phutil_register_library_map(array(
'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php', 'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php',
'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php', 'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php',
'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php', 'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php',
'PhabricatorAuthFactorResult' => 'applications/auth/factor/PhabricatorAuthFactorResult.php',
'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php', 'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php',
'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php', 'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php',
'PhabricatorAuthHMACKey' => 'applications/auth/storage/PhabricatorAuthHMACKey.php', 'PhabricatorAuthHMACKey' => 'applications/auth/storage/PhabricatorAuthHMACKey.php',
@ -7833,6 +7834,7 @@ phutil_register_library_map(array(
'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthFactor' => 'Phobject', 'PhabricatorAuthFactor' => 'Phobject',
'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO', 'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO',
'PhabricatorAuthFactorResult' => 'Phobject',
'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase', 'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthFinishController' => 'PhabricatorAuthController', 'PhabricatorAuthFinishController' => 'PhabricatorAuthController',
'PhabricatorAuthHMACKey' => 'PhabricatorAuthDAO', 'PhabricatorAuthHMACKey' => 'PhabricatorAuthDAO',

View file

@ -496,14 +496,25 @@ final class PhabricatorAuthSessionEngine extends Phobject {
$id = $factor->getID(); $id = $factor->getID();
$impl = $factor->requireImplementation(); $impl = $factor->requireImplementation();
$validation_results[$id] = $impl->processValidateFactorForm( $validation_result = $impl->processValidateFactorForm(
$factor, $factor,
$viewer, $viewer,
$request); $request);
if (!$impl->isFactorValid($factor, $validation_results[$id])) { if (!($validation_result instanceof PhabricatorAuthFactorResult)) {
throw new Exception(
pht(
'Expected "processValidateFactorForm()" to return an object '.
'of class "%s"; got something else (from "%s").',
'PhabricatorAuthFactorResult',
get_class($impl)));
}
if (!$validation_result->getIsValid()) {
$ok = false; $ok = false;
} }
$validation_results[$id] = $validation_result;
} }
if ($ok) { if ($ok) {
@ -595,17 +606,20 @@ final class PhabricatorAuthSessionEngine extends Phobject {
array $validation_results, array $validation_results,
PhabricatorUser $viewer, PhabricatorUser $viewer,
AphrontRequest $request) { AphrontRequest $request) {
assert_instances_of($validation_results, 'PhabricatorAuthFactorResult');
$form = id(new AphrontFormView()) $form = id(new AphrontFormView())
->setUser($viewer) ->setUser($viewer)
->appendRemarkupInstructions(''); ->appendRemarkupInstructions('');
foreach ($factors as $factor) { foreach ($factors as $factor) {
$result = idx($validation_results, $factor->getID());
$factor->requireImplementation()->renderValidateFactorForm( $factor->requireImplementation()->renderValidateFactorForm(
$factor, $factor,
$form, $form,
$viewer, $viewer,
idx($validation_results, $factor->getID())); $result);
} }
$form->appendRemarkupInstructions(''); $form->appendRemarkupInstructions('');

View file

@ -7,6 +7,7 @@ final class PhabricatorAuthHighSecurityRequiredException extends Exception {
private $factorValidationResults; private $factorValidationResults;
public function setFactorValidationResults(array $results) { public function setFactorValidationResults(array $results) {
assert_instances_of($results, 'PhabricatorAuthFactorResult');
$this->factorValidationResults = $results; $this->factorValidationResults = $results;
return $this; return $this;
} }

View file

@ -14,19 +14,13 @@ abstract class PhabricatorAuthFactor extends Phobject {
PhabricatorAuthFactorConfig $config, PhabricatorAuthFactorConfig $config,
AphrontFormView $form, AphrontFormView $form,
PhabricatorUser $viewer, PhabricatorUser $viewer,
$validation_result); PhabricatorAuthFactorResult $validation_result = null);
abstract public function processValidateFactorForm( abstract public function processValidateFactorForm(
PhabricatorAuthFactorConfig $config, PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer, PhabricatorUser $viewer,
AphrontRequest $request); AphrontRequest $request);
public function isFactorValid(
PhabricatorAuthFactorConfig $config,
$validation_result) {
return (idx($validation_result, 'valid') === true);
}
public function getParameterName( public function getParameterName(
PhabricatorAuthFactorConfig $config, PhabricatorAuthFactorConfig $config,
$name) { $name) {

View file

@ -0,0 +1,37 @@
<?php
final class PhabricatorAuthFactorResult
extends Phobject {
private $isValid = false;
private $hint;
private $value;
public function setIsValid($is_valid) {
$this->isValid = $is_valid;
return $this;
}
public function getIsValid() {
return $this->isValid;
}
public function setHint($hint) {
$this->hint = $hint;
return $this;
}
public function getHint() {
return $this->hint;
}
public function setValue($value) {
$this->value = $value;
return $this;
}
public function getValue() {
return $this->value;
}
}

View file

@ -154,10 +154,14 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
PhabricatorAuthFactorConfig $config, PhabricatorAuthFactorConfig $config,
AphrontFormView $form, AphrontFormView $form,
PhabricatorUser $viewer, PhabricatorUser $viewer,
$validation_result) { PhabricatorAuthFactorResult $validation_result = null) {
if (!$validation_result) { if ($validation_result) {
$validation_result = array(); $value = $validation_result->getValue();
$hint = $validation_result->getHint();
} else {
$value = null;
$hint = true;
} }
$form->appendChild( $form->appendChild(
@ -166,8 +170,8 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
->setLabel(pht('App Code')) ->setLabel(pht('App Code'))
->setDisableAutocomplete(true) ->setDisableAutocomplete(true)
->setCaption(pht('Factor Name: %s', $config->getFactorName())) ->setCaption(pht('Factor Name: %s', $config->getFactorName()))
->setValue(idx($validation_result, 'value')) ->setValue($value)
->setError(idx($validation_result, 'error', true))); ->setError($hint));
} }
public function processValidateFactorForm( public function processValidateFactorForm(
@ -178,21 +182,22 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
$code = $request->getStr($this->getParameterName($config, 'totpcode')); $code = $request->getStr($this->getParameterName($config, 'totpcode'));
$key = new PhutilOpaqueEnvelope($config->getFactorSecret()); $key = new PhutilOpaqueEnvelope($config->getFactorSecret());
if (self::verifyTOTPCode($viewer, $key, $code)) { $result = id(new PhabricatorAuthFactorResult())
return array( ->setValue($code);
'error' => null,
'value' => $code,
'valid' => true,
);
} else {
return array(
'error' => strlen($code) ? pht('Invalid') : pht('Required'),
'value' => $code,
'valid' => false,
);
}
}
if (self::verifyTOTPCode($viewer, $key, $code)) {
$result->setIsValid(true);
} else {
if (strlen($code)) {
$hint = pht('Invalid');
} else {
$hint = pht('Required');
}
$result->setHint($hint);
}
return $result;
}
public static function generateNewTOTPKey() { public static function generateNewTOTPKey() {
return strtoupper(Filesystem::readRandomCharacters(32)); return strtoupper(Filesystem::readRandomCharacters(32));