@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 CSRF to SMS challenges, and pave the way for more MFA types (including Duo)

Summary:
Depends on D20026. Ref T13222. Ref T13231. The primary change here is that we'll no longer send you an SMS if you hit an MFA gate without CSRF tokens.

Then there's a lot of support for genralizing into Duo (and other push factors, potentially), I'll annotate things inline.

Test Plan: Implemented Duo, elsewhere.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13231, T13222

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

+279 -113
+5 -5
resources/celerity/map.php
··· 9 9 'names' => array( 10 10 'conpherence.pkg.css' => '3c8a0668', 11 11 'conpherence.pkg.js' => '020aebcf', 12 - 'core.pkg.css' => 'a66ea2e7', 12 + 'core.pkg.css' => 'e0cb8094', 13 13 'core.pkg.js' => '5c737607', 14 14 'differential.pkg.css' => 'b8df73d4', 15 15 'differential.pkg.js' => '67c9ea4c', ··· 151 151 'rsrc/css/phui/phui-document.css' => '52b748a5', 152 152 'rsrc/css/phui/phui-feed-story.css' => 'a0c05029', 153 153 'rsrc/css/phui/phui-fontkit.css' => '9b714a5e', 154 - 'rsrc/css/phui/phui-form-view.css' => '9508671e', 154 + 'rsrc/css/phui/phui-form-view.css' => '0807e7ac', 155 155 'rsrc/css/phui/phui-form.css' => '159e2d9c', 156 156 'rsrc/css/phui/phui-head-thing.css' => 'd7f293df', 157 157 'rsrc/css/phui/phui-header-view.css' => '93cea4ec', ··· 159 159 'rsrc/css/phui/phui-icon-set-selector.css' => '7aa5f3ec', 160 160 'rsrc/css/phui/phui-icon.css' => '281f964d', 161 161 'rsrc/css/phui/phui-image-mask.css' => '62c7f4d2', 162 - 'rsrc/css/phui/phui-info-view.css' => 'f9464caf', 162 + 'rsrc/css/phui/phui-info-view.css' => '37b8d9ce', 163 163 'rsrc/css/phui/phui-invisible-character-view.css' => 'c694c4a4', 164 164 'rsrc/css/phui/phui-left-right.css' => '68513c34', 165 165 'rsrc/css/phui/phui-lightbox.css' => '4ebf22da', ··· 817 817 'phui-font-icon-base-css' => 'd7994e06', 818 818 'phui-fontkit-css' => '9b714a5e', 819 819 'phui-form-css' => '159e2d9c', 820 - 'phui-form-view-css' => '9508671e', 820 + 'phui-form-view-css' => '0807e7ac', 821 821 'phui-head-thing-view-css' => 'd7f293df', 822 822 'phui-header-view-css' => '93cea4ec', 823 823 'phui-hovercard' => '074f0783', ··· 825 825 'phui-icon-set-selector-css' => '7aa5f3ec', 826 826 'phui-icon-view-css' => '281f964d', 827 827 'phui-image-mask-css' => '62c7f4d2', 828 - 'phui-info-view-css' => 'f9464caf', 828 + 'phui-info-view-css' => '37b8d9ce', 829 829 'phui-inline-comment-view-css' => '48acce5b', 830 830 'phui-invisible-character-view-css' => 'c694c4a4', 831 831 'phui-left-right-css' => '68513c34',
+2
src/__phutil_library_map__.php
··· 2239 2239 'PhabricatorAuthFactorProviderTransactionType' => 'applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php', 2240 2240 'PhabricatorAuthFactorProviderViewController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php', 2241 2241 'PhabricatorAuthFactorResult' => 'applications/auth/factor/PhabricatorAuthFactorResult.php', 2242 + 'PhabricatorAuthFactorResultException' => 'applications/auth/exception/PhabricatorAuthFactorResultException.php', 2242 2243 'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php', 2243 2244 'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php', 2244 2245 'PhabricatorAuthHMACKey' => 'applications/auth/storage/PhabricatorAuthHMACKey.php', ··· 7965 7966 'PhabricatorAuthFactorProviderTransactionType' => 'PhabricatorModularTransactionType', 7966 7967 'PhabricatorAuthFactorProviderViewController' => 'PhabricatorAuthFactorProviderController', 7967 7968 'PhabricatorAuthFactorResult' => 'Phobject', 7969 + 'PhabricatorAuthFactorResultException' => 'Exception', 7968 7970 'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase', 7969 7971 'PhabricatorAuthFinishController' => 'PhabricatorAuthController', 7970 7972 'PhabricatorAuthHMACKey' => 'PhabricatorAuthDAO',
+18 -12
src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php
··· 38 38 $request); 39 39 40 40 $is_wait = false; 41 + $is_continue = false; 41 42 foreach ($results as $result) { 42 43 if ($result->getIsWait()) { 43 44 $is_wait = true; 44 - break; 45 + } 46 + 47 + if ($result->getIsContinue()) { 48 + $is_continue = true; 45 49 } 46 50 } 47 51 ··· 55 59 56 60 if ($is_wait) { 57 61 $submit = pht('Wait Patiently'); 58 - } else if ($is_upgrade) { 62 + } else if ($is_upgrade && !$is_continue) { 59 63 $submit = pht('Enter High Security'); 60 64 } else { 61 65 $submit = pht('Continue'); ··· 74 78 $form_layout = $form->buildLayoutView(); 75 79 76 80 if ($is_upgrade) { 81 + $messages = array( 82 + pht( 83 + 'You are taking an action which requires you to enter '. 84 + 'high security.'), 85 + ); 86 + 87 + $info_view = id(new PHUIInfoView()) 88 + ->setSeverity(PHUIInfoView::SEVERITY_MFA) 89 + ->setErrors($messages); 90 + 77 91 $dialog 78 - ->setErrors( 79 - array( 80 - pht( 81 - 'You are taking an action which requires you to enter '. 82 - 'high security.'), 83 - )) 92 + ->appendChild($info_view) 84 93 ->appendParagraph( 85 94 pht( 86 - 'High security mode helps protect your account from security '. 87 - 'threats, like session theft or someone messing with your stuff '. 88 - 'while you\'re grabbing a coffee. To enter high security mode, '. 89 - 'confirm your credentials.')) 95 + 'To enter high security mode, confirm your credentials:')) 90 96 ->appendChild($form_layout) 91 97 ->appendParagraph( 92 98 pht(
+21 -5
src/applications/auth/engine/PhabricatorAuthSessionEngine.php
··· 47 47 48 48 49 49 private $workflowKey; 50 + private $request; 50 51 51 52 public function setWorkflowKey($workflow_key) { 52 53 $this->workflowKey = $workflow_key; ··· 63 64 } 64 65 65 66 return $this->workflowKey; 67 + } 68 + 69 + public function getRequest() { 70 + return $this->request; 66 71 } 67 72 68 73 ··· 480 485 return $this->issueHighSecurityToken($session, true); 481 486 } 482 487 488 + $this->request = $request; 483 489 foreach ($factors as $factor) { 484 490 $factor->setSessionEngine($this); 485 491 } ··· 523 529 $provider = $factor->getFactorProvider(); 524 530 $impl = $provider->getFactor(); 525 531 526 - $new_challenges = $impl->getNewIssuedChallenges( 527 - $factor, 528 - $viewer, 529 - $issued_challenges); 532 + try { 533 + $new_challenges = $impl->getNewIssuedChallenges( 534 + $factor, 535 + $viewer, 536 + $issued_challenges); 537 + } catch (PhabricatorAuthFactorResultException $ex) { 538 + $ok = false; 539 + $validation_results[$factor_phid] = $ex->getResult(); 540 + $challenge_map[$factor_phid] = $issued_challenges; 541 + continue; 542 + } 530 543 531 544 foreach ($new_challenges as $new_challenge) { 532 545 $issued_challenges[] = $new_challenge; ··· 546 559 continue; 547 560 } 548 561 549 - $ok = false; 562 + if (!$result->getIsValid()) { 563 + $ok = false; 564 + } 565 + 550 566 $validation_results[$factor_phid] = $result; 551 567 } 552 568
+17
src/applications/auth/exception/PhabricatorAuthFactorResultException.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthFactorResultException 4 + extends Exception { 5 + 6 + private $result; 7 + 8 + public function __construct(PhabricatorAuthFactorResult $result) { 9 + $this->result = $result; 10 + parent::__construct(); 11 + } 12 + 13 + public function getResult() { 14 + return $this->result; 15 + } 16 + 17 + }
+120
src/applications/auth/factor/PhabricatorAuthFactor.php
··· 232 232 final protected function newAutomaticControl( 233 233 PhabricatorAuthFactorResult $result) { 234 234 235 + $is_error = $result->getIsError(); 236 + if ($is_error) { 237 + return $this->newErrorControl($result); 238 + } 239 + 240 + $is_continue = $result->getIsContinue(); 241 + if ($is_continue) { 242 + return $this->newContinueControl($result); 243 + } 244 + 235 245 $is_answered = (bool)$result->getAnsweredChallenge(); 236 246 if ($is_answered) { 237 247 return $this->newAnsweredControl($result); ··· 270 280 ->appendChild( 271 281 pht('You responded to this challenge correctly.')); 272 282 } 283 + 284 + private function newErrorControl( 285 + PhabricatorAuthFactorResult $result) { 286 + 287 + $error = $result->getErrorMessage(); 288 + 289 + $icon = id(new PHUIIconView()) 290 + ->setIcon('fa-times', 'red'); 291 + 292 + return id(new PHUIFormTimerControl()) 293 + ->setIcon($icon) 294 + ->appendChild($error) 295 + ->setError(pht('Error')); 296 + } 297 + 298 + private function newContinueControl( 299 + PhabricatorAuthFactorResult $result) { 300 + 301 + $error = $result->getErrorMessage(); 302 + 303 + $icon = id(new PHUIIconView()) 304 + ->setIcon('fa-commenting', 'green'); 305 + 306 + return id(new PHUIFormTimerControl()) 307 + ->setIcon($icon) 308 + ->appendChild($error); 309 + } 310 + 273 311 274 312 275 313 /* -( Synchronizing New Factors )------------------------------------------ */ ··· 398 436 } 399 437 400 438 return null; 439 + } 440 + 441 + 442 + /** 443 + * @phutil-external-symbol class QRcode 444 + */ 445 + final protected function newQRCode($uri) { 446 + $root = dirname(phutil_get_library_root('phabricator')); 447 + require_once $root.'/externals/phpqrcode/phpqrcode.php'; 448 + 449 + $lines = QRcode::text($uri); 450 + 451 + $total_width = 240; 452 + $cell_size = floor($total_width / count($lines)); 453 + 454 + $rows = array(); 455 + foreach ($lines as $line) { 456 + $cells = array(); 457 + for ($ii = 0; $ii < strlen($line); $ii++) { 458 + if ($line[$ii] == '1') { 459 + $color = '#000'; 460 + } else { 461 + $color = '#fff'; 462 + } 463 + 464 + $cells[] = phutil_tag( 465 + 'td', 466 + array( 467 + 'width' => $cell_size, 468 + 'height' => $cell_size, 469 + 'style' => 'background: '.$color, 470 + ), 471 + ''); 472 + } 473 + $rows[] = phutil_tag('tr', array(), $cells); 474 + } 475 + 476 + return phutil_tag( 477 + 'table', 478 + array( 479 + 'style' => 'margin: 24px auto;', 480 + ), 481 + $rows); 482 + } 483 + 484 + final protected function throwResult(PhabricatorAuthFactorResult $result) { 485 + throw new PhabricatorAuthFactorResultException($result); 486 + } 487 + 488 + final protected function getInstallDisplayName() { 489 + $uri = PhabricatorEnv::getURI('/'); 490 + $uri = new PhutilURI($uri); 491 + return $uri->getDomain(); 492 + } 493 + 494 + final protected function getChallengeResponseParameterName( 495 + PhabricatorAuthFactorConfig $config) { 496 + return $this->getParameterName($config, 'mfa.response'); 497 + } 498 + 499 + final protected function getChallengeResponseFromRequest( 500 + PhabricatorAuthFactorConfig $config, 501 + AphrontRequest $request) { 502 + 503 + $name = $this->getChallengeResponseParameterName($config); 504 + 505 + $value = $request->getStr($name); 506 + $value = (string)$value; 507 + $value = trim($value); 508 + 509 + return $value; 510 + } 511 + 512 + final protected function hasCSRF(PhabricatorAuthFactorConfig $config) { 513 + $engine = $config->getSessionEngine(); 514 + $request = $engine->getRequest(); 515 + 516 + if (!$request->isHTTPPost()) { 517 + return false; 518 + } 519 + 520 + return $request->validateCSRF(); 401 521 } 402 522 403 523 }
+20
src/applications/auth/factor/PhabricatorAuthFactorResult.php
··· 5 5 6 6 private $answeredChallenge; 7 7 private $isWait = false; 8 + private $isError = false; 9 + private $isContinue = false; 8 10 private $errorMessage; 9 11 private $value; 10 12 private $issuedChallenges = array(); ··· 42 44 43 45 public function getIsWait() { 44 46 return $this->isWait; 47 + } 48 + 49 + public function setIsError($is_error) { 50 + $this->isError = $is_error; 51 + return $this; 52 + } 53 + 54 + public function getIsError() { 55 + return $this->isError; 56 + } 57 + 58 + public function setIsContinue($is_continue) { 59 + $this->isContinue = $is_continue; 60 + return $this; 61 + } 62 + 63 + public function getIsContinue() { 64 + return $this->isContinue; 45 65 } 46 66 47 67 public function setErrorMessage($error_message) {
+33 -23
src/applications/auth/factor/PhabricatorSMSAuthFactor.php
··· 171 171 return array(); 172 172 } 173 173 174 + if (!$this->loadUserContactNumber($viewer)) { 175 + $result = $this->newResult() 176 + ->setIsError(true) 177 + ->setErrorMessage( 178 + pht( 179 + 'Your account has no primary contact number.')); 180 + 181 + $this->throwResult($result); 182 + } 183 + 184 + if (!$this->isSMSMailerConfigured()) { 185 + $result = $this->newResult() 186 + ->setIsError(true) 187 + ->setErrorMessage( 188 + pht( 189 + 'No outbound mailer which can deliver SMS messages is '. 190 + 'configured.')); 191 + 192 + $this->throwResult($result); 193 + } 194 + 195 + if (!$this->hasCSRF($config)) { 196 + $result = $this->newResult() 197 + ->setIsContinue(true) 198 + ->setErrorMessage( 199 + pht( 200 + 'A text message with an authorization code will be sent to your '. 201 + 'primary contact number.')); 202 + 203 + $this->throwResult($result); 204 + } 205 + 174 206 // Otherwise, issue a new challenge. 175 207 176 208 $challenge_code = $this->newSMSChallengeCode(); ··· 329 361 private function sendSMSCodeToUser( 330 362 PhutilOpaqueEnvelope $envelope, 331 363 PhabricatorUser $user) { 332 - 333 - $uri = PhabricatorEnv::getURI('/'); 334 - $uri = new PhutilURI($uri); 335 - 336 364 return id(new PhabricatorMetaMTAMail()) 337 365 ->setMessageType(PhabricatorMailSMSMessage::MESSAGETYPE) 338 366 ->addTos(array($user->getPHID())) ··· 341 369 ->setBody( 342 370 pht( 343 371 'Phabricator (%s) MFA Code: %s', 344 - $uri->getDomain(), 372 + $this->getInstallDisplayName(), 345 373 $envelope->openEnvelope())) 346 374 ->save(); 347 375 } 348 376 349 377 private function normalizeSMSCode($code) { 350 378 return trim($code); 351 - } 352 - 353 - private function getChallengeResponseParameterName( 354 - PhabricatorAuthFactorConfig $config) { 355 - return $this->getParameterName($config, 'sms.code'); 356 - } 357 - 358 - private function getChallengeResponseFromRequest( 359 - PhabricatorAuthFactorConfig $config, 360 - AphrontRequest $request) { 361 - 362 - $name = $this->getChallengeResponseParameterName($config); 363 - 364 - $value = $request->getStr($name); 365 - $value = (string)$value; 366 - $value = trim($value); 367 - 368 - return $value; 369 379 } 370 380 371 381 }
+1 -62
src/applications/auth/factor/PhabricatorTOTPAuthFactor.php
··· 90 90 $secret, 91 91 $issuer); 92 92 93 - $qrcode = $this->renderQRCode($uri); 93 + $qrcode = $this->newQRCode($uri); 94 94 $form->appendChild($qrcode); 95 95 96 96 $form->appendChild( ··· 390 390 return $code; 391 391 } 392 392 393 - 394 - /** 395 - * @phutil-external-symbol class QRcode 396 - */ 397 - private function renderQRCode($uri) { 398 - $root = dirname(phutil_get_library_root('phabricator')); 399 - require_once $root.'/externals/phpqrcode/phpqrcode.php'; 400 - 401 - $lines = QRcode::text($uri); 402 - 403 - $total_width = 240; 404 - $cell_size = floor($total_width / count($lines)); 405 - 406 - $rows = array(); 407 - foreach ($lines as $line) { 408 - $cells = array(); 409 - for ($ii = 0; $ii < strlen($line); $ii++) { 410 - if ($line[$ii] == '1') { 411 - $color = '#000'; 412 - } else { 413 - $color = '#fff'; 414 - } 415 - 416 - $cells[] = phutil_tag( 417 - 'td', 418 - array( 419 - 'width' => $cell_size, 420 - 'height' => $cell_size, 421 - 'style' => 'background: '.$color, 422 - ), 423 - ''); 424 - } 425 - $rows[] = phutil_tag('tr', array(), $cells); 426 - } 427 - 428 - return phutil_tag( 429 - 'table', 430 - array( 431 - 'style' => 'margin: 24px auto;', 432 - ), 433 - $rows); 434 - } 435 - 436 393 private function getTimestepDuration() { 437 394 return 30; 438 395 } ··· 468 425 } 469 426 470 427 return null; 471 - } 472 - 473 - private function getChallengeResponseParameterName( 474 - PhabricatorAuthFactorConfig $config) { 475 - return $this->getParameterName($config, 'totpcode'); 476 - } 477 - 478 - private function getChallengeResponseFromRequest( 479 - PhabricatorAuthFactorConfig $config, 480 - AphrontRequest $request) { 481 - 482 - $name = $this->getChallengeResponseParameterName($config); 483 - 484 - $value = $request->getStr($name); 485 - $value = (string)$value; 486 - $value = trim($value); 487 - 488 - return $value; 489 428 } 490 429 491 430 protected function newMFASyncTokenProperties(PhabricatorUser $user) {
+5
src/applications/auth/future/PhabricatorDuoFuture.php
··· 112 112 $this->secretKey->openEnvelope()); 113 113 $signature = new PhutilOpaqueEnvelope($signature); 114 114 115 + if ($http_method === 'GET') { 116 + $uri->setQueryParams($data); 117 + $data = array(); 118 + } 119 + 115 120 $future = id(new HTTPSFuture($uri, $data)) 116 121 ->setHTTPBasicAuthCredentials($this->integrationKey, $signature) 117 122 ->setMethod($http_method)
+7 -1
src/applications/auth/storage/PhabricatorAuthChallenge.php
··· 163 163 $token = Filesystem::readRandomCharacters(32); 164 164 $token = new PhutilOpaqueEnvelope($token); 165 165 166 - return $this 166 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 167 + 168 + $this 167 169 ->setResponseToken($token) 168 170 ->setResponseTTL($ttl) 169 171 ->save(); 172 + 173 + unset($unguarded); 174 + 175 + return $this; 170 176 } 171 177 172 178 public function markChallengeAsCompleted() {
+9
src/applications/auth/storage/PhabricatorAuthFactorConfig.php
··· 71 71 return $this->mfaSyncToken; 72 72 } 73 73 74 + public function getAuthFactorConfigProperty($key, $default = null) { 75 + return idx($this->properties, $key, $default); 76 + } 77 + 78 + public function setAuthFactorConfigProperty($key, $value) { 79 + $this->properties[$key] = $value; 80 + return $this; 81 + } 82 + 74 83 75 84 /* -( PhabricatorPolicyInterface )----------------------------------------- */ 76 85
+8 -5
src/view/phui/PHUIInfoView.php
··· 8 8 const SEVERITY_NODATA = 'nodata'; 9 9 const SEVERITY_SUCCESS = 'success'; 10 10 const SEVERITY_PLAIN = 'plain'; 11 + const SEVERITY_MFA = 'mfa'; 11 12 12 13 private $title; 13 14 private $errors = array(); ··· 73 74 switch ($this->getSeverity()) { 74 75 case self::SEVERITY_ERROR: 75 76 $icon = 'fa-exclamation-circle'; 76 - break; 77 + break; 77 78 case self::SEVERITY_WARNING: 78 79 $icon = 'fa-exclamation-triangle'; 79 - break; 80 + break; 80 81 case self::SEVERITY_NOTICE: 81 82 $icon = 'fa-info-circle'; 82 - break; 83 + break; 83 84 case self::SEVERITY_PLAIN: 84 85 case self::SEVERITY_NODATA: 85 86 return null; 86 - break; 87 87 case self::SEVERITY_SUCCESS: 88 88 $icon = 'fa-check-circle'; 89 - break; 89 + break; 90 + case self::SEVERITY_MFA: 91 + $icon = 'fa-lock'; 92 + break; 90 93 } 91 94 92 95 $icon = id(new PHUIIconView())
+4
webroot/rsrc/css/phui/phui-form-view.css
··· 574 574 color: {$darkgreytext}; 575 575 vertical-align: middle; 576 576 } 577 + 578 + .mfa-form-enroll-button { 579 + text-align: center; 580 + }
+9
webroot/rsrc/css/phui/phui-info-view.css
··· 93 93 color: {$red}; 94 94 } 95 95 96 + .phui-info-severity-mfa { 97 + border-color: {$blue}; 98 + border-left-width: 6px; 99 + } 100 + 101 + .phui-info-severity-mfa .phui-info-icon { 102 + color: {$blue}; 103 + } 104 + 96 105 .phui-info-severity-warning { 97 106 border-color: {$yellow}; 98 107 border-left-width: 6px;