mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-29 18:22:41 +01:00
Add multi-factor auth and TOTP support
Summary: Ref T4398. This is still pretty rough and isn't exposed in the UI yet, but basically works. Some missing features / areas for improvement: - Rate limiting attempts (see TODO). - Marking tokens used after they're used once (see TODO), maybe. I can't think of ways an attacker could capture a token without also capturing a session, offhand. - Actually turning this on (see TODO). - This workflow is pretty wordy. It would be nice to calm it down a bit. - But also add more help/context to help users figure out what's going on here, I think it's not very obvious if you don't already know what "TOTP" is. - Add admin tool to strip auth factors off an account ("Help, I lost my phone and can't log in!"). - Add admin tool to show users who don't have multi-factor auth? (so you can pester them) - Generate QR codes to make the transfer process easier (they're fairly complicated). - Make the "entering hi-sec" workflow actually check for auth factors and use them correctly. - Turn this on so users can use it. - Adding SMS as an option would be nice eventually. - Adding "password" as an option, maybe? TOTP feels fairly good to me. I'll post a couple of screens... Test Plan: - Added TOTP token with Google Authenticator. - Added TOTP token with Authy. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T4398 Differential Revision: https://secure.phabricator.com/D8875
This commit is contained in:
parent
93f23674bf
commit
17709bc167
10 changed files with 685 additions and 0 deletions
13
resources/sql/autopatches/20140427.mfactor.1.sql
Normal file
13
resources/sql/autopatches/20140427.mfactor.1.sql
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
CREATE TABLE {$NAMESPACE}_auth.auth_factorconfig (
|
||||||
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
phid VARCHAR(64) NOT NULL COLLATE utf8_bin,
|
||||||
|
userPHID VARCHAR(64) NOT NULL COLLATE utf8_bin,
|
||||||
|
factorKey VARCHAR(64) NOT NULL COLLATE utf8_bin,
|
||||||
|
factorName LONGTEXT NOT NULL COLLATE utf8_general_ci,
|
||||||
|
factorSecret LONGTEXT NOT NULL COLLATE utf8_bin,
|
||||||
|
properties LONGTEXT NOT NULL COLLATE utf8_bin,
|
||||||
|
dateCreated INT UNSIGNED NOT NULL,
|
||||||
|
dateModified INT UNSIGNED NOT NULL,
|
||||||
|
KEY `key_user` (userPHID),
|
||||||
|
UNIQUE KEY `key_phid` (phid)
|
||||||
|
) ENGINE=InnoDB, COLLATE utf8_general_ci;
|
|
@ -1208,6 +1208,10 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php',
|
'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php',
|
||||||
'PhabricatorAuthDowngradeSessionController' => 'applications/auth/controller/PhabricatorAuthDowngradeSessionController.php',
|
'PhabricatorAuthDowngradeSessionController' => 'applications/auth/controller/PhabricatorAuthDowngradeSessionController.php',
|
||||||
'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php',
|
'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php',
|
||||||
|
'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php',
|
||||||
|
'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php',
|
||||||
|
'PhabricatorAuthFactorTOTP' => 'applications/auth/factor/PhabricatorAuthFactorTOTP.php',
|
||||||
|
'PhabricatorAuthFactorTOTPTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTOTPTestCase.php',
|
||||||
'PhabricatorAuthHighSecurityRequiredException' => 'applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php',
|
'PhabricatorAuthHighSecurityRequiredException' => 'applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php',
|
||||||
'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php',
|
'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php',
|
||||||
'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php',
|
'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php',
|
||||||
|
@ -1220,6 +1224,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorAuthNeedsApprovalController' => 'applications/auth/controller/PhabricatorAuthNeedsApprovalController.php',
|
'PhabricatorAuthNeedsApprovalController' => 'applications/auth/controller/PhabricatorAuthNeedsApprovalController.php',
|
||||||
'PhabricatorAuthNewController' => 'applications/auth/controller/config/PhabricatorAuthNewController.php',
|
'PhabricatorAuthNewController' => 'applications/auth/controller/config/PhabricatorAuthNewController.php',
|
||||||
'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php',
|
'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php',
|
||||||
|
'PhabricatorAuthPHIDTypeAuthFactor' => 'applications/auth/phid/PhabricatorAuthPHIDTypeAuthFactor.php',
|
||||||
'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php',
|
'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php',
|
||||||
'PhabricatorAuthProviderConfig' => 'applications/auth/storage/PhabricatorAuthProviderConfig.php',
|
'PhabricatorAuthProviderConfig' => 'applications/auth/storage/PhabricatorAuthProviderConfig.php',
|
||||||
'PhabricatorAuthProviderConfigController' => 'applications/auth/controller/config/PhabricatorAuthProviderConfigController.php',
|
'PhabricatorAuthProviderConfigController' => 'applications/auth/controller/config/PhabricatorAuthProviderConfigController.php',
|
||||||
|
@ -2065,6 +2070,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorSettingsPanelEmailPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php',
|
'PhabricatorSettingsPanelEmailPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php',
|
||||||
'PhabricatorSettingsPanelExternalAccounts' => 'applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php',
|
'PhabricatorSettingsPanelExternalAccounts' => 'applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php',
|
||||||
'PhabricatorSettingsPanelHomePreferences' => 'applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php',
|
'PhabricatorSettingsPanelHomePreferences' => 'applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php',
|
||||||
|
'PhabricatorSettingsPanelMultiFactor' => 'applications/settings/panel/PhabricatorSettingsPanelMultiFactor.php',
|
||||||
'PhabricatorSettingsPanelPassword' => 'applications/settings/panel/PhabricatorSettingsPanelPassword.php',
|
'PhabricatorSettingsPanelPassword' => 'applications/settings/panel/PhabricatorSettingsPanelPassword.php',
|
||||||
'PhabricatorSettingsPanelSSHKeys' => 'applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php',
|
'PhabricatorSettingsPanelSSHKeys' => 'applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php',
|
||||||
'PhabricatorSettingsPanelSearchPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php',
|
'PhabricatorSettingsPanelSearchPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php',
|
||||||
|
@ -3956,6 +3962,10 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController',
|
'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController',
|
||||||
'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController',
|
'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController',
|
||||||
'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController',
|
'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController',
|
||||||
|
'PhabricatorAuthFactor' => 'Phobject',
|
||||||
|
'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO',
|
||||||
|
'PhabricatorAuthFactorTOTP' => 'PhabricatorAuthFactor',
|
||||||
|
'PhabricatorAuthFactorTOTPTestCase' => 'PhabricatorTestCase',
|
||||||
'PhabricatorAuthHighSecurityRequiredException' => 'Exception',
|
'PhabricatorAuthHighSecurityRequiredException' => 'Exception',
|
||||||
'PhabricatorAuthLinkController' => 'PhabricatorAuthController',
|
'PhabricatorAuthLinkController' => 'PhabricatorAuthController',
|
||||||
'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController',
|
'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController',
|
||||||
|
@ -3967,6 +3977,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorAuthNeedsApprovalController' => 'PhabricatorAuthController',
|
'PhabricatorAuthNeedsApprovalController' => 'PhabricatorAuthController',
|
||||||
'PhabricatorAuthNewController' => 'PhabricatorAuthProviderConfigController',
|
'PhabricatorAuthNewController' => 'PhabricatorAuthProviderConfigController',
|
||||||
'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController',
|
'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController',
|
||||||
|
'PhabricatorAuthPHIDTypeAuthFactor' => 'PhabricatorPHIDType',
|
||||||
'PhabricatorAuthProviderConfig' =>
|
'PhabricatorAuthProviderConfig' =>
|
||||||
array(
|
array(
|
||||||
0 => 'PhabricatorAuthDAO',
|
0 => 'PhabricatorAuthDAO',
|
||||||
|
@ -4962,6 +4973,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorSettingsPanelEmailPreferences' => 'PhabricatorSettingsPanel',
|
'PhabricatorSettingsPanelEmailPreferences' => 'PhabricatorSettingsPanel',
|
||||||
'PhabricatorSettingsPanelExternalAccounts' => 'PhabricatorSettingsPanel',
|
'PhabricatorSettingsPanelExternalAccounts' => 'PhabricatorSettingsPanel',
|
||||||
'PhabricatorSettingsPanelHomePreferences' => 'PhabricatorSettingsPanel',
|
'PhabricatorSettingsPanelHomePreferences' => 'PhabricatorSettingsPanel',
|
||||||
|
'PhabricatorSettingsPanelMultiFactor' => 'PhabricatorSettingsPanel',
|
||||||
'PhabricatorSettingsPanelPassword' => 'PhabricatorSettingsPanel',
|
'PhabricatorSettingsPanelPassword' => 'PhabricatorSettingsPanel',
|
||||||
'PhabricatorSettingsPanelSSHKeys' => 'PhabricatorSettingsPanel',
|
'PhabricatorSettingsPanelSSHKeys' => 'PhabricatorSettingsPanel',
|
||||||
'PhabricatorSettingsPanelSearchPreferences' => 'PhabricatorSettingsPanel',
|
'PhabricatorSettingsPanelSearchPreferences' => 'PhabricatorSettingsPanel',
|
||||||
|
|
51
src/applications/auth/factor/PhabricatorAuthFactor.php
Normal file
51
src/applications/auth/factor/PhabricatorAuthFactor.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
abstract class PhabricatorAuthFactor extends Phobject {
|
||||||
|
|
||||||
|
abstract public function getFactorName();
|
||||||
|
abstract public function getFactorKey();
|
||||||
|
abstract public function getFactorDescription();
|
||||||
|
abstract public function processAddFactorForm(
|
||||||
|
AphrontFormView $form,
|
||||||
|
AphrontRequest $request,
|
||||||
|
PhabricatorUser $user);
|
||||||
|
|
||||||
|
public static function getAllFactors() {
|
||||||
|
static $factors;
|
||||||
|
|
||||||
|
if ($factors === null) {
|
||||||
|
$map = id(new PhutilSymbolLoader())
|
||||||
|
->setAncestorClass(__CLASS__)
|
||||||
|
->loadObjects();
|
||||||
|
|
||||||
|
$factors = array();
|
||||||
|
foreach ($map as $factor) {
|
||||||
|
$key = $factor->getFactorKey();
|
||||||
|
if (empty($factors[$key])) {
|
||||||
|
$factors[$key] = $factor;
|
||||||
|
} else {
|
||||||
|
$this_class = get_class($factor);
|
||||||
|
$that_class = get_class($factors[$key]);
|
||||||
|
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Two auth factors (with classes "%s" and "%s") both provide '.
|
||||||
|
'implementations with the same key ("%s"). Each factor must '.
|
||||||
|
'have a unique key.',
|
||||||
|
$this_class,
|
||||||
|
$that_class,
|
||||||
|
$key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $factors;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function newConfigForUser(PhabricatorUser $user) {
|
||||||
|
return id(new PhabricatorAuthFactorConfig())
|
||||||
|
->setUserPHID($user->getPHID())
|
||||||
|
->setFactorKey($this->getFactorKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
179
src/applications/auth/factor/PhabricatorAuthFactorTOTP.php
Normal file
179
src/applications/auth/factor/PhabricatorAuthFactorTOTP.php
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorAuthFactorTOTP extends PhabricatorAuthFactor {
|
||||||
|
|
||||||
|
public function getFactorKey() {
|
||||||
|
return 'totp';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFactorName() {
|
||||||
|
return pht('Mobile Phone App (TOTP)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFactorDescription() {
|
||||||
|
return pht(
|
||||||
|
'Attach a mobile authenticator application (like Authy '.
|
||||||
|
'or Google Authenticator) to your account. When you need to '.
|
||||||
|
'authenticate, you will enter a code shown on your phone.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processAddFactorForm(
|
||||||
|
AphrontFormView $form,
|
||||||
|
AphrontRequest $request,
|
||||||
|
PhabricatorUser $user) {
|
||||||
|
|
||||||
|
|
||||||
|
$key = $request->getStr('totpkey');
|
||||||
|
if (!strlen($key)) {
|
||||||
|
// TODO: When the user submits a key, we should require that it be
|
||||||
|
// one we generated for them, so there's no way an attacker can ever
|
||||||
|
// force a key they control onto an account. However, it's clumsy to
|
||||||
|
// do this right now. Once we have one-time tokens for SMS and email,
|
||||||
|
// we should be able to put it on that infrastructure.
|
||||||
|
$key = self::generateNewTOTPKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = $request->getStr('totpcode');
|
||||||
|
|
||||||
|
$e_code = true;
|
||||||
|
if ($request->getExists('totp')) {
|
||||||
|
$okay = self::verifyTOTPCode(
|
||||||
|
$user,
|
||||||
|
new PhutilOpaqueEnvelope($key),
|
||||||
|
$code);
|
||||||
|
|
||||||
|
if ($okay) {
|
||||||
|
$config = $this->newConfigForUser($user)
|
||||||
|
->setFactorName(pht('Mobile App (TOTP)'))
|
||||||
|
->setFactorSecret($key);
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
} else {
|
||||||
|
if (!strlen($code)) {
|
||||||
|
$e_code = pht('Required');
|
||||||
|
} else {
|
||||||
|
$e_code = pht('Invalid');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$form->addHiddenInput('totp', true);
|
||||||
|
$form->addHiddenInput('totpkey', $key);
|
||||||
|
|
||||||
|
$form->appendRemarkupInstructions(
|
||||||
|
pht(
|
||||||
|
'First, download an authenticator application on your phone. Two '.
|
||||||
|
'applications which work well are **Authy** and **Google '.
|
||||||
|
'Authenticator**, but any other TOTP application should also work.'));
|
||||||
|
|
||||||
|
$form->appendInstructions(
|
||||||
|
pht(
|
||||||
|
'Launch the application on your phone, and add a new entry for '.
|
||||||
|
'this Phabricator install. When prompted, enter the key shown '.
|
||||||
|
'below into the application.'));
|
||||||
|
|
||||||
|
$form->appendChild(
|
||||||
|
id(new AphrontFormStaticControl())
|
||||||
|
->setLabel(pht('Key'))
|
||||||
|
->setValue(phutil_tag('strong', array(), $key)));
|
||||||
|
|
||||||
|
$form->appendInstructions(
|
||||||
|
pht(
|
||||||
|
'(If given an option, select that this key is "Time Based", not '.
|
||||||
|
'"Counter Based".)'));
|
||||||
|
|
||||||
|
$form->appendInstructions(
|
||||||
|
pht(
|
||||||
|
'After entering the key, the application should display a numeric '.
|
||||||
|
'code. Enter that code below to confirm that you have configured '.
|
||||||
|
'the authenticator correctly:'));
|
||||||
|
|
||||||
|
$form->appendChild(
|
||||||
|
id(new AphrontFormTextControl())
|
||||||
|
->setLabel(pht('TOTP Code'))
|
||||||
|
->setName('totpcode')
|
||||||
|
->setValue($code)
|
||||||
|
->setError($e_code));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateNewTOTPKey() {
|
||||||
|
return strtoupper(Filesystem::readRandomCharacters(16));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function verifyTOTPCode(
|
||||||
|
PhabricatorUser $user,
|
||||||
|
PhutilOpaqueEnvelope $key,
|
||||||
|
$code) {
|
||||||
|
|
||||||
|
// TODO: This should use rate limiting to prevent multiple attempts in a
|
||||||
|
// short period of time.
|
||||||
|
|
||||||
|
$now = (int)(time() / 30);
|
||||||
|
|
||||||
|
// Allow the user to enter a code a few minutes away on either side, in
|
||||||
|
// case the server or client has some clock skew.
|
||||||
|
for ($offset = -2; $offset <= 2; $offset++) {
|
||||||
|
$real = self::getTOTPCode($key, $now + $offset);
|
||||||
|
if ($real === $code) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: After validating a code, this should mark it as used and prevent
|
||||||
|
// it from being reused.
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static function base32Decode($buf) {
|
||||||
|
$buf = strtoupper($buf);
|
||||||
|
|
||||||
|
$map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||||
|
$map = str_split($map);
|
||||||
|
$map = array_flip($map);
|
||||||
|
|
||||||
|
$out = '';
|
||||||
|
$len = strlen($buf);
|
||||||
|
$acc = 0;
|
||||||
|
$bits = 0;
|
||||||
|
for ($ii = 0; $ii < $len; $ii++) {
|
||||||
|
$chr = $buf[$ii];
|
||||||
|
$val = $map[$chr];
|
||||||
|
|
||||||
|
$acc = $acc << 5;
|
||||||
|
$acc = $acc + $val;
|
||||||
|
|
||||||
|
$bits += 5;
|
||||||
|
if ($bits >= 8) {
|
||||||
|
$bits = $bits - 8;
|
||||||
|
$out .= chr(($acc & (0xFF << $bits)) >> $bits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getTOTPCode(PhutilOpaqueEnvelope $key, $timestamp) {
|
||||||
|
$binary_timestamp = pack('N*', 0).pack('N*', $timestamp);
|
||||||
|
$binary_key = self::base32Decode($key->openEnvelope());
|
||||||
|
|
||||||
|
$hash = hash_hmac('sha1', $binary_timestamp, $binary_key, true);
|
||||||
|
|
||||||
|
// See RFC 4226.
|
||||||
|
|
||||||
|
$offset = ord($hash[19]) & 0x0F;
|
||||||
|
|
||||||
|
$code = ((ord($hash[$offset + 0]) & 0x7F) << 24) |
|
||||||
|
((ord($hash[$offset + 1]) & 0xFF) << 16) |
|
||||||
|
((ord($hash[$offset + 2]) & 0xFF) << 8) |
|
||||||
|
((ord($hash[$offset + 3]) ) );
|
||||||
|
|
||||||
|
$code = ($code % 1000000);
|
||||||
|
$code = str_pad($code, 6, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorAuthFactorTOTPTestCase extends PhabricatorTestCase {
|
||||||
|
|
||||||
|
public function testTOTPCodeGeneration() {
|
||||||
|
$tests = array(
|
||||||
|
array(
|
||||||
|
'AAAABBBBCCCCDDDD',
|
||||||
|
46620383,
|
||||||
|
'724492',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'AAAABBBBCCCCDDDD',
|
||||||
|
46620390,
|
||||||
|
'935803',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'Z3RFWEFJN233R23P',
|
||||||
|
46620398,
|
||||||
|
'273030',
|
||||||
|
),
|
||||||
|
|
||||||
|
// This is testing the case where the code has leading zeroes.
|
||||||
|
array(
|
||||||
|
'Z3RFWEFJN233R23W',
|
||||||
|
46620399,
|
||||||
|
'072346',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($tests as $test) {
|
||||||
|
list($key, $time, $code) = $test;
|
||||||
|
$this->assertEqual(
|
||||||
|
$code,
|
||||||
|
PhabricatorAuthFactorTOTP::getTOTPCode(
|
||||||
|
new PhutilOpaqueEnvelope($key),
|
||||||
|
$time));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorAuthPHIDTypeAuthFactor extends PhabricatorPHIDType {
|
||||||
|
|
||||||
|
const TYPECONST = 'AFTR';
|
||||||
|
|
||||||
|
public function getTypeConstant() {
|
||||||
|
return self::TYPECONST;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTypeName() {
|
||||||
|
return pht('Auth Factor');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newObject() {
|
||||||
|
return new PhabricatorAuthFactorConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildQueryForObjects(
|
||||||
|
PhabricatorObjectQuery $query,
|
||||||
|
array $phids) {
|
||||||
|
|
||||||
|
// TODO: Maybe we need this eventually?
|
||||||
|
throw new Exception(pht('Not Supported'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadHandles(
|
||||||
|
PhabricatorHandleQuery $query,
|
||||||
|
array $handles,
|
||||||
|
array $objects) {
|
||||||
|
|
||||||
|
foreach ($handles as $phid => $handle) {
|
||||||
|
$factor = $objects[$phid];
|
||||||
|
|
||||||
|
$handle->setName($factor->getFactorName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorAuthFactorConfig extends PhabricatorAuthDAO {
|
||||||
|
|
||||||
|
protected $userPHID;
|
||||||
|
protected $factorKey;
|
||||||
|
protected $factorName;
|
||||||
|
protected $factorSecret;
|
||||||
|
protected $properties = array();
|
||||||
|
|
||||||
|
public function getConfiguration() {
|
||||||
|
return array(
|
||||||
|
self::CONFIG_SERIALIZATION => array(
|
||||||
|
'properties' => self::SERIALIZATION_JSON,
|
||||||
|
),
|
||||||
|
self::CONFIG_AUX_PHID => true,
|
||||||
|
) + parent::getConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generatePHID() {
|
||||||
|
return PhabricatorPHID::generateNewPHID(
|
||||||
|
PhabricatorAuthPHIDTypeAuthFactor::TYPECONST);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getImplementation() {
|
||||||
|
return idx(PhabricatorAuthFactor::getAllFactors(), $this->getFactorKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -30,6 +30,9 @@ 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_MULTI_ADD = 'multi-add';
|
||||||
|
const ACTION_MULTI_REMOVE = 'multi-remove';
|
||||||
|
|
||||||
protected $actorPHID;
|
protected $actorPHID;
|
||||||
protected $userPHID;
|
protected $userPHID;
|
||||||
protected $action;
|
protected $action;
|
||||||
|
@ -63,6 +66,8 @@ 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_MULTI_ADD => pht('Multi-Factor: Add Factor'),
|
||||||
|
self::ACTION_MULTI_REMOVE => pht('Multi-Factor: Remove Factor'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
final class PhabricatorSettingsPanelActivity
|
final class PhabricatorSettingsPanelActivity
|
||||||
extends PhabricatorSettingsPanel {
|
extends PhabricatorSettingsPanel {
|
||||||
|
|
||||||
|
public function isEditableByAdministrators() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public function getPanelKey() {
|
public function getPanelKey() {
|
||||||
return 'activity';
|
return 'activity';
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,309 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorSettingsPanelMultiFactor
|
||||||
|
extends PhabricatorSettingsPanel {
|
||||||
|
|
||||||
|
public function getPanelKey() {
|
||||||
|
return 'multifactor';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPanelName() {
|
||||||
|
return pht('Multi-Factor Auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPanelGroup() {
|
||||||
|
return pht('Authentication');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEnabled() {
|
||||||
|
// TODO: Enable this panel once more pieces work correctly.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processRequest(AphrontRequest $request) {
|
||||||
|
if ($request->getExists('new')) {
|
||||||
|
return $this->processNew($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->getExists('edit')) {
|
||||||
|
return $this->processEdit($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->getExists('delete')) {
|
||||||
|
return $this->processDelete($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->getUser();
|
||||||
|
$viewer = $request->getUser();
|
||||||
|
|
||||||
|
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
|
||||||
|
'userPHID = %s',
|
||||||
|
$user->getPHID());
|
||||||
|
|
||||||
|
$rows = array();
|
||||||
|
$rowc = array();
|
||||||
|
|
||||||
|
$highlight_id = $request->getInt('id');
|
||||||
|
foreach ($factors as $factor) {
|
||||||
|
|
||||||
|
$impl = $factor->getImplementation();
|
||||||
|
if ($impl) {
|
||||||
|
$type = $impl->getFactorName();
|
||||||
|
} else {
|
||||||
|
$type = $factor->getFactorKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($factor->getID() == $highlight_id) {
|
||||||
|
$rowc[] = 'highlighted';
|
||||||
|
} else {
|
||||||
|
$rowc[] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows[] = array(
|
||||||
|
javelin_tag(
|
||||||
|
'a',
|
||||||
|
array(
|
||||||
|
'href' => $this->getPanelURI('?edit='.$factor->getID()),
|
||||||
|
'sigil' => 'workflow',
|
||||||
|
),
|
||||||
|
$factor->getFactorName()),
|
||||||
|
$type,
|
||||||
|
phabricator_datetime($factor->getDateCreated(), $viewer),
|
||||||
|
javelin_tag(
|
||||||
|
'a',
|
||||||
|
array(
|
||||||
|
'href' => $this->getPanelURI('?delete='.$factor->getID()),
|
||||||
|
'sigil' => 'workflow',
|
||||||
|
'class' => 'small grey button',
|
||||||
|
),
|
||||||
|
pht('Remove')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = new AphrontTableView($rows);
|
||||||
|
$table->setNoDataString(
|
||||||
|
pht("You haven't added any authentication factors to your account yet."));
|
||||||
|
$table->setHeaders(
|
||||||
|
array(
|
||||||
|
pht('Name'),
|
||||||
|
pht('Type'),
|
||||||
|
pht('Created'),
|
||||||
|
'',
|
||||||
|
));
|
||||||
|
$table->setColumnClasses(
|
||||||
|
array(
|
||||||
|
'wide pri',
|
||||||
|
'',
|
||||||
|
'right',
|
||||||
|
'action',
|
||||||
|
));
|
||||||
|
$table->setRowClasses($rowc);
|
||||||
|
$table->setDeviceVisibility(
|
||||||
|
array(
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
|
||||||
|
$panel = new PHUIObjectBoxView();
|
||||||
|
$header = new PHUIHeaderView();
|
||||||
|
|
||||||
|
$create_icon = id(new PHUIIconView())
|
||||||
|
->setSpriteSheet(PHUIIconView::SPRITE_ICONS)
|
||||||
|
->setSpriteIcon('new');
|
||||||
|
$create_button = id(new PHUIButtonView())
|
||||||
|
->setText(pht('Add Authentication Factor'))
|
||||||
|
->setHref($this->getPanelURI('?new=true'))
|
||||||
|
->setTag('a')
|
||||||
|
->setWorkflow(true)
|
||||||
|
->setIcon($create_icon);
|
||||||
|
|
||||||
|
$header->setHeader(pht('Authentication Factors'));
|
||||||
|
$header->addActionLink($create_button);
|
||||||
|
|
||||||
|
$panel->setHeader($header);
|
||||||
|
$panel->appendChild($table);
|
||||||
|
|
||||||
|
return $panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processNew(AphrontRequest $request) {
|
||||||
|
$viewer = $request->getUser();
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
|
||||||
|
$viewer,
|
||||||
|
$request,
|
||||||
|
$this->getPanelURI());
|
||||||
|
|
||||||
|
$factors = PhabricatorAuthFactor::getAllFactors();
|
||||||
|
|
||||||
|
$form = id(new AphrontFormView())
|
||||||
|
->setUser($viewer);
|
||||||
|
|
||||||
|
$type = $request->getStr('type');
|
||||||
|
if (empty($factors[$type]) || !$request->isFormPost()) {
|
||||||
|
$factor = null;
|
||||||
|
} else {
|
||||||
|
$factor = $factors[$type];
|
||||||
|
}
|
||||||
|
|
||||||
|
$dialog = id(new AphrontDialogView())
|
||||||
|
->setUser($viewer)
|
||||||
|
->addHiddenInput('new', true);
|
||||||
|
|
||||||
|
if ($factor === null) {
|
||||||
|
$choice_control = id(new AphrontFormRadioButtonControl())
|
||||||
|
->setName('type')
|
||||||
|
->setValue(key($factors));
|
||||||
|
|
||||||
|
foreach ($factors as $available_factor) {
|
||||||
|
$choice_control->addButton(
|
||||||
|
$available_factor->getFactorKey(),
|
||||||
|
$available_factor->getFactorName(),
|
||||||
|
$available_factor->getFactorDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
$dialog->appendParagraph(
|
||||||
|
pht(
|
||||||
|
'Adding an additional authentication factor increases the security '.
|
||||||
|
'of your account.'));
|
||||||
|
|
||||||
|
$form
|
||||||
|
->appendChild($choice_control);
|
||||||
|
} else {
|
||||||
|
$dialog->addHiddenInput('type', $type);
|
||||||
|
|
||||||
|
$config = $factor->processAddFactorForm(
|
||||||
|
$form,
|
||||||
|
$request,
|
||||||
|
$user);
|
||||||
|
|
||||||
|
if ($config) {
|
||||||
|
$config->save();
|
||||||
|
|
||||||
|
$log = PhabricatorUserLog::initializeNewLog(
|
||||||
|
$viewer,
|
||||||
|
$user->getPHID(),
|
||||||
|
PhabricatorUserLog::ACTION_MULTI_ADD);
|
||||||
|
$log->save();
|
||||||
|
|
||||||
|
return id(new AphrontRedirectResponse())
|
||||||
|
->setURI($this->getPanelURI('?id='.$config->getID()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$dialog
|
||||||
|
->setWidth(AphrontDialogView::WIDTH_FORM)
|
||||||
|
->setTitle(pht('Add Authentication Factor'))
|
||||||
|
->appendChild($form->buildLayoutView())
|
||||||
|
->addSubmitButton(pht('Continue'))
|
||||||
|
->addCancelButton($this->getPanelURI());
|
||||||
|
|
||||||
|
return id(new AphrontDialogResponse())
|
||||||
|
->setDialog($dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processEdit(AphrontRequest $request) {
|
||||||
|
$viewer = $request->getUser();
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
$factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere(
|
||||||
|
'id = %d AND userPHID = %s',
|
||||||
|
$request->getInt('edit'),
|
||||||
|
$user->getPHID());
|
||||||
|
if (!$factor) {
|
||||||
|
return new Aphront404Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
$e_name = true;
|
||||||
|
$errors = array();
|
||||||
|
if ($request->isFormPost()) {
|
||||||
|
$name = $request->getStr('name');
|
||||||
|
if (!strlen($name)) {
|
||||||
|
$e_name = pht('Required');
|
||||||
|
$errors[] = pht(
|
||||||
|
'Authentication factors must have a name to identify them.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$errors) {
|
||||||
|
$factor->setFactorName($name);
|
||||||
|
$factor->save();
|
||||||
|
|
||||||
|
return id(new AphrontRedirectResponse())
|
||||||
|
->setURI($this->getPanelURI('?id='.$factor->getID()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$name = $factor->getFactorName();
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = id(new AphrontFormView())
|
||||||
|
->setUser($viewer)
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormTextControl())
|
||||||
|
->setName('name')
|
||||||
|
->setLabel(pht('Name'))
|
||||||
|
->setValue($name)
|
||||||
|
->setError($e_name));
|
||||||
|
|
||||||
|
$dialog = id(new AphrontDialogView())
|
||||||
|
->setUser($viewer)
|
||||||
|
->addHiddenInput('edit', $factor->getID())
|
||||||
|
->setTitle(pht('Edit Authentication Factor'))
|
||||||
|
->setErrors($errors)
|
||||||
|
->appendChild($form->buildLayoutView())
|
||||||
|
->addSubmitButton(pht('Save'))
|
||||||
|
->addCancelButton($this->getPanelURI());
|
||||||
|
|
||||||
|
return id(new AphrontDialogResponse())
|
||||||
|
->setDialog($dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processDelete(AphrontRequest $request) {
|
||||||
|
$viewer = $request->getUser();
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
|
||||||
|
$viewer,
|
||||||
|
$request,
|
||||||
|
$this->getPanelURI());
|
||||||
|
|
||||||
|
$factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere(
|
||||||
|
'id = %d AND userPHID = %s',
|
||||||
|
$request->getInt('delete'),
|
||||||
|
$user->getPHID());
|
||||||
|
if (!$factor) {
|
||||||
|
return new Aphront404Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->isFormPost()) {
|
||||||
|
$factor->delete();
|
||||||
|
|
||||||
|
$log = PhabricatorUserLog::initializeNewLog(
|
||||||
|
$viewer,
|
||||||
|
$user->getPHID(),
|
||||||
|
PhabricatorUserLog::ACTION_MULTI_REMOVE);
|
||||||
|
$log->save();
|
||||||
|
|
||||||
|
return id(new AphrontRedirectResponse())
|
||||||
|
->setURI($this->getPanelURI());
|
||||||
|
}
|
||||||
|
|
||||||
|
$dialog = id(new AphrontDialogView())
|
||||||
|
->setUser($viewer)
|
||||||
|
->addHiddenInput('delete', $factor->getID())
|
||||||
|
->setTitle(pht('Delete Authentication Factor'))
|
||||||
|
->appendParagraph(
|
||||||
|
pht(
|
||||||
|
'Really remove the authentication factor %s from your account?',
|
||||||
|
phutil_tag('strong', array(), $factor->getFactorName())))
|
||||||
|
->addSubmitButton(pht('Remove Factor'))
|
||||||
|
->addCancelButton($this->getPanelURI());
|
||||||
|
|
||||||
|
return id(new AphrontDialogResponse())
|
||||||
|
->setDialog($dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue