mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-10 00:42: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:
parent
54b952df5d
commit
c731508d74
6 changed files with 82 additions and 29 deletions
|
@ -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',
|
||||||
|
|
|
@ -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('');
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
37
src/applications/auth/factor/PhabricatorAuthFactorResult.php
Normal file
37
src/applications/auth/factor/PhabricatorAuthFactorResult.php
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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());
|
||||||
|
|
||||||
|
$result = id(new PhabricatorAuthFactorResult())
|
||||||
|
->setValue($code);
|
||||||
|
|
||||||
if (self::verifyTOTPCode($viewer, $key, $code)) {
|
if (self::verifyTOTPCode($viewer, $key, $code)) {
|
||||||
return array(
|
$result->setIsValid(true);
|
||||||
'error' => null,
|
|
||||||
'value' => $code,
|
|
||||||
'valid' => true,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return array(
|
if (strlen($code)) {
|
||||||
'error' => strlen($code) ? pht('Invalid') : pht('Required'),
|
$hint = pht('Invalid');
|
||||||
'value' => $code,
|
} else {
|
||||||
'valid' => false,
|
$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));
|
||||||
|
|
Loading…
Reference in a new issue