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

Require multiple auth factors to establish web sessions

Summary:
Ref T4398. This prompts users for multi-factor auth on login.

Roughly, this introduces the idea of "partial" sessions, which we haven't finished constructing yet. In practice, this means the session has made it through primary auth but not through multi-factor auth. Add a workflow for bringing a partial session up to a full one.

Test Plan:
- Used Conduit.
- Logged in as multi-factor user.
- Logged in as no-factor user.
- Tried to do non-login-things with a partial session.
- Reviewed account activity logs.

{F149295}

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T4398

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

+190 -27
+2
resources/sql/autopatches/20140430.auth.1.partial.sql
··· 1 + ALTER TABLE {$NAMESPACE}_user.phabricator_session 2 + ADD isPartial BOOL NOT NULL DEFAULT 0;
+2
src/__phutil_library_map__.php
··· 1213 1213 'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php', 1214 1214 'PhabricatorAuthFactorTOTP' => 'applications/auth/factor/PhabricatorAuthFactorTOTP.php', 1215 1215 'PhabricatorAuthFactorTOTPTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTOTPTestCase.php', 1216 + 'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php', 1216 1217 'PhabricatorAuthHighSecurityRequiredException' => 'applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php', 1217 1218 'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php', 1218 1219 'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php', ··· 3980 3981 'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO', 3981 3982 'PhabricatorAuthFactorTOTP' => 'PhabricatorAuthFactor', 3982 3983 'PhabricatorAuthFactorTOTPTestCase' => 'PhabricatorTestCase', 3984 + 'PhabricatorAuthFinishController' => 'PhabricatorAuthController', 3983 3985 'PhabricatorAuthHighSecurityRequiredException' => 'Exception', 3984 3986 'PhabricatorAuthLinkController' => 'PhabricatorAuthController', 3985 3987 'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController',
+4
src/aphront/console/DarkConsoleController.php
··· 16 16 return !PhabricatorEnv::getEnvConfig('darkconsole.always-on'); 17 17 } 18 18 19 + public function shouldAllowPartialSessions() { 20 + return true; 21 + } 22 + 19 23 public function processRequest() { 20 24 $request = $this->getRequest(); 21 25 $user = $request->getUser();
+4
src/aphront/console/DarkConsoleDataController.php
··· 15 15 return !PhabricatorEnv::getEnvConfig('darkconsole.always-on'); 16 16 } 17 17 18 + public function shouldAllowPartialSessions() { 19 + return true; 20 + } 21 + 18 22 public function willProcessRequest(array $data) { 19 23 $this->key = $data['key']; 20 24 }
+1
src/applications/auth/application/PhabricatorApplicationAuth.php
··· 83 83 'register/(?:(?P<akey>[^/]+)/)?' => 'PhabricatorAuthRegisterController', 84 84 'start/' => 'PhabricatorAuthStartController', 85 85 'validate/' => 'PhabricatorAuthValidateController', 86 + 'finish/' => 'PhabricatorAuthFinishController', 86 87 'unlink/(?P<pkey>[^/]+)/' => 'PhabricatorAuthUnlinkController', 87 88 '(?P<action>link|refresh)/(?P<pkey>[^/]+)/' 88 89 => 'PhabricatorAuthLinkController',
+1 -1
src/applications/auth/controller/PhabricatorAuthController.php
··· 82 82 $should_login = $event->getValue('shouldLogin'); 83 83 if ($should_login) { 84 84 $session_key = id(new PhabricatorAuthSessionEngine()) 85 - ->establishSession($session_type, $user->getPHID()); 85 + ->establishSession($session_type, $user->getPHID(), $partial = true); 86 86 87 87 // NOTE: We allow disabled users to login and roadblock them later, so 88 88 // there's no check for users being disabled here.
+71
src/applications/auth/controller/PhabricatorAuthFinishController.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthFinishController 4 + extends PhabricatorAuthController { 5 + 6 + public function shouldRequireLogin() { 7 + return false; 8 + } 9 + 10 + public function shouldAllowPartialSessions() { 11 + return true; 12 + } 13 + 14 + public function processRequest() { 15 + $request = $this->getRequest(); 16 + $viewer = $request->getUser(); 17 + 18 + // If the user already has a full session, just kick them out of here. 19 + $has_partial_session = $viewer->hasSession() && 20 + $viewer->getSession()->getIsPartial(); 21 + if (!$has_partial_session) { 22 + return id(new AphrontRedirectResponse())->setURI('/'); 23 + } 24 + 25 + $engine = new PhabricatorAuthSessionEngine(); 26 + 27 + try { 28 + $token = $engine->requireHighSecuritySession( 29 + $viewer, 30 + $request, 31 + '/logout/'); 32 + } catch (PhabricatorAuthHighSecurityRequiredException $ex) { 33 + $form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm( 34 + $ex->getFactors(), 35 + $ex->getFactorValidationResults(), 36 + $viewer, 37 + $request); 38 + 39 + return $this->newDialog() 40 + ->setTitle(pht('Provide Multi-Factor Credentials')) 41 + ->setShortTitle(pht('Multi-Factor Login')) 42 + ->setWidth(AphrontDialogView::WIDTH_FORM) 43 + ->addHiddenInput(AphrontRequest::TYPE_HISEC, true) 44 + ->appendParagraph( 45 + pht( 46 + 'Welcome, %s. To complete the login process, provide your '. 47 + 'multi-factor credentials.', 48 + phutil_tag('strong', array(), $viewer->getUsername()))) 49 + ->appendChild($form->buildLayoutView()) 50 + ->setSubmitURI($request->getPath()) 51 + ->addCancelButton($ex->getCancelURI()) 52 + ->addSubmitButton(pht('Continue')); 53 + } 54 + 55 + // Upgrade the partial session to a full session. 56 + $engine->upgradePartialSession($viewer); 57 + 58 + // TODO: It might be nice to add options like "bind this session to my IP" 59 + // here, even for accounts without multi-factor auth attached to them. 60 + 61 + $next = PhabricatorCookies::getNextURICookie($request); 62 + $request->clearCookie(PhabricatorCookies::COOKIE_NEXTURI); 63 + 64 + if (!PhabricatorEnv::isValidLocalWebResource($next)) { 65 + $next = '/'; 66 + } 67 + 68 + return id(new AphrontRedirectResponse())->setURI($next); 69 + } 70 + 71 + }
+6 -8
src/applications/auth/controller/PhabricatorAuthValidateController.php
··· 7 7 return false; 8 8 } 9 9 10 + public function shouldAllowPartialSessions() { 11 + return true; 12 + } 13 + 10 14 public function processRequest() { 11 15 $request = $this->getRequest(); 12 16 $viewer = $request->getUser(); ··· 54 58 return $this->renderErrors($failures); 55 59 } 56 60 57 - $next = PhabricatorCookies::getNextURICookie($request); 58 - $request->clearCookie(PhabricatorCookies::COOKIE_NEXTURI); 59 - 60 - if (!PhabricatorEnv::isValidLocalWebResource($next)) { 61 - $next = '/'; 62 - } 63 - 64 - return id(new AphrontRedirectResponse())->setURI($next); 61 + $finish_uri = $this->getApplicationURI('finish/'); 62 + return id(new AphrontRedirectResponse())->setURI($finish_uri); 65 63 } 66 64 67 65 private function renderErrors(array $messages) {
+4
src/applications/auth/controller/PhabricatorLogoutController.php
··· 17 17 return false; 18 18 } 19 19 20 + public function shouldAllowPartialSessions() { 21 + return true; 22 + } 23 + 20 24 public function processRequest() { 21 25 $request = $this->getRequest(); 22 26 $user = $request->getUser();
+62 -9
src/applications/auth/engine/PhabricatorAuthSessionEngine.php
··· 94 94 s.sessionExpires AS s_sessionExpires, 95 95 s.sessionStart AS s_sessionStart, 96 96 s.highSecurityUntil AS s_highSecurityUntil, 97 + s.isPartial AS s_isPartial, 97 98 u.* 98 99 FROM %T u JOIN %T s ON u.phid = s.userPHID 99 100 AND s.type = %s AND s.sessionKey = %s', ··· 159 160 * @{class:PhabricatorAuthSession}). 160 161 * @param phid|null Identity to establish a session for, usually a user 161 162 * PHID. With `null`, generates an anonymous session. 163 + * @param bool True to issue a partial session. 162 164 * @return string Newly generated session key. 163 165 */ 164 - public function establishSession($session_type, $identity_phid) { 166 + public function establishSession($session_type, $identity_phid, $partial) { 165 167 // Consume entropy to generate a new session key, forestalling the eventual 166 168 // heat death of the universe. 167 169 $session_key = Filesystem::readRandomCharacters(40); ··· 176 178 // This has a side effect of validating the session type. 177 179 $session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type); 178 180 181 + $digest_key = PhabricatorHash::digest($session_key); 182 + 179 183 // Logging-in users don't have CSRF stuff yet, so we have to unguard this 180 184 // write. 181 185 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 182 186 id(new PhabricatorAuthSession()) 183 187 ->setUserPHID($identity_phid) 184 188 ->setType($session_type) 185 - ->setSessionKey(PhabricatorHash::digest($session_key)) 189 + ->setSessionKey($digest_key) 186 190 ->setSessionStart(time()) 187 191 ->setSessionExpires(time() + $session_ttl) 192 + ->setIsPartial($partial ? 1 : 0) 188 193 ->save(); 189 194 190 195 $log = PhabricatorUserLog::initializeNewLog( 191 196 null, 192 197 $identity_phid, 193 - PhabricatorUserLog::ACTION_LOGIN); 198 + ($partial 199 + ? PhabricatorUserLog::ACTION_LOGIN_PARTIAL 200 + : PhabricatorUserLog::ACTION_LOGIN)); 201 + 194 202 $log->setDetails( 195 203 array( 196 204 'session_type' => $session_type, 197 205 )); 198 - $log->setSession($session_key); 206 + $log->setSession($digest_key); 199 207 $log->save(); 200 208 unset($unguarded); 201 209 ··· 287 295 new PhabricatorAuthTryFactorAction(), 288 296 -1); 289 297 298 + if ($session->getIsPartial()) { 299 + // If we have a partial session, just issue a token without 300 + // putting it in high security mode. 301 + return $this->issueHighSecurityToken($session, true); 302 + } 303 + 290 304 $until = time() + phutil_units('15 minutes in seconds'); 291 305 $session->setHighSecurityUntil($until); 292 306 ··· 303 317 PhabricatorUserLog::ACTION_ENTER_HISEC); 304 318 $log->save(); 305 319 } else { 306 - 307 - 308 - 309 320 $log = PhabricatorUserLog::initializeNewLog( 310 321 $viewer, 311 322 $viewer->getPHID(), ··· 331 342 * Issue a high security token for a session, if authorized. 332 343 * 333 344 * @param PhabricatorAuthSession Session to issue a token for. 345 + * @param bool Force token issue. 334 346 * @return PhabricatorAuthHighSecurityToken|null Token, if authorized. 335 347 */ 336 - private function issueHighSecurityToken(PhabricatorAuthSession $session) { 348 + private function issueHighSecurityToken( 349 + PhabricatorAuthSession $session, 350 + $force = false) { 351 + 337 352 $until = $session->getHighSecurityUntil(); 338 - if ($until > time()) { 353 + if ($until > time() || $force) { 339 354 return new PhabricatorAuthHighSecurityToken(); 340 355 } 341 356 return null; ··· 388 403 $viewer->getPHID(), 389 404 PhabricatorUserLog::ACTION_EXIT_HISEC); 390 405 $log->save(); 406 + } 407 + 408 + 409 + /** 410 + * Upgrade a partial session to a full session. 411 + * 412 + * @param PhabricatorAuthSession Session to upgrade. 413 + * @return void 414 + */ 415 + public function upgradePartialSession(PhabricatorUser $viewer) { 416 + if (!$viewer->hasSession()) { 417 + throw new Exception( 418 + pht('Upgrading partial session of user with no session!')); 419 + } 420 + 421 + $session = $viewer->getSession(); 422 + 423 + if (!$session->getIsPartial()) { 424 + throw new Exception(pht('Session is not partial!')); 425 + } 426 + 427 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 428 + $session->setIsPartial(0); 429 + 430 + queryfx( 431 + $session->establishConnection('w'), 432 + 'UPDATE %T SET isPartial = %d WHERE id = %d', 433 + $session->getTableName(), 434 + 0, 435 + $session->getID()); 436 + 437 + $log = PhabricatorUserLog::initializeNewLog( 438 + $viewer, 439 + $viewer->getPHID(), 440 + PhabricatorUserLog::ACTION_LOGIN_FULL); 441 + $log->save(); 442 + unset($unguarded); 443 + 391 444 } 392 445 393 446 }
+1
src/applications/auth/storage/PhabricatorAuthSession.php
··· 12 12 protected $sessionStart; 13 13 protected $sessionExpires; 14 14 protected $highSecurityUntil; 15 + protected $isPartial; 15 16 16 17 private $identityObject = self::ATTACHABLE; 17 18
+19 -4
src/applications/base/controller/PhabricatorController.php
··· 20 20 return false; 21 21 } 22 22 23 + public function shouldAllowPartialSessions() { 24 + return false; 25 + } 26 + 23 27 public function shouldRequireEmailVerification() { 24 28 return PhabricatorUserEmail::isEmailVerificationRequired(); 25 29 } ··· 53 57 // session. This is used to provide CSRF protection to logged-out users. 54 58 $phsid = $session_engine->establishSession( 55 59 PhabricatorAuthSession::TYPE_WEB, 56 - null); 60 + null, 61 + $partial = false); 57 62 58 63 // This may be a resource request, in which case we just don't set 59 64 // the cookie. ··· 133 138 return $this->delegateToController($checker_controller); 134 139 } 135 140 141 + $auth_class = 'PhabricatorApplicationAuth'; 142 + $auth_application = PhabricatorApplication::getByClass($auth_class); 143 + 144 + // Require partial sessions to finish login before doing anything. 145 + if (!$this->shouldAllowPartialSessions()) { 146 + if ($user->hasSession() && 147 + $user->getSession()->getIsPartial()) { 148 + $login_controller = new PhabricatorAuthFinishController($request); 149 + $this->setCurrentApplication($auth_application); 150 + return $this->delegateToController($login_controller); 151 + } 152 + } 153 + 136 154 if ($this->shouldRequireLogin()) { 137 155 // This actually means we need either: 138 156 // - a valid user, or a public controller; and 139 157 // - permission to see the application. 140 - 141 - $auth_class = 'PhabricatorApplicationAuth'; 142 - $auth_application = PhabricatorApplication::getByClass($auth_class); 143 158 144 159 $allow_public = $this->shouldAllowPublic() && 145 160 PhabricatorEnv::getEnvConfig('policy.allow-public');
+4 -4
src/applications/conduit/method/ConduitAPI_conduit_connect_Method.php
··· 145 145 if ($valid != $signature) { 146 146 throw new ConduitException('ERR-INVALID-CERTIFICATE'); 147 147 } 148 - $session_key = id(new PhabricatorAuthSessionEngine()) 149 - ->establishSession( 150 - PhabricatorAuthSession::TYPE_CONDUIT, 151 - $user->getPHID()); 148 + $session_key = id(new PhabricatorAuthSessionEngine())->establishSession( 149 + PhabricatorAuthSession::TYPE_CONDUIT, 150 + $user->getPHID(), 151 + $partial = false); 152 152 } else { 153 153 throw new ConduitException('ERR-NO-CERTIFICATE'); 154 154 }
+5 -1
src/applications/people/storage/PhabricatorUserLog.php
··· 4 4 implements PhabricatorPolicyInterface { 5 5 6 6 const ACTION_LOGIN = 'login'; 7 + const ACTION_LOGIN_PARTIAL = 'login-partial'; 8 + const ACTION_LOGIN_FULL = 'login-full'; 7 9 const ACTION_LOGOUT = 'logout'; 8 10 const ACTION_LOGIN_FAILURE = 'login-fail'; 9 11 const ACTION_RESET_PASSWORD = 'reset-pass'; ··· 46 48 public static function getActionTypeMap() { 47 49 return array( 48 50 self::ACTION_LOGIN => pht('Login'), 49 - self::ACTION_LOGIN_FAILURE => pht('Login Failure'), 51 + self::ACTION_LOGIN_PARTIAL => pht('Login: Partial Login'), 52 + self::ACTION_LOGIN_FULL => pht('Login: Upgrade to Full'), 53 + self::ACTION_LOGIN_FAILURE => pht('Login: Failure'), 50 54 self::ACTION_LOGOUT => pht('Logout'), 51 55 self::ACTION_RESET_PASSWORD => pht('Reset Password'), 52 56 self::ACTION_CREATE => pht('Create Account'),
+4
src/infrastructure/celerity/CelerityResourceController.php
··· 14 14 return false; 15 15 } 16 16 17 + public function shouldAllowPartialSessions() { 18 + return true; 19 + } 20 + 17 21 abstract public function getCelerityResourceMap(); 18 22 19 23 protected function serveResource($path, $package_hash = null) {