1
0
Fork 0
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:
epriestley 2014-04-28 10:20:54 -07:00
parent bf6bda6ef4
commit a017a8e02b
9 changed files with 171 additions and 36 deletions

View file

@ -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);

View file

@ -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;
} }

View file

@ -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;

View file

@ -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;

View file

@ -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));
} }

View file

@ -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;
}
} }

View file

@ -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'),
); );

View file

@ -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);

View file

@ -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);