@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
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add "High Security" mode to support multi-factor auth

Summary:
Ref T4398. This is roughly a "sudo" mode, like GitHub has for accessing SSH keys, or Facebook has for managing credit cards. GitHub actually calls theirs "sudo" mode, but I think that's too technical for big parts of our audience. I've gone with "high security mode".

This doesn't actually get exposed in the UI yet (and we don't have any meaningful auth factors to prompt the user for) but the workflow works overall. I'll go through it in a comment, since I need to arrange some screenshots.

Test Plan: See guided walkthrough.

Reviewers: chad, btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T4398

Differential Revision: https://secure.phabricator.com/D8851

+346 -11
+2
resources/sql/autopatches/20140423.session.1.hisec.sql
··· 1 + ALTER TABLE {$NAMESPACE}_user.phabricator_session 2 + ADD highSecurityUntil INT UNSIGNED;
+5
src/__phutil_library_map__.php
··· 1206 1206 'PhabricatorAuthController' => 'applications/auth/controller/PhabricatorAuthController.php', 1207 1207 'PhabricatorAuthDAO' => 'applications/auth/storage/PhabricatorAuthDAO.php', 1208 1208 'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php', 1209 + 'PhabricatorAuthDowngradeSessionController' => 'applications/auth/controller/PhabricatorAuthDowngradeSessionController.php', 1209 1210 'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php', 1211 + 'PhabricatorAuthHighSecurityRequiredException' => 'applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php', 1212 + 'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php', 1210 1213 'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php', 1211 1214 'PhabricatorAuthListController' => 'applications/auth/controller/config/PhabricatorAuthListController.php', 1212 1215 'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php', ··· 3947 3950 'PhabricatorAuthController' => 'PhabricatorController', 3948 3951 'PhabricatorAuthDAO' => 'PhabricatorLiskDAO', 3949 3952 'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController', 3953 + 'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController', 3950 3954 'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController', 3955 + 'PhabricatorAuthHighSecurityRequiredException' => 'Exception', 3951 3956 'PhabricatorAuthLinkController' => 'PhabricatorAuthController', 3952 3957 'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController', 3953 3958 'PhabricatorAuthLoginController' => 'PhabricatorAuthController',
+2
src/aphront/AphrontRequest.php
··· 18 18 const TYPE_WORKFLOW = '__wflow__'; 19 19 const TYPE_CONTINUE = '__continue__'; 20 20 const TYPE_PREVIEW = '__preview__'; 21 + const TYPE_HISEC = '__hisec__'; 21 22 22 23 private $host; 23 24 private $path; ··· 263 264 264 265 final public function isFormPost() { 265 266 $post = $this->getExists(self::TYPE_FORM) && 267 + !$this->getExists(self::TYPE_HISEC) && 266 268 $this->isHTTPPost(); 267 269 268 270 if (!$post) {
+43
src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
··· 123 123 return $response; 124 124 } 125 125 126 + if ($ex instanceof PhabricatorAuthHighSecurityRequiredException) { 127 + 128 + $form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm( 129 + $user, 130 + $request); 131 + 132 + $dialog = id(new AphrontDialogView()) 133 + ->setUser($user) 134 + ->setTitle(pht('Entering High Security')) 135 + ->setShortTitle(pht('Security Checkpoint')) 136 + ->setWidth(AphrontDialogView::WIDTH_FORM) 137 + ->addHiddenInput(AphrontRequest::TYPE_HISEC, true) 138 + ->setErrors( 139 + array( 140 + pht( 141 + 'You are taking an action which requires you to enter '. 142 + 'high security.'), 143 + )) 144 + ->appendParagraph( 145 + pht( 146 + 'High security mode helps protect your account from security '. 147 + 'threats, like session theft or someone messing with your stuff '. 148 + 'while you\'re grabbing a coffee. To enter high security mode, '. 149 + 'confirm your credentials.')) 150 + ->appendChild($form->buildLayoutView()) 151 + ->appendParagraph( 152 + pht( 153 + 'Your account will remain in high security mode for a short '. 154 + 'period of time. When you are finished taking sensitive '. 155 + 'actions, you should leave high security.')) 156 + ->setSubmitURI($request->getPath()) 157 + ->addCancelButton($ex->getCancelURI()) 158 + ->addSubmitButton(pht('Enter High Security')); 159 + 160 + foreach ($request->getPassthroughRequestParameters() as $key => $value) { 161 + $dialog->addHiddenInput($key, $value); 162 + } 163 + 164 + $response = new AphrontDialogResponse(); 165 + $response->setDialog($dialog); 166 + return $response; 167 + } 168 + 126 169 if ($ex instanceof PhabricatorPolicyException) { 127 170 128 171 if (!$user->isLoggedIn()) {
+2
src/applications/auth/application/PhabricatorApplicationAuth.php
··· 88 88 => 'PhabricatorAuthConfirmLinkController', 89 89 'session/terminate/(?P<id>[^/]+)/' 90 90 => 'PhabricatorAuthTerminateSessionController', 91 + 'session/downgrade/' 92 + => 'PhabricatorAuthDowngradeSessionController', 91 93 ), 92 94 93 95 '/oauth/(?P<provider>\w+)/login/'
+52
src/applications/auth/controller/PhabricatorAuthDowngradeSessionController.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthDowngradeSessionController 4 + extends PhabricatorAuthController { 5 + 6 + public function processRequest() { 7 + $request = $this->getRequest(); 8 + $viewer = $request->getUser(); 9 + 10 + $panel_uri = '/settings/panel/sessions/'; 11 + 12 + $session = $viewer->getSession(); 13 + if ($session->getHighSecurityUntil() < time()) { 14 + return $this->newDialog() 15 + ->setTitle(pht('Normal Security Restored')) 16 + ->appendParagraph( 17 + pht('Your session is no longer in high security.')) 18 + ->addCancelButton($panel_uri, pht('Continue')); 19 + } 20 + 21 + if ($request->isFormPost()) { 22 + 23 + queryfx( 24 + $session->establishConnection('w'), 25 + 'UPDATE %T SET highSecurityUntil = NULL WHERE id = %d', 26 + $session->getTableName(), 27 + $session->getID()); 28 + 29 + return id(new AphrontRedirectResponse()) 30 + ->setURI($this->getApplicationURI('session/downgrade/')); 31 + } 32 + 33 + return $this->newDialog() 34 + ->setTitle(pht('Leaving High Security')) 35 + ->appendParagraph( 36 + pht( 37 + 'Leave high security and return your session to normal '. 38 + 'security levels?')) 39 + ->appendParagraph( 40 + pht( 41 + 'If you leave high security, you will need to authenticate '. 42 + 'again the next time you try to take a high security action.')) 43 + ->appendParagraph( 44 + pht( 45 + 'On the plus side, that purple notification bubble will '. 46 + 'disappear.')) 47 + ->addSubmitButton(pht('Leave High Security')) 48 + ->addCancelButton($panel_uri, pht('Stay in High Security')); 49 + } 50 + 51 + 52 + }
+5
src/applications/auth/data/PhabricatorAuthHighSecurityToken.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthHighSecurityToken { 4 + 5 + }
+131 -11
src/applications/auth/engine/PhabricatorAuthSessionEngine.php
··· 1 1 <?php 2 2 3 + /** 4 + * @task hisec High Security Mode 5 + */ 3 6 final class PhabricatorAuthSessionEngine extends Phobject { 4 7 5 8 /** ··· 78 81 $session_table = new PhabricatorAuthSession(); 79 82 $user_table = new PhabricatorUser(); 80 83 $conn_r = $session_table->establishConnection('r'); 84 + $session_key = PhabricatorHash::digest($session_token); 81 85 82 86 // NOTE: We're being clever here because this happens on every page load, 83 - // and by joining we can save a query. 87 + // and by joining we can save a query. This might be getting too clever 88 + // for its own good, though... 84 89 85 90 $info = queryfx_one( 86 91 $conn_r, 87 - 'SELECT s.sessionExpires AS _sessionExpires, s.id AS _sessionID, u.* 92 + 'SELECT 93 + s.id AS s_id, 94 + s.sessionExpires AS s_sessionExpires, 95 + s.sessionStart AS s_sessionStart, 96 + s.highSecurityUntil AS s_highSecurityUntil, 97 + u.* 88 98 FROM %T u JOIN %T s ON u.phid = s.userPHID 89 99 AND s.type = %s AND s.sessionKey = %s', 90 100 $user_table->getTableName(), 91 101 $session_table->getTableName(), 92 102 $session_type, 93 - PhabricatorHash::digest($session_token)); 103 + $session_key); 94 104 95 105 if (!$info) { 96 106 return null; 97 107 } 98 108 99 - $expires = $info['_sessionExpires']; 100 - $id = $info['_sessionID']; 101 - unset($info['_sessionExpires']); 102 - unset($info['_sessionID']); 109 + $session_dict = array( 110 + 'userPHID' => $info['phid'], 111 + 'sessionKey' => $session_key, 112 + 'type' => $session_type, 113 + ); 114 + foreach ($info as $key => $value) { 115 + if (strncmp($key, 's_', 2) === 0) { 116 + unset($info[$key]); 117 + $session_dict[substr($key, 2)] = $value; 118 + } 119 + } 120 + $session = id(new PhabricatorAuthSession())->loadFromArray($session_dict); 103 121 104 122 $ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type); 105 123 ··· 107 125 // TTL back up to the full duration. The idea here is that sessions are 108 126 // good forever if used regularly, but get GC'd when they fall out of use. 109 127 110 - if (time() + (0.80 * $ttl) > $expires) { 128 + if (time() + (0.80 * $ttl) > $session->getSessionExpires()) { 111 129 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 112 130 $conn_w = $session_table->establishConnection('w'); 113 131 queryfx( 114 132 $conn_w, 115 133 'UPDATE %T SET sessionExpires = UNIX_TIMESTAMP() + %d WHERE id = %d', 116 - $session_table->getTableName(), 134 + $session->getTableName(), 117 135 $ttl, 118 - $id); 136 + $session->getID()); 119 137 unset($unguarded); 120 138 } 121 139 122 - return $user_table->loadFromArray($info); 140 + $user = $user_table->loadFromArray($info); 141 + $user->attachSession($session); 142 + return $user; 123 143 } 124 144 125 145 ··· 181 201 182 202 return $session_key; 183 203 } 204 + 205 + 206 + /** 207 + * Require high security, or prompt the user to enter high security. 208 + * 209 + * If the user's session is in high security, this method will return a 210 + * token. Otherwise, it will throw an exception which will eventually 211 + * be converted into a multi-factor authentication workflow. 212 + * 213 + * @param PhabricatorUser User whose session needs to be in high security. 214 + * @param AphrontReqeust Current request. 215 + * @param string URI to return the user to if they cancel. 216 + * @return PhabricatorAuthHighSecurityToken Security token. 217 + */ 218 + public function requireHighSecuritySession( 219 + PhabricatorUser $viewer, 220 + AphrontRequest $request, 221 + $cancel_uri) { 222 + 223 + if (!$viewer->hasSession()) { 224 + throw new Exception( 225 + pht('Requiring a high-security session from a user with no session!')); 226 + } 227 + 228 + $session = $viewer->getSession(); 229 + 230 + $token = $this->issueHighSecurityToken($session); 231 + if ($token) { 232 + return $token; 233 + } 234 + 235 + if ($request->isHTTPPost()) { 236 + $request->validateCSRF(); 237 + if ($request->getExists(AphrontRequest::TYPE_HISEC)) { 238 + 239 + // TODO: Actually verify that the user provided some multi-factor 240 + // auth credentials here. For now, we just let you enter high 241 + // security. 242 + 243 + $until = time() + phutil_units('15 minutes in seconds'); 244 + $session->setHighSecurityUntil($until); 245 + 246 + queryfx( 247 + $session->establishConnection('w'), 248 + 'UPDATE %T SET highSecurityUntil = %d WHERE id = %d', 249 + $session->getTableName(), 250 + $until, 251 + $session->getID()); 252 + } 253 + } 254 + 255 + $token = $this->issueHighSecurityToken($session); 256 + if ($token) { 257 + return $token; 258 + } 259 + 260 + throw id(new PhabricatorAuthHighSecurityRequiredException()) 261 + ->setCancelURI($cancel_uri); 262 + } 263 + 264 + 265 + /** 266 + * Issue a high security token for a session, if authorized. 267 + * 268 + * @param PhabricatorAuthSession Session to issue a token for. 269 + * @return PhabricatorAuthHighSecurityToken|null Token, if authorized. 270 + */ 271 + private function issueHighSecurityToken(PhabricatorAuthSession $session) { 272 + $until = $session->getHighSecurityUntil(); 273 + if ($until > time()) { 274 + return new PhabricatorAuthHighSecurityToken(); 275 + } 276 + return null; 277 + } 278 + 279 + 280 + /** 281 + * Render a form for providing relevant multi-factor credentials. 282 + * 283 + * @param PhabricatorUser Viewing user. 284 + * @param AphrontRequest Current request. 285 + * @return AphrontFormView Renderable form. 286 + */ 287 + public function renderHighSecurityForm( 288 + PhabricatorUser $viewer, 289 + AphrontRequest $request) { 290 + 291 + // TODO: This is stubbed. 292 + 293 + $form = id(new AphrontFormView()) 294 + ->setUser($viewer) 295 + ->appendRemarkupInstructions('') 296 + ->appendChild( 297 + id(new AphrontFormTextControl()) 298 + ->setLabel(pht('Secret Stuff'))) 299 + ->appendRemarkupInstructions(''); 300 + 301 + return $form; 302 + } 303 + 184 304 185 305 }
+16
src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthHighSecurityRequiredException extends Exception { 4 + 5 + private $cancelURI; 6 + 7 + public function setCancelURI($cancel_uri) { 8 + $this->cancelURI = $cancel_uri; 9 + return $this; 10 + } 11 + 12 + public function getCancelURI() { 13 + return $this->cancelURI; 14 + } 15 + 16 + }
+1
src/applications/auth/storage/PhabricatorAuthSession.php
··· 11 11 protected $sessionKey; 12 12 protected $sessionStart; 13 13 protected $sessionExpires; 14 + protected $highSecurityUntil; 14 15 15 16 private $identityObject = self::ATTACHABLE; 16 17
+14
src/applications/people/storage/PhabricatorUser.php
··· 42 42 private $customFields = self::ATTACHABLE; 43 43 44 44 private $alternateCSRFString = self::ATTACHABLE; 45 + private $session = self::ATTACHABLE; 45 46 46 47 protected function readField($field) { 47 48 switch ($field) { ··· 176 177 ->queueDocumentForIndexing($this->getPHID()); 177 178 178 179 return $result; 180 + } 181 + 182 + public function attachSession(PhabricatorAuthSession $session) { 183 + $this->session = $session; 184 + return $this; 185 + } 186 + 187 + public function getSession() { 188 + return $this->assertAttached($this->session); 189 + } 190 + 191 + public function hasSession() { 192 + return ($this->session !== self::ATTACHABLE); 179 193 } 180 194 181 195 private function generateConduitCertificate() {
+12
src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php
··· 38 38 return $this->renderKeyListView($request); 39 39 } 40 40 41 + /* 42 + 43 + NOTE: Uncomment this to test hisec. 44 + TOOD: Implement this fully once hisec does something useful. 45 + 46 + $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( 47 + $viewer, 48 + $request, 49 + '/settings/panel/ssh/'); 50 + 51 + */ 52 + 41 53 $id = nonempty($edit, $delete); 42 54 43 55 if ($id && is_numeric($id)) {
+21
src/applications/settings/panel/PhabricatorSettingsPanelSessions.php
··· 66 66 pht('Terminate')); 67 67 } 68 68 69 + $hisec = ($session->getHighSecurityUntil() - time()); 70 + 69 71 $rows[] = array( 70 72 $handles[$session->getUserPHID()]->renderLink(), 71 73 substr($session->getSessionKey(), 0, 6), 72 74 $session->getType(), 75 + ($hisec > 0) 76 + ? phabricator_format_relative_time($hisec) 77 + : null, 73 78 phabricator_datetime($session->getSessionStart(), $viewer), 74 79 phabricator_date($session->getSessionExpires(), $viewer), 75 80 $button, ··· 84 89 pht('Identity'), 85 90 pht('Session'), 86 91 pht('Type'), 92 + pht('HiSec'), 87 93 pht('Created'), 88 94 pht('Expires'), 89 95 pht(''), ··· 95 101 '', 96 102 'right', 97 103 'right', 104 + 'right', 98 105 'action', 99 106 )); 100 107 ··· 112 119 $header = id(new PHUIHeaderView()) 113 120 ->setHeader(pht('Active Login Sessions')) 114 121 ->addActionLink($terminate_button); 122 + 123 + $hisec = ($viewer->getSession()->getHighSecurityUntil() - time()); 124 + if ($hisec > 0) { 125 + $hisec_icon = id(new PHUIIconView()) 126 + ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) 127 + ->setSpriteIcon('lock'); 128 + $hisec_button = id(new PHUIButtonView()) 129 + ->setText(pht('Leave High Security')) 130 + ->setHref('/auth/session/downgrade/') 131 + ->setTag('a') 132 + ->setWorkflow(true) 133 + ->setIcon($hisec_icon); 134 + $header->addActionLink($hisec_button); 135 + } 115 136 116 137 $panel = id(new PHUIObjectBoxView()) 117 138 ->setHeader($header)
+16
src/view/page/PhabricatorStandardPageView.php
··· 168 168 169 169 Javelin::initBehavior('device'); 170 170 171 + if ($user->hasSession()) { 172 + $hisec = ($user->getSession()->getHighSecurityUntil() - time()); 173 + if ($hisec > 0) { 174 + $remaining_time = phabricator_format_relative_time($hisec); 175 + Javelin::initBehavior( 176 + 'high-security-warning', 177 + array( 178 + 'uri' => '/auth/session/downgrade/', 179 + 'message' => pht( 180 + 'Your session is in high security mode. When you '. 181 + 'finish using it, click here to leave.', 182 + $remaining_time), 183 + )); 184 + } 185 + } 186 + 171 187 if ($console) { 172 188 require_celerity_resource('aphront-dark-console-css'); 173 189
+5
webroot/rsrc/css/aphront/notification.css
··· 47 47 border: 1px solid {$red}; 48 48 } 49 49 50 + .jx-notification-security { 51 + background: {$lightviolet}; 52 + border: 1px solid {$violet}; 53 + } 54 + 50 55 .jx-notification-container .phabricator-notification { 51 56 padding: 0; 52 57 }
+19
webroot/rsrc/js/core/behavior-high-security-warning.js
··· 1 + /** 2 + * @provides javelin-behavior-high-security-warning 3 + * @requires javelin-behavior 4 + * javelin-uri 5 + * phabricator-notification 6 + */ 7 + 8 + JX.behavior('high-security-warning', function(config) { 9 + 10 + var n = new JX.Notification() 11 + .setContent(config.message) 12 + .setDuration(0) 13 + .alterClassName('jx-notification-security', true); 14 + 15 + n.listen('activate', function() { JX.$U(config.uri).go(); }); 16 + 17 + n.show(); 18 + 19 + });