mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-20 13:52:40 +01:00
Make two-factor auth actually work
Summary: Ref T4398. Allows auth factors to render and validate when prompted to take a hi-sec action. This has a whole lot of rough edges still (see D8875) but does fundamentally work correctly. Test Plan: - Added two different TOTP factors to my account for EXTRA SECURITY. - Took hisec actions with no auth factors, and with attached auth factors. - Hit all the error/failure states of the hisec entry process. - Verified hisec failures appear in activity logs. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T4398 Differential Revision: https://secure.phabricator.com/D8886
This commit is contained in:
parent
bf6bda6ef4
commit
a017a8e02b
9 changed files with 171 additions and 36 deletions
|
@ -126,6 +126,8 @@ class AphrontDefaultApplicationConfiguration
|
||||||
if ($ex instanceof PhabricatorAuthHighSecurityRequiredException) {
|
if ($ex instanceof PhabricatorAuthHighSecurityRequiredException) {
|
||||||
|
|
||||||
$form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm(
|
$form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm(
|
||||||
|
$ex->getFactors(),
|
||||||
|
$ex->getFactorValidationResults(),
|
||||||
$user,
|
$user,
|
||||||
$request);
|
$request);
|
||||||
|
|
||||||
|
|
|
@ -227,34 +227,69 @@ final class PhabricatorAuthSessionEngine extends Phobject {
|
||||||
|
|
||||||
$session = $viewer->getSession();
|
$session = $viewer->getSession();
|
||||||
|
|
||||||
|
// Check if the session is already in high security mode.
|
||||||
$token = $this->issueHighSecurityToken($session);
|
$token = $this->issueHighSecurityToken($session);
|
||||||
if ($token) {
|
if ($token) {
|
||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the multi-factor auth sources attached to this account.
|
||||||
|
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
|
||||||
|
'userPHID = %s',
|
||||||
|
$viewer->getPHID());
|
||||||
|
|
||||||
|
// If the account has no associated multi-factor auth, just issue a token
|
||||||
|
// without putting the session into high security mode. This is generally
|
||||||
|
// easier for users. A minor but desirable side effect is that when a user
|
||||||
|
// adds an auth factor, existing sessions won't get a free pass into hisec,
|
||||||
|
// since they never actually got marked as hisec.
|
||||||
|
if (!$factors) {
|
||||||
|
return $this->issueHighSecurityToken($session, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validation_results = array();
|
||||||
if ($request->isHTTPPost()) {
|
if ($request->isHTTPPost()) {
|
||||||
$request->validateCSRF();
|
$request->validateCSRF();
|
||||||
if ($request->getExists(AphrontRequest::TYPE_HISEC)) {
|
if ($request->getExists(AphrontRequest::TYPE_HISEC)) {
|
||||||
|
|
||||||
// TODO: Actually verify that the user provided some multi-factor
|
$ok = true;
|
||||||
// auth credentials here. For now, we just let you enter high
|
foreach ($factors as $factor) {
|
||||||
// security.
|
$id = $factor->getID();
|
||||||
|
$impl = $factor->requireImplementation();
|
||||||
|
|
||||||
$until = time() + phutil_units('15 minutes in seconds');
|
$validation_results[$id] = $impl->processValidateFactorForm(
|
||||||
$session->setHighSecurityUntil($until);
|
$factor,
|
||||||
|
$viewer,
|
||||||
|
$request);
|
||||||
|
|
||||||
queryfx(
|
if (!$impl->isFactorValid($factor, $validation_results[$id])) {
|
||||||
$session->establishConnection('w'),
|
$ok = false;
|
||||||
'UPDATE %T SET highSecurityUntil = %d WHERE id = %d',
|
}
|
||||||
$session->getTableName(),
|
}
|
||||||
$until,
|
|
||||||
$session->getID());
|
|
||||||
|
|
||||||
$log = PhabricatorUserLog::initializeNewLog(
|
if ($ok) {
|
||||||
$viewer,
|
$until = time() + phutil_units('15 minutes in seconds');
|
||||||
$viewer->getPHID(),
|
$session->setHighSecurityUntil($until);
|
||||||
PhabricatorUserLog::ACTION_ENTER_HISEC);
|
|
||||||
$log->save();
|
queryfx(
|
||||||
|
$session->establishConnection('w'),
|
||||||
|
'UPDATE %T SET highSecurityUntil = %d WHERE id = %d',
|
||||||
|
$session->getTableName(),
|
||||||
|
$until,
|
||||||
|
$session->getID());
|
||||||
|
|
||||||
|
$log = PhabricatorUserLog::initializeNewLog(
|
||||||
|
$viewer,
|
||||||
|
$viewer->getPHID(),
|
||||||
|
PhabricatorUserLog::ACTION_ENTER_HISEC);
|
||||||
|
$log->save();
|
||||||
|
} else {
|
||||||
|
$log = PhabricatorUserLog::initializeNewLog(
|
||||||
|
$viewer,
|
||||||
|
$viewer->getPHID(),
|
||||||
|
PhabricatorUserLog::ACTION_FAIL_HISEC);
|
||||||
|
$log->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,7 +299,9 @@ final class PhabricatorAuthSessionEngine extends Phobject {
|
||||||
}
|
}
|
||||||
|
|
||||||
throw id(new PhabricatorAuthHighSecurityRequiredException())
|
throw id(new PhabricatorAuthHighSecurityRequiredException())
|
||||||
->setCancelURI($cancel_uri);
|
->setCancelURI($cancel_uri)
|
||||||
|
->setFactors($factors)
|
||||||
|
->setFactorValidationResults($validation_results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -291,19 +328,25 @@ final class PhabricatorAuthSessionEngine extends Phobject {
|
||||||
* @return AphrontFormView Renderable form.
|
* @return AphrontFormView Renderable form.
|
||||||
*/
|
*/
|
||||||
public function renderHighSecurityForm(
|
public function renderHighSecurityForm(
|
||||||
|
array $factors,
|
||||||
|
array $validation_results,
|
||||||
PhabricatorUser $viewer,
|
PhabricatorUser $viewer,
|
||||||
AphrontRequest $request) {
|
AphrontRequest $request) {
|
||||||
|
|
||||||
// TODO: This is stubbed.
|
|
||||||
|
|
||||||
$form = id(new AphrontFormView())
|
$form = id(new AphrontFormView())
|
||||||
->setUser($viewer)
|
->setUser($viewer)
|
||||||
->appendRemarkupInstructions('')
|
|
||||||
->appendChild(
|
|
||||||
id(new AphrontFormTextControl())
|
|
||||||
->setLabel(pht('Secret Stuff')))
|
|
||||||
->appendRemarkupInstructions('');
|
->appendRemarkupInstructions('');
|
||||||
|
|
||||||
|
foreach ($factors as $factor) {
|
||||||
|
$factor->requireImplementation()->renderValidateFactorForm(
|
||||||
|
$factor,
|
||||||
|
$form,
|
||||||
|
$viewer,
|
||||||
|
idx($validation_results, $factor->getID()));
|
||||||
|
}
|
||||||
|
|
||||||
|
$form->appendRemarkupInstructions('');
|
||||||
|
|
||||||
return $form;
|
return $form;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,27 @@
|
||||||
final class PhabricatorAuthHighSecurityRequiredException extends Exception {
|
final class PhabricatorAuthHighSecurityRequiredException extends Exception {
|
||||||
|
|
||||||
private $cancelURI;
|
private $cancelURI;
|
||||||
|
private $factors;
|
||||||
|
private $factorValidationResults;
|
||||||
|
|
||||||
|
public function setFactorValidationResults(array $results) {
|
||||||
|
$this->factorValidationResults = $results;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFactorValidationResults() {
|
||||||
|
return $this->factorValidationResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFactors(array $factors) {
|
||||||
|
assert_instances_of($factors, 'PhabricatorAuthFactorConfig');
|
||||||
|
$this->factors = $factors;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFactors() {
|
||||||
|
return $this->factors;
|
||||||
|
}
|
||||||
|
|
||||||
public function setCancelURI($cancel_uri) {
|
public function setCancelURI($cancel_uri) {
|
||||||
$this->cancelURI = $cancel_uri;
|
$this->cancelURI = $cancel_uri;
|
||||||
|
|
|
@ -10,6 +10,29 @@ abstract class PhabricatorAuthFactor extends Phobject {
|
||||||
AphrontRequest $request,
|
AphrontRequest $request,
|
||||||
PhabricatorUser $user);
|
PhabricatorUser $user);
|
||||||
|
|
||||||
|
abstract public function renderValidateFactorForm(
|
||||||
|
PhabricatorAuthFactorConfig $config,
|
||||||
|
AphrontFormView $form,
|
||||||
|
PhabricatorUser $viewer,
|
||||||
|
$validation_result);
|
||||||
|
|
||||||
|
abstract public function processValidateFactorForm(
|
||||||
|
PhabricatorAuthFactorConfig $config,
|
||||||
|
PhabricatorUser $viewer,
|
||||||
|
AphrontRequest $request);
|
||||||
|
|
||||||
|
public function isFactorValid(
|
||||||
|
PhabricatorAuthFactorConfig $config,
|
||||||
|
$validation_result) {
|
||||||
|
return (idx($validation_result, 'valid') === true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParameterName(
|
||||||
|
PhabricatorAuthFactorConfig $config,
|
||||||
|
$name) {
|
||||||
|
return 'authfactor.'.$config->getID().'.'.$name;
|
||||||
|
}
|
||||||
|
|
||||||
public static function getAllFactors() {
|
public static function getAllFactors() {
|
||||||
static $factors;
|
static $factors;
|
||||||
|
|
||||||
|
|
|
@ -97,6 +97,49 @@ final class PhabricatorAuthFactorTOTP extends PhabricatorAuthFactor {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function renderValidateFactorForm(
|
||||||
|
PhabricatorAuthFactorConfig $config,
|
||||||
|
AphrontFormView $form,
|
||||||
|
PhabricatorUser $viewer,
|
||||||
|
$validation_result) {
|
||||||
|
|
||||||
|
if (!$validation_result) {
|
||||||
|
$validation_result = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$form->appendChild(
|
||||||
|
id(new AphrontFormTextControl())
|
||||||
|
->setName($this->getParameterName($config, 'totpcode'))
|
||||||
|
->setLabel(pht('App Code'))
|
||||||
|
->setCaption(pht('Factor Name: %s', $config->getFactorName()))
|
||||||
|
->setValue(idx($validation_result, 'value'))
|
||||||
|
->setError(idx($validation_result, 'error', true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processValidateFactorForm(
|
||||||
|
PhabricatorAuthFactorConfig $config,
|
||||||
|
PhabricatorUser $viewer,
|
||||||
|
AphrontRequest $request) {
|
||||||
|
|
||||||
|
$code = $request->getStr($this->getParameterName($config, 'totpcode'));
|
||||||
|
$key = new PhutilOpaqueEnvelope($config->getFactorSecret());
|
||||||
|
|
||||||
|
if (self::verifyTOTPCode($viewer, $key, $code)) {
|
||||||
|
return array(
|
||||||
|
'error' => null,
|
||||||
|
'value' => $code,
|
||||||
|
'valid' => true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return array(
|
||||||
|
'error' => strlen($code) ? pht('Invalid') : pht('Required'),
|
||||||
|
'value' => $code,
|
||||||
|
'valid' => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static function generateNewTOTPKey() {
|
public static function generateNewTOTPKey() {
|
||||||
return strtoupper(Filesystem::readRandomCharacters(16));
|
return strtoupper(Filesystem::readRandomCharacters(16));
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,4 +26,17 @@ final class PhabricatorAuthFactorConfig extends PhabricatorAuthDAO {
|
||||||
return idx(PhabricatorAuthFactor::getAllFactors(), $this->getFactorKey());
|
return idx(PhabricatorAuthFactor::getAllFactors(), $this->getFactorKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function requireImplementation() {
|
||||||
|
$impl = $this->getImplementation();
|
||||||
|
if (!$impl) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Attempting to operate on multi-factor auth which has no '.
|
||||||
|
'corresponding implementation (factor key is "%s").',
|
||||||
|
$this->getFactorKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $impl;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ final class PhabricatorUserLog extends PhabricatorUserDAO
|
||||||
|
|
||||||
const ACTION_ENTER_HISEC = 'hisec-enter';
|
const ACTION_ENTER_HISEC = 'hisec-enter';
|
||||||
const ACTION_EXIT_HISEC = 'hisec-exit';
|
const ACTION_EXIT_HISEC = 'hisec-exit';
|
||||||
|
const ACTION_FAIL_HISEC = 'hisec-fail';
|
||||||
|
|
||||||
const ACTION_MULTI_ADD = 'multi-add';
|
const ACTION_MULTI_ADD = 'multi-add';
|
||||||
const ACTION_MULTI_REMOVE = 'multi-remove';
|
const ACTION_MULTI_REMOVE = 'multi-remove';
|
||||||
|
@ -66,6 +67,7 @@ final class PhabricatorUserLog extends PhabricatorUserDAO
|
||||||
self::ACTION_CHANGE_USERNAME => pht('Change Username'),
|
self::ACTION_CHANGE_USERNAME => pht('Change Username'),
|
||||||
self::ACTION_ENTER_HISEC => pht('Hisec: Enter'),
|
self::ACTION_ENTER_HISEC => pht('Hisec: Enter'),
|
||||||
self::ACTION_EXIT_HISEC => pht('Hisec: Exit'),
|
self::ACTION_EXIT_HISEC => pht('Hisec: Exit'),
|
||||||
|
self::ACTION_FAIL_HISEC => pht('Hisec: Failed Attempt'),
|
||||||
self::ACTION_MULTI_ADD => pht('Multi-Factor: Add Factor'),
|
self::ACTION_MULTI_ADD => pht('Multi-Factor: Add Factor'),
|
||||||
self::ACTION_MULTI_REMOVE => pht('Multi-Factor: Remove Factor'),
|
self::ACTION_MULTI_REMOVE => pht('Multi-Factor: Remove Factor'),
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,11 +15,6 @@ final class PhabricatorSettingsPanelMultiFactor
|
||||||
return pht('Authentication');
|
return pht('Authentication');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isEnabled() {
|
|
||||||
// TODO: Enable this panel once more pieces work correctly.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function processRequest(AphrontRequest $request) {
|
public function processRequest(AphrontRequest $request) {
|
||||||
if ($request->getExists('new')) {
|
if ($request->getExists('new')) {
|
||||||
return $this->processNew($request);
|
return $this->processNew($request);
|
||||||
|
|
|
@ -38,17 +38,10 @@ final class PhabricatorSettingsPanelSSHKeys
|
||||||
return $this->renderKeyListView($request);
|
return $this->renderKeyListView($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
NOTE: Uncomment this to test hisec.
|
|
||||||
TOOD: Implement this fully once hisec does something useful.
|
|
||||||
|
|
||||||
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
|
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
|
||||||
$viewer,
|
$viewer,
|
||||||
$request,
|
$request,
|
||||||
'/settings/panel/ssh/');
|
$this->getPanelURI());
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
$id = nonempty($edit, $delete);
|
$id = nonempty($edit, $delete);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue