@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator

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

+685
+13
resources/sql/autopatches/20140427.mfactor.1.sql
··· 1 + CREATE TABLE {$NAMESPACE}_auth.auth_factorconfig ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, 4 + userPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 5 + factorKey VARCHAR(64) NOT NULL COLLATE utf8_bin, 6 + factorName LONGTEXT NOT NULL COLLATE utf8_general_ci, 7 + factorSecret LONGTEXT NOT NULL COLLATE utf8_bin, 8 + properties LONGTEXT NOT NULL COLLATE utf8_bin, 9 + dateCreated INT UNSIGNED NOT NULL, 10 + dateModified INT UNSIGNED NOT NULL, 11 + KEY `key_user` (userPHID), 12 + UNIQUE KEY `key_phid` (phid) 13 + ) ENGINE=InnoDB, COLLATE utf8_general_ci;
+12
src/__phutil_library_map__.php
··· 1208 1208 'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php', 1209 1209 'PhabricatorAuthDowngradeSessionController' => 'applications/auth/controller/PhabricatorAuthDowngradeSessionController.php', 1210 1210 'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php', 1211 + 'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php', 1212 + 'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php', 1213 + 'PhabricatorAuthFactorTOTP' => 'applications/auth/factor/PhabricatorAuthFactorTOTP.php', 1214 + 'PhabricatorAuthFactorTOTPTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTOTPTestCase.php', 1211 1215 'PhabricatorAuthHighSecurityRequiredException' => 'applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php', 1212 1216 'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php', 1213 1217 'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php', ··· 1220 1224 'PhabricatorAuthNeedsApprovalController' => 'applications/auth/controller/PhabricatorAuthNeedsApprovalController.php', 1221 1225 'PhabricatorAuthNewController' => 'applications/auth/controller/config/PhabricatorAuthNewController.php', 1222 1226 'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php', 1227 + 'PhabricatorAuthPHIDTypeAuthFactor' => 'applications/auth/phid/PhabricatorAuthPHIDTypeAuthFactor.php', 1223 1228 'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php', 1224 1229 'PhabricatorAuthProviderConfig' => 'applications/auth/storage/PhabricatorAuthProviderConfig.php', 1225 1230 'PhabricatorAuthProviderConfigController' => 'applications/auth/controller/config/PhabricatorAuthProviderConfigController.php', ··· 2065 2070 'PhabricatorSettingsPanelEmailPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php', 2066 2071 'PhabricatorSettingsPanelExternalAccounts' => 'applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php', 2067 2072 'PhabricatorSettingsPanelHomePreferences' => 'applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php', 2073 + 'PhabricatorSettingsPanelMultiFactor' => 'applications/settings/panel/PhabricatorSettingsPanelMultiFactor.php', 2068 2074 'PhabricatorSettingsPanelPassword' => 'applications/settings/panel/PhabricatorSettingsPanelPassword.php', 2069 2075 'PhabricatorSettingsPanelSSHKeys' => 'applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php', 2070 2076 'PhabricatorSettingsPanelSearchPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php', ··· 3956 3962 'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController', 3957 3963 'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController', 3958 3964 'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController', 3965 + 'PhabricatorAuthFactor' => 'Phobject', 3966 + 'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO', 3967 + 'PhabricatorAuthFactorTOTP' => 'PhabricatorAuthFactor', 3968 + 'PhabricatorAuthFactorTOTPTestCase' => 'PhabricatorTestCase', 3959 3969 'PhabricatorAuthHighSecurityRequiredException' => 'Exception', 3960 3970 'PhabricatorAuthLinkController' => 'PhabricatorAuthController', 3961 3971 'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController', ··· 3967 3977 'PhabricatorAuthNeedsApprovalController' => 'PhabricatorAuthController', 3968 3978 'PhabricatorAuthNewController' => 'PhabricatorAuthProviderConfigController', 3969 3979 'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController', 3980 + 'PhabricatorAuthPHIDTypeAuthFactor' => 'PhabricatorPHIDType', 3970 3981 'PhabricatorAuthProviderConfig' => 3971 3982 array( 3972 3983 0 => 'PhabricatorAuthDAO', ··· 4962 4973 'PhabricatorSettingsPanelEmailPreferences' => 'PhabricatorSettingsPanel', 4963 4974 'PhabricatorSettingsPanelExternalAccounts' => 'PhabricatorSettingsPanel', 4964 4975 'PhabricatorSettingsPanelHomePreferences' => 'PhabricatorSettingsPanel', 4976 + 'PhabricatorSettingsPanelMultiFactor' => 'PhabricatorSettingsPanel', 4965 4977 'PhabricatorSettingsPanelPassword' => 'PhabricatorSettingsPanel', 4966 4978 'PhabricatorSettingsPanelSSHKeys' => 'PhabricatorSettingsPanel', 4967 4979 'PhabricatorSettingsPanelSearchPreferences' => 'PhabricatorSettingsPanel',
+51
src/applications/auth/factor/PhabricatorAuthFactor.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorAuthFactor extends Phobject { 4 + 5 + abstract public function getFactorName(); 6 + abstract public function getFactorKey(); 7 + abstract public function getFactorDescription(); 8 + abstract public function processAddFactorForm( 9 + AphrontFormView $form, 10 + AphrontRequest $request, 11 + PhabricatorUser $user); 12 + 13 + public static function getAllFactors() { 14 + static $factors; 15 + 16 + if ($factors === null) { 17 + $map = id(new PhutilSymbolLoader()) 18 + ->setAncestorClass(__CLASS__) 19 + ->loadObjects(); 20 + 21 + $factors = array(); 22 + foreach ($map as $factor) { 23 + $key = $factor->getFactorKey(); 24 + if (empty($factors[$key])) { 25 + $factors[$key] = $factor; 26 + } else { 27 + $this_class = get_class($factor); 28 + $that_class = get_class($factors[$key]); 29 + 30 + throw new Exception( 31 + pht( 32 + 'Two auth factors (with classes "%s" and "%s") both provide '. 33 + 'implementations with the same key ("%s"). Each factor must '. 34 + 'have a unique key.', 35 + $this_class, 36 + $that_class, 37 + $key)); 38 + } 39 + } 40 + } 41 + 42 + return $factors; 43 + } 44 + 45 + protected function newConfigForUser(PhabricatorUser $user) { 46 + return id(new PhabricatorAuthFactorConfig()) 47 + ->setUserPHID($user->getPHID()) 48 + ->setFactorKey($this->getFactorKey()); 49 + } 50 + 51 + }
+179
src/applications/auth/factor/PhabricatorAuthFactorTOTP.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthFactorTOTP extends PhabricatorAuthFactor { 4 + 5 + public function getFactorKey() { 6 + return 'totp'; 7 + } 8 + 9 + public function getFactorName() { 10 + return pht('Mobile Phone App (TOTP)'); 11 + } 12 + 13 + public function getFactorDescription() { 14 + return pht( 15 + 'Attach a mobile authenticator application (like Authy '. 16 + 'or Google Authenticator) to your account. When you need to '. 17 + 'authenticate, you will enter a code shown on your phone.'); 18 + } 19 + 20 + public function processAddFactorForm( 21 + AphrontFormView $form, 22 + AphrontRequest $request, 23 + PhabricatorUser $user) { 24 + 25 + 26 + $key = $request->getStr('totpkey'); 27 + if (!strlen($key)) { 28 + // TODO: When the user submits a key, we should require that it be 29 + // one we generated for them, so there's no way an attacker can ever 30 + // force a key they control onto an account. However, it's clumsy to 31 + // do this right now. Once we have one-time tokens for SMS and email, 32 + // we should be able to put it on that infrastructure. 33 + $key = self::generateNewTOTPKey(); 34 + } 35 + 36 + $code = $request->getStr('totpcode'); 37 + 38 + $e_code = true; 39 + if ($request->getExists('totp')) { 40 + $okay = self::verifyTOTPCode( 41 + $user, 42 + new PhutilOpaqueEnvelope($key), 43 + $code); 44 + 45 + if ($okay) { 46 + $config = $this->newConfigForUser($user) 47 + ->setFactorName(pht('Mobile App (TOTP)')) 48 + ->setFactorSecret($key); 49 + 50 + return $config; 51 + } else { 52 + if (!strlen($code)) { 53 + $e_code = pht('Required'); 54 + } else { 55 + $e_code = pht('Invalid'); 56 + } 57 + } 58 + } 59 + 60 + $form->addHiddenInput('totp', true); 61 + $form->addHiddenInput('totpkey', $key); 62 + 63 + $form->appendRemarkupInstructions( 64 + pht( 65 + 'First, download an authenticator application on your phone. Two '. 66 + 'applications which work well are **Authy** and **Google '. 67 + 'Authenticator**, but any other TOTP application should also work.')); 68 + 69 + $form->appendInstructions( 70 + pht( 71 + 'Launch the application on your phone, and add a new entry for '. 72 + 'this Phabricator install. When prompted, enter the key shown '. 73 + 'below into the application.')); 74 + 75 + $form->appendChild( 76 + id(new AphrontFormStaticControl()) 77 + ->setLabel(pht('Key')) 78 + ->setValue(phutil_tag('strong', array(), $key))); 79 + 80 + $form->appendInstructions( 81 + pht( 82 + '(If given an option, select that this key is "Time Based", not '. 83 + '"Counter Based".)')); 84 + 85 + $form->appendInstructions( 86 + pht( 87 + 'After entering the key, the application should display a numeric '. 88 + 'code. Enter that code below to confirm that you have configured '. 89 + 'the authenticator correctly:')); 90 + 91 + $form->appendChild( 92 + id(new AphrontFormTextControl()) 93 + ->setLabel(pht('TOTP Code')) 94 + ->setName('totpcode') 95 + ->setValue($code) 96 + ->setError($e_code)); 97 + 98 + } 99 + 100 + public static function generateNewTOTPKey() { 101 + return strtoupper(Filesystem::readRandomCharacters(16)); 102 + } 103 + 104 + public static function verifyTOTPCode( 105 + PhabricatorUser $user, 106 + PhutilOpaqueEnvelope $key, 107 + $code) { 108 + 109 + // TODO: This should use rate limiting to prevent multiple attempts in a 110 + // short period of time. 111 + 112 + $now = (int)(time() / 30); 113 + 114 + // Allow the user to enter a code a few minutes away on either side, in 115 + // case the server or client has some clock skew. 116 + for ($offset = -2; $offset <= 2; $offset++) { 117 + $real = self::getTOTPCode($key, $now + $offset); 118 + if ($real === $code) { 119 + return true; 120 + } 121 + } 122 + 123 + // TODO: After validating a code, this should mark it as used and prevent 124 + // it from being reused. 125 + 126 + return false; 127 + } 128 + 129 + 130 + public static function base32Decode($buf) { 131 + $buf = strtoupper($buf); 132 + 133 + $map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; 134 + $map = str_split($map); 135 + $map = array_flip($map); 136 + 137 + $out = ''; 138 + $len = strlen($buf); 139 + $acc = 0; 140 + $bits = 0; 141 + for ($ii = 0; $ii < $len; $ii++) { 142 + $chr = $buf[$ii]; 143 + $val = $map[$chr]; 144 + 145 + $acc = $acc << 5; 146 + $acc = $acc + $val; 147 + 148 + $bits += 5; 149 + if ($bits >= 8) { 150 + $bits = $bits - 8; 151 + $out .= chr(($acc & (0xFF << $bits)) >> $bits); 152 + } 153 + } 154 + 155 + return $out; 156 + } 157 + 158 + public static function getTOTPCode(PhutilOpaqueEnvelope $key, $timestamp) { 159 + $binary_timestamp = pack('N*', 0).pack('N*', $timestamp); 160 + $binary_key = self::base32Decode($key->openEnvelope()); 161 + 162 + $hash = hash_hmac('sha1', $binary_timestamp, $binary_key, true); 163 + 164 + // See RFC 4226. 165 + 166 + $offset = ord($hash[19]) & 0x0F; 167 + 168 + $code = ((ord($hash[$offset + 0]) & 0x7F) << 24) | 169 + ((ord($hash[$offset + 1]) & 0xFF) << 16) | 170 + ((ord($hash[$offset + 2]) & 0xFF) << 8) | 171 + ((ord($hash[$offset + 3]) ) ); 172 + 173 + $code = ($code % 1000000); 174 + $code = str_pad($code, 6, '0', STR_PAD_LEFT); 175 + 176 + return $code; 177 + } 178 + 179 + }
+44
src/applications/auth/factor/__tests__/PhabricatorAuthFactorTOTPTestCase.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthFactorTOTPTestCase extends PhabricatorTestCase { 4 + 5 + public function testTOTPCodeGeneration() { 6 + $tests = array( 7 + array( 8 + 'AAAABBBBCCCCDDDD', 9 + 46620383, 10 + '724492', 11 + ), 12 + array( 13 + 'AAAABBBBCCCCDDDD', 14 + 46620390, 15 + '935803', 16 + ), 17 + array( 18 + 'Z3RFWEFJN233R23P', 19 + 46620398, 20 + '273030', 21 + ), 22 + 23 + // This is testing the case where the code has leading zeroes. 24 + array( 25 + 'Z3RFWEFJN233R23W', 26 + 46620399, 27 + '072346', 28 + ), 29 + ); 30 + 31 + foreach ($tests as $test) { 32 + list($key, $time, $code) = $test; 33 + $this->assertEqual( 34 + $code, 35 + PhabricatorAuthFactorTOTP::getTOTPCode( 36 + new PhutilOpaqueEnvelope($key), 37 + $time)); 38 + } 39 + 40 + } 41 + 42 + 43 + 44 + }
+39
src/applications/auth/phid/PhabricatorAuthPHIDTypeAuthFactor.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthPHIDTypeAuthFactor extends PhabricatorPHIDType { 4 + 5 + const TYPECONST = 'AFTR'; 6 + 7 + public function getTypeConstant() { 8 + return self::TYPECONST; 9 + } 10 + 11 + public function getTypeName() { 12 + return pht('Auth Factor'); 13 + } 14 + 15 + public function newObject() { 16 + return new PhabricatorAuthFactorConfig(); 17 + } 18 + 19 + protected function buildQueryForObjects( 20 + PhabricatorObjectQuery $query, 21 + array $phids) { 22 + 23 + // TODO: Maybe we need this eventually? 24 + throw new Exception(pht('Not Supported')); 25 + } 26 + 27 + public function loadHandles( 28 + PhabricatorHandleQuery $query, 29 + array $handles, 30 + array $objects) { 31 + 32 + foreach ($handles as $phid => $handle) { 33 + $factor = $objects[$phid]; 34 + 35 + $handle->setName($factor->getFactorName()); 36 + } 37 + } 38 + 39 + }
+29
src/applications/auth/storage/PhabricatorAuthFactorConfig.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthFactorConfig extends PhabricatorAuthDAO { 4 + 5 + protected $userPHID; 6 + protected $factorKey; 7 + protected $factorName; 8 + protected $factorSecret; 9 + protected $properties = array(); 10 + 11 + public function getConfiguration() { 12 + return array( 13 + self::CONFIG_SERIALIZATION => array( 14 + 'properties' => self::SERIALIZATION_JSON, 15 + ), 16 + self::CONFIG_AUX_PHID => true, 17 + ) + parent::getConfiguration(); 18 + } 19 + 20 + public function generatePHID() { 21 + return PhabricatorPHID::generateNewPHID( 22 + PhabricatorAuthPHIDTypeAuthFactor::TYPECONST); 23 + } 24 + 25 + public function getImplementation() { 26 + return idx(PhabricatorAuthFactor::getAllFactors(), $this->getFactorKey()); 27 + } 28 + 29 + }
+5
src/applications/people/storage/PhabricatorUserLog.php
··· 30 30 const ACTION_ENTER_HISEC = 'hisec-enter'; 31 31 const ACTION_EXIT_HISEC = 'hisec-exit'; 32 32 33 + const ACTION_MULTI_ADD = 'multi-add'; 34 + const ACTION_MULTI_REMOVE = 'multi-remove'; 35 + 33 36 protected $actorPHID; 34 37 protected $userPHID; 35 38 protected $action; ··· 63 66 self::ACTION_CHANGE_USERNAME => pht('Change Username'), 64 67 self::ACTION_ENTER_HISEC => pht('Hisec: Enter'), 65 68 self::ACTION_EXIT_HISEC => pht('Hisec: Exit'), 69 + self::ACTION_MULTI_ADD => pht('Multi-Factor: Add Factor'), 70 + self::ACTION_MULTI_REMOVE => pht('Multi-Factor: Remove Factor'), 66 71 ); 67 72 } 68 73
+4
src/applications/settings/panel/PhabricatorSettingsPanelActivity.php
··· 3 3 final class PhabricatorSettingsPanelActivity 4 4 extends PhabricatorSettingsPanel { 5 5 6 + public function isEditableByAdministrators() { 7 + return true; 8 + } 9 + 6 10 public function getPanelKey() { 7 11 return 'activity'; 8 12 }
+309
src/applications/settings/panel/PhabricatorSettingsPanelMultiFactor.php
··· 1 + <?php 2 + 3 + final class PhabricatorSettingsPanelMultiFactor 4 + extends PhabricatorSettingsPanel { 5 + 6 + public function getPanelKey() { 7 + return 'multifactor'; 8 + } 9 + 10 + public function getPanelName() { 11 + return pht('Multi-Factor Auth'); 12 + } 13 + 14 + public function getPanelGroup() { 15 + return pht('Authentication'); 16 + } 17 + 18 + public function isEnabled() { 19 + // TODO: Enable this panel once more pieces work correctly. 20 + return false; 21 + } 22 + 23 + public function processRequest(AphrontRequest $request) { 24 + if ($request->getExists('new')) { 25 + return $this->processNew($request); 26 + } 27 + 28 + if ($request->getExists('edit')) { 29 + return $this->processEdit($request); 30 + } 31 + 32 + if ($request->getExists('delete')) { 33 + return $this->processDelete($request); 34 + } 35 + 36 + $user = $this->getUser(); 37 + $viewer = $request->getUser(); 38 + 39 + $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( 40 + 'userPHID = %s', 41 + $user->getPHID()); 42 + 43 + $rows = array(); 44 + $rowc = array(); 45 + 46 + $highlight_id = $request->getInt('id'); 47 + foreach ($factors as $factor) { 48 + 49 + $impl = $factor->getImplementation(); 50 + if ($impl) { 51 + $type = $impl->getFactorName(); 52 + } else { 53 + $type = $factor->getFactorKey(); 54 + } 55 + 56 + if ($factor->getID() == $highlight_id) { 57 + $rowc[] = 'highlighted'; 58 + } else { 59 + $rowc[] = null; 60 + } 61 + 62 + $rows[] = array( 63 + javelin_tag( 64 + 'a', 65 + array( 66 + 'href' => $this->getPanelURI('?edit='.$factor->getID()), 67 + 'sigil' => 'workflow', 68 + ), 69 + $factor->getFactorName()), 70 + $type, 71 + phabricator_datetime($factor->getDateCreated(), $viewer), 72 + javelin_tag( 73 + 'a', 74 + array( 75 + 'href' => $this->getPanelURI('?delete='.$factor->getID()), 76 + 'sigil' => 'workflow', 77 + 'class' => 'small grey button', 78 + ), 79 + pht('Remove')), 80 + ); 81 + } 82 + 83 + $table = new AphrontTableView($rows); 84 + $table->setNoDataString( 85 + pht("You haven't added any authentication factors to your account yet.")); 86 + $table->setHeaders( 87 + array( 88 + pht('Name'), 89 + pht('Type'), 90 + pht('Created'), 91 + '', 92 + )); 93 + $table->setColumnClasses( 94 + array( 95 + 'wide pri', 96 + '', 97 + 'right', 98 + 'action', 99 + )); 100 + $table->setRowClasses($rowc); 101 + $table->setDeviceVisibility( 102 + array( 103 + true, 104 + false, 105 + false, 106 + true, 107 + )); 108 + 109 + $panel = new PHUIObjectBoxView(); 110 + $header = new PHUIHeaderView(); 111 + 112 + $create_icon = id(new PHUIIconView()) 113 + ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) 114 + ->setSpriteIcon('new'); 115 + $create_button = id(new PHUIButtonView()) 116 + ->setText(pht('Add Authentication Factor')) 117 + ->setHref($this->getPanelURI('?new=true')) 118 + ->setTag('a') 119 + ->setWorkflow(true) 120 + ->setIcon($create_icon); 121 + 122 + $header->setHeader(pht('Authentication Factors')); 123 + $header->addActionLink($create_button); 124 + 125 + $panel->setHeader($header); 126 + $panel->appendChild($table); 127 + 128 + return $panel; 129 + } 130 + 131 + private function processNew(AphrontRequest $request) { 132 + $viewer = $request->getUser(); 133 + $user = $this->getUser(); 134 + 135 + $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( 136 + $viewer, 137 + $request, 138 + $this->getPanelURI()); 139 + 140 + $factors = PhabricatorAuthFactor::getAllFactors(); 141 + 142 + $form = id(new AphrontFormView()) 143 + ->setUser($viewer); 144 + 145 + $type = $request->getStr('type'); 146 + if (empty($factors[$type]) || !$request->isFormPost()) { 147 + $factor = null; 148 + } else { 149 + $factor = $factors[$type]; 150 + } 151 + 152 + $dialog = id(new AphrontDialogView()) 153 + ->setUser($viewer) 154 + ->addHiddenInput('new', true); 155 + 156 + if ($factor === null) { 157 + $choice_control = id(new AphrontFormRadioButtonControl()) 158 + ->setName('type') 159 + ->setValue(key($factors)); 160 + 161 + foreach ($factors as $available_factor) { 162 + $choice_control->addButton( 163 + $available_factor->getFactorKey(), 164 + $available_factor->getFactorName(), 165 + $available_factor->getFactorDescription()); 166 + } 167 + 168 + $dialog->appendParagraph( 169 + pht( 170 + 'Adding an additional authentication factor increases the security '. 171 + 'of your account.')); 172 + 173 + $form 174 + ->appendChild($choice_control); 175 + } else { 176 + $dialog->addHiddenInput('type', $type); 177 + 178 + $config = $factor->processAddFactorForm( 179 + $form, 180 + $request, 181 + $user); 182 + 183 + if ($config) { 184 + $config->save(); 185 + 186 + $log = PhabricatorUserLog::initializeNewLog( 187 + $viewer, 188 + $user->getPHID(), 189 + PhabricatorUserLog::ACTION_MULTI_ADD); 190 + $log->save(); 191 + 192 + return id(new AphrontRedirectResponse()) 193 + ->setURI($this->getPanelURI('?id='.$config->getID())); 194 + } 195 + } 196 + 197 + $dialog 198 + ->setWidth(AphrontDialogView::WIDTH_FORM) 199 + ->setTitle(pht('Add Authentication Factor')) 200 + ->appendChild($form->buildLayoutView()) 201 + ->addSubmitButton(pht('Continue')) 202 + ->addCancelButton($this->getPanelURI()); 203 + 204 + return id(new AphrontDialogResponse()) 205 + ->setDialog($dialog); 206 + } 207 + 208 + private function processEdit(AphrontRequest $request) { 209 + $viewer = $request->getUser(); 210 + $user = $this->getUser(); 211 + 212 + $factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere( 213 + 'id = %d AND userPHID = %s', 214 + $request->getInt('edit'), 215 + $user->getPHID()); 216 + if (!$factor) { 217 + return new Aphront404Response(); 218 + } 219 + 220 + $e_name = true; 221 + $errors = array(); 222 + if ($request->isFormPost()) { 223 + $name = $request->getStr('name'); 224 + if (!strlen($name)) { 225 + $e_name = pht('Required'); 226 + $errors[] = pht( 227 + 'Authentication factors must have a name to identify them.'); 228 + } 229 + 230 + if (!$errors) { 231 + $factor->setFactorName($name); 232 + $factor->save(); 233 + 234 + return id(new AphrontRedirectResponse()) 235 + ->setURI($this->getPanelURI('?id='.$factor->getID())); 236 + } 237 + } else { 238 + $name = $factor->getFactorName(); 239 + } 240 + 241 + $form = id(new AphrontFormView()) 242 + ->setUser($viewer) 243 + ->appendChild( 244 + id(new AphrontFormTextControl()) 245 + ->setName('name') 246 + ->setLabel(pht('Name')) 247 + ->setValue($name) 248 + ->setError($e_name)); 249 + 250 + $dialog = id(new AphrontDialogView()) 251 + ->setUser($viewer) 252 + ->addHiddenInput('edit', $factor->getID()) 253 + ->setTitle(pht('Edit Authentication Factor')) 254 + ->setErrors($errors) 255 + ->appendChild($form->buildLayoutView()) 256 + ->addSubmitButton(pht('Save')) 257 + ->addCancelButton($this->getPanelURI()); 258 + 259 + return id(new AphrontDialogResponse()) 260 + ->setDialog($dialog); 261 + } 262 + 263 + private function processDelete(AphrontRequest $request) { 264 + $viewer = $request->getUser(); 265 + $user = $this->getUser(); 266 + 267 + $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( 268 + $viewer, 269 + $request, 270 + $this->getPanelURI()); 271 + 272 + $factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere( 273 + 'id = %d AND userPHID = %s', 274 + $request->getInt('delete'), 275 + $user->getPHID()); 276 + if (!$factor) { 277 + return new Aphront404Response(); 278 + } 279 + 280 + if ($request->isFormPost()) { 281 + $factor->delete(); 282 + 283 + $log = PhabricatorUserLog::initializeNewLog( 284 + $viewer, 285 + $user->getPHID(), 286 + PhabricatorUserLog::ACTION_MULTI_REMOVE); 287 + $log->save(); 288 + 289 + return id(new AphrontRedirectResponse()) 290 + ->setURI($this->getPanelURI()); 291 + } 292 + 293 + $dialog = id(new AphrontDialogView()) 294 + ->setUser($viewer) 295 + ->addHiddenInput('delete', $factor->getID()) 296 + ->setTitle(pht('Delete Authentication Factor')) 297 + ->appendParagraph( 298 + pht( 299 + 'Really remove the authentication factor %s from your account?', 300 + phutil_tag('strong', array(), $factor->getFactorName()))) 301 + ->addSubmitButton(pht('Remove Factor')) 302 + ->addCancelButton($this->getPanelURI()); 303 + 304 + return id(new AphrontDialogResponse()) 305 + ->setDialog($dialog); 306 + } 307 + 308 + 309 + }