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

at recaptime-dev/main 648 lines 18 kB view raw
1<?php 2 3abstract class PhabricatorAuthFactor extends Phobject { 4 5 abstract public function getFactorName(); 6 abstract public function getFactorShortName(); 7 abstract public function getFactorKey(); 8 abstract public function getFactorCreateHelp(); 9 abstract public function getFactorDescription(); 10 abstract public function processAddFactorForm( 11 PhabricatorAuthFactorProvider $provider, 12 AphrontFormView $form, 13 AphrontRequest $request, 14 PhabricatorUser $user); 15 16 abstract public function renderValidateFactorForm( 17 PhabricatorAuthFactorConfig $config, 18 AphrontFormView $form, 19 PhabricatorUser $viewer, 20 PhabricatorAuthFactorResult $validation_result); 21 22 public function getParameterName( 23 PhabricatorAuthFactorConfig $config, 24 $name) { 25 return 'authfactor.'.$config->getID().'.'.$name; 26 } 27 28 public static function getAllFactors() { 29 return id(new PhutilClassMapQuery()) 30 ->setAncestorClass(self::class) 31 ->setUniqueMethod('getFactorKey') 32 ->execute(); 33 } 34 35 protected function newConfigForUser(PhabricatorUser $user) { 36 return id(new PhabricatorAuthFactorConfig()) 37 ->setUserPHID($user->getPHID()) 38 ->setFactorSecret(''); 39 } 40 41 protected function newResult() { 42 return new PhabricatorAuthFactorResult(); 43 } 44 45 public function newIconView() { 46 return id(new PHUIIconView()) 47 ->setIcon('fa-mobile'); 48 } 49 50 public function canCreateNewProvider() { 51 return true; 52 } 53 54 public function getProviderCreateDescription() { 55 return null; 56 } 57 58 public function canCreateNewConfiguration( 59 PhabricatorAuthFactorProvider $provider, 60 PhabricatorUser $user) { 61 return true; 62 } 63 64 public function getConfigurationCreateDescription( 65 PhabricatorAuthFactorProvider $provider, 66 PhabricatorUser $user) { 67 return null; 68 } 69 70 public function getConfigurationListDetails( 71 PhabricatorAuthFactorConfig $config, 72 PhabricatorAuthFactorProvider $provider, 73 PhabricatorUser $viewer) { 74 return null; 75 } 76 77 public function newEditEngineFields( 78 PhabricatorEditEngine $engine, 79 PhabricatorAuthFactorProvider $provider) { 80 return array(); 81 } 82 83 public function newChallengeStatusView( 84 PhabricatorAuthFactorConfig $config, 85 PhabricatorAuthFactorProvider $provider, 86 PhabricatorUser $viewer, 87 PhabricatorAuthChallenge $challenge) { 88 return null; 89 } 90 91 /** 92 * Is this a factor which depends on the user's contact number? 93 * 94 * If a user has a "contact number" factor configured, they can not modify 95 * or switch their primary contact number. 96 * 97 * @return bool True if this factor should lock contact numbers. 98 */ 99 public function isContactNumberFactor() { 100 return false; 101 } 102 103 abstract public function getEnrollDescription( 104 PhabricatorAuthFactorProvider $provider, 105 PhabricatorUser $user); 106 107 public function getEnrollButtonText( 108 PhabricatorAuthFactorProvider $provider, 109 PhabricatorUser $user) { 110 return pht('Continue'); 111 } 112 113 public function getFactorOrder() { 114 return 1000; 115 } 116 117 final public function newSortVector() { 118 return id(new PhutilSortVector()) 119 ->addInt($this->canCreateNewProvider() ? 0 : 1) 120 ->addInt($this->getFactorOrder()) 121 ->addString($this->getFactorName()); 122 } 123 124 protected function newChallenge( 125 PhabricatorAuthFactorConfig $config, 126 PhabricatorUser $viewer) { 127 128 $engine = $config->getSessionEngine(); 129 130 return PhabricatorAuthChallenge::initializeNewChallenge() 131 ->setUserPHID($viewer->getPHID()) 132 ->setSessionPHID($viewer->getSession()->getPHID()) 133 ->setFactorPHID($config->getPHID()) 134 ->setIsNewChallenge(true) 135 ->setWorkflowKey($engine->getWorkflowKey()); 136 } 137 138 abstract public function getRequestHasChallengeResponse( 139 PhabricatorAuthFactorConfig $config, 140 AphrontRequest $response); 141 142 /** 143 * @param PhabricatorAuthFactorConfig $config 144 * @param PhabricatorUser $viewer 145 * @param array<PhabricatorAuthChallenge> $challenges 146 */ 147 final public function getNewIssuedChallenges( 148 PhabricatorAuthFactorConfig $config, 149 PhabricatorUser $viewer, 150 array $challenges) { 151 assert_instances_of($challenges, PhabricatorAuthChallenge::class); 152 153 $now = PhabricatorTime::getNow(); 154 155 // Factor implementations may need to perform writes in order to issue 156 // challenges, particularly push factors like SMS. 157 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 158 159 $new_challenges = $this->newIssuedChallenges( 160 $config, 161 $viewer, 162 $challenges); 163 164 if ($this->isAuthResult($new_challenges)) { 165 unset($unguarded); 166 return $new_challenges; 167 } 168 169 assert_instances_of($new_challenges, PhabricatorAuthChallenge::class); 170 171 foreach ($new_challenges as $new_challenge) { 172 $ttl = $new_challenge->getChallengeTTL(); 173 if (!$ttl) { 174 throw new Exception( 175 pht('Newly issued MFA challenges must have a valid TTL!')); 176 } 177 178 if ($ttl < $now) { 179 throw new Exception( 180 pht( 181 'Newly issued MFA challenges must have a future TTL. This '. 182 'factor issued a bad TTL ("%s"). (Did you use a relative '. 183 'time instead of an epoch?)', 184 $ttl)); 185 } 186 } 187 188 foreach ($new_challenges as $challenge) { 189 $challenge->save(); 190 } 191 192 unset($unguarded); 193 194 return $new_challenges; 195 } 196 197 abstract protected function newIssuedChallenges( 198 PhabricatorAuthFactorConfig $config, 199 PhabricatorUser $viewer, 200 array $challenges); 201 202 /** 203 * @param PhabricatorAuthFactorConfig $config 204 * @param PhabricatorUser $viewer 205 * @param array<PhabricatorAuthChallenge> $challenges 206 */ 207 final public function getResultFromIssuedChallenges( 208 PhabricatorAuthFactorConfig $config, 209 PhabricatorUser $viewer, 210 array $challenges) { 211 assert_instances_of($challenges, PhabricatorAuthChallenge::class); 212 213 $result = $this->newResultFromIssuedChallenges( 214 $config, 215 $viewer, 216 $challenges); 217 218 if ($result === null) { 219 return $result; 220 } 221 222 if (!$this->isAuthResult($result)) { 223 throw new Exception( 224 pht( 225 'Expected "newResultFromIssuedChallenges()" to return null or '. 226 'an object of class "%s"; got something else (in "%s").', 227 'PhabricatorAuthFactorResult', 228 get_class($this))); 229 } 230 231 return $result; 232 } 233 234 /** 235 * @param PhabricatorAuthFactorConfig $config 236 * @param PhabricatorUser $viewer 237 * @param AphrontRequest $request 238 * @param array<PhabricatorAuthChallenge> $challenges 239 */ 240 final public function getResultForPrompt( 241 PhabricatorAuthFactorConfig $config, 242 PhabricatorUser $viewer, 243 AphrontRequest $request, 244 array $challenges) { 245 assert_instances_of($challenges, PhabricatorAuthChallenge::class); 246 247 $result = $this->newResultForPrompt( 248 $config, 249 $viewer, 250 $request, 251 $challenges); 252 253 if (!$this->isAuthResult($result)) { 254 throw new Exception( 255 pht( 256 'Expected "newResultForPrompt()" to return an object of class "%s", '. 257 'but it returned something else ("%s"; in "%s").', 258 'PhabricatorAuthFactorResult', 259 phutil_describe_type($result), 260 get_class($this))); 261 } 262 263 return $result; 264 } 265 266 protected function newResultForPrompt( 267 PhabricatorAuthFactorConfig $config, 268 PhabricatorUser $viewer, 269 AphrontRequest $request, 270 array $challenges) { 271 return $this->newResult(); 272 } 273 274 abstract protected function newResultFromIssuedChallenges( 275 PhabricatorAuthFactorConfig $config, 276 PhabricatorUser $viewer, 277 array $challenges); 278 279 /** 280 * @param PhabricatorAuthFactorConfig $config 281 * @param PhabricatorUser $viewer 282 * @param AphrontRequest $request 283 * @param array<PhabricatorAuthChallenge> $challenges 284 */ 285 final public function getResultFromChallengeResponse( 286 PhabricatorAuthFactorConfig $config, 287 PhabricatorUser $viewer, 288 AphrontRequest $request, 289 array $challenges) { 290 assert_instances_of($challenges, PhabricatorAuthChallenge::class); 291 292 $result = $this->newResultFromChallengeResponse( 293 $config, 294 $viewer, 295 $request, 296 $challenges); 297 298 if (!$this->isAuthResult($result)) { 299 throw new Exception( 300 pht( 301 'Expected "newResultFromChallengeResponse()" to return an object '. 302 'of class "%s"; got something else (in "%s").', 303 'PhabricatorAuthFactorResult', 304 get_class($this))); 305 } 306 307 return $result; 308 } 309 310 abstract protected function newResultFromChallengeResponse( 311 PhabricatorAuthFactorConfig $config, 312 PhabricatorUser $viewer, 313 AphrontRequest $request, 314 array $challenges); 315 316 final protected function newAutomaticControl( 317 PhabricatorAuthFactorResult $result) { 318 319 $is_error = $result->getIsError(); 320 if ($is_error) { 321 return $this->newErrorControl($result); 322 } 323 324 $is_continue = $result->getIsContinue(); 325 if ($is_continue) { 326 return $this->newContinueControl($result); 327 } 328 329 $is_answered = (bool)$result->getAnsweredChallenge(); 330 if ($is_answered) { 331 return $this->newAnsweredControl($result); 332 } 333 334 $is_wait = $result->getIsWait(); 335 if ($is_wait) { 336 return $this->newWaitControl($result); 337 } 338 339 return null; 340 } 341 342 private function newWaitControl( 343 PhabricatorAuthFactorResult $result) { 344 345 $error = $result->getErrorMessage(); 346 347 $icon = $result->getIcon(); 348 if (!$icon) { 349 $icon = id(new PHUIIconView()) 350 ->setIcon('fa-clock-o', 'red'); 351 } 352 353 return id(new PHUIFormTimerControl()) 354 ->setIcon($icon) 355 ->appendChild($error) 356 ->setError(pht('Wait')); 357 } 358 359 private function newAnsweredControl( 360 PhabricatorAuthFactorResult $result) { 361 362 $icon = $result->getIcon(); 363 if (!$icon) { 364 $icon = id(new PHUIIconView()) 365 ->setIcon('fa-check-circle-o', 'green'); 366 } 367 368 return id(new PHUIFormTimerControl()) 369 ->setIcon($icon) 370 ->appendChild( 371 pht('You responded to this challenge correctly.')); 372 } 373 374 private function newErrorControl( 375 PhabricatorAuthFactorResult $result) { 376 377 $error = $result->getErrorMessage(); 378 379 $icon = $result->getIcon(); 380 if (!$icon) { 381 $icon = id(new PHUIIconView()) 382 ->setIcon('fa-times', 'red'); 383 } 384 385 return id(new PHUIFormTimerControl()) 386 ->setIcon($icon) 387 ->appendChild($error) 388 ->setError(pht('Error')); 389 } 390 391 private function newContinueControl( 392 PhabricatorAuthFactorResult $result) { 393 394 $error = $result->getErrorMessage(); 395 396 $icon = $result->getIcon(); 397 if (!$icon) { 398 $icon = id(new PHUIIconView()) 399 ->setIcon('fa-commenting', 'green'); 400 } 401 402 $control = id(new PHUIFormTimerControl()) 403 ->setIcon($icon) 404 ->appendChild($error); 405 406 $status_challenge = $result->getStatusChallenge(); 407 if ($status_challenge) { 408 $id = $status_challenge->getID(); 409 $uri = "/auth/mfa/challenge/status/{$id}/"; 410 $control->setUpdateURI($uri); 411 } 412 413 return $control; 414 } 415 416 417 418/* -( Synchronizing New Factors )------------------------------------------ */ 419 420 421 final protected function loadMFASyncToken( 422 PhabricatorAuthFactorProvider $provider, 423 AphrontRequest $request, 424 AphrontFormView $form, 425 PhabricatorUser $user) { 426 427 // If the form included a synchronization key, load the corresponding 428 // token. The user must synchronize to a key we generated because this 429 // raises the barrier to theoretical attacks where an attacker might 430 // provide a known key for factors like TOTP. 431 432 // (We store and verify the hash of the key, not the key itself, to limit 433 // how useful the data in the table is to an attacker.) 434 435 $sync_type = PhabricatorAuthMFASyncTemporaryTokenType::TOKENTYPE; 436 $sync_token = null; 437 438 $sync_key = $request->getStr($this->getMFASyncTokenFormKey(), ''); 439 if ($sync_key !== '') { 440 $sync_key_digest = PhabricatorHash::digestWithNamedKey( 441 $sync_key, 442 PhabricatorAuthMFASyncTemporaryTokenType::DIGEST_KEY); 443 444 $sync_token = id(new PhabricatorAuthTemporaryTokenQuery()) 445 ->setViewer($user) 446 ->withTokenResources(array($user->getPHID())) 447 ->withTokenTypes(array($sync_type)) 448 ->withExpired(false) 449 ->withTokenCodes(array($sync_key_digest)) 450 ->executeOne(); 451 } 452 453 if (!$sync_token) { 454 455 // Don't generate a new sync token if there are too many outstanding 456 // tokens already. This is mostly relevant for push factors like SMS, 457 // where generating a token has the side effect of sending a user a 458 // message. 459 460 $outstanding_limit = 10; 461 $outstanding_tokens = id(new PhabricatorAuthTemporaryTokenQuery()) 462 ->setViewer($user) 463 ->withTokenResources(array($user->getPHID())) 464 ->withTokenTypes(array($sync_type)) 465 ->withExpired(false) 466 ->execute(); 467 if (count($outstanding_tokens) > $outstanding_limit) { 468 throw new Exception( 469 pht( 470 'Your account has too many outstanding, incomplete MFA '. 471 'synchronization attempts. Wait an hour and try again.')); 472 } 473 474 $now = PhabricatorTime::getNow(); 475 476 $sync_key = Filesystem::readRandomCharacters(32); 477 $sync_key_digest = PhabricatorHash::digestWithNamedKey( 478 $sync_key, 479 PhabricatorAuthMFASyncTemporaryTokenType::DIGEST_KEY); 480 $sync_ttl = $this->getMFASyncTokenTTL(); 481 482 $sync_token = id(new PhabricatorAuthTemporaryToken()) 483 ->setIsNewTemporaryToken(true) 484 ->setTokenResource($user->getPHID()) 485 ->setTokenType($sync_type) 486 ->setTokenCode($sync_key_digest) 487 ->setTokenExpires($now + $sync_ttl); 488 489 $properties = $this->newMFASyncTokenProperties( 490 $provider, 491 $user); 492 493 if ($this->isAuthResult($properties)) { 494 return $properties; 495 } 496 497 foreach ($properties as $key => $value) { 498 $sync_token->setTemporaryTokenProperty($key, $value); 499 } 500 501 $sync_token->save(); 502 } 503 504 $form->addHiddenInput($this->getMFASyncTokenFormKey(), $sync_key); 505 506 return $sync_token; 507 } 508 509 protected function newMFASyncTokenProperties( 510 PhabricatorAuthFactorProvider $provider, 511 PhabricatorUser $user) { 512 return array(); 513 } 514 515 private function getMFASyncTokenFormKey() { 516 return 'sync.key'; 517 } 518 519 private function getMFASyncTokenTTL() { 520 return phutil_units('1 hour in seconds'); 521 } 522 523 final protected function getChallengeForCurrentContext( 524 PhabricatorAuthFactorConfig $config, 525 PhabricatorUser $viewer, 526 array $challenges) { 527 528 $session_phid = $viewer->getSession()->getPHID(); 529 $engine = $config->getSessionEngine(); 530 $workflow_key = $engine->getWorkflowKey(); 531 532 foreach ($challenges as $challenge) { 533 if ($challenge->getSessionPHID() !== $session_phid) { 534 continue; 535 } 536 537 if ($challenge->getWorkflowKey() !== $workflow_key) { 538 continue; 539 } 540 541 if ($challenge->getIsCompleted()) { 542 continue; 543 } 544 545 if ($challenge->getIsReusedChallenge()) { 546 continue; 547 } 548 549 return $challenge; 550 } 551 552 return null; 553 } 554 555 556 /** 557 * @phutil-external-symbol class QRcode 558 */ 559 final protected function newQRCode($uri) { 560 $root = dirname(phutil_get_library_root('phabricator')); 561 require_once $root.'/externals/phpqrcode/phpqrcode.php'; 562 563 $lines = QRcode::text($uri); 564 565 $total_width = 240; 566 $cell_size = floor($total_width / count($lines)); 567 568 $rows = array(); 569 foreach ($lines as $line) { 570 $cells = array(); 571 for ($ii = 0; $ii < strlen($line); $ii++) { 572 if ($line[$ii] == '1') { 573 $color = '#000'; 574 } else { 575 $color = '#fff'; 576 } 577 578 $cells[] = phutil_tag( 579 'td', 580 array( 581 'width' => $cell_size, 582 'height' => $cell_size, 583 'style' => 'background: '.$color, 584 ), 585 ''); 586 } 587 $rows[] = phutil_tag('tr', array(), $cells); 588 } 589 590 return phutil_tag( 591 'table', 592 array( 593 'style' => 'margin: 24px auto;', 594 ), 595 $rows); 596 } 597 598 final protected function getInstallDisplayName() { 599 $uri = PhabricatorEnv::getURI('/'); 600 $uri = new PhutilURI($uri); 601 return $uri->getDomain(); 602 } 603 604 final protected function getChallengeResponseParameterName( 605 PhabricatorAuthFactorConfig $config) { 606 return $this->getParameterName($config, 'mfa.response'); 607 } 608 609 final protected function getChallengeResponseFromRequest( 610 PhabricatorAuthFactorConfig $config, 611 AphrontRequest $request) { 612 613 $name = $this->getChallengeResponseParameterName($config); 614 615 $value = $request->getStr($name); 616 $value = (string)$value; 617 $value = trim($value); 618 619 return $value; 620 } 621 622 final protected function hasCSRF(PhabricatorAuthFactorConfig $config) { 623 $engine = $config->getSessionEngine(); 624 $request = $engine->getRequest(); 625 626 if (!$request->isHTTPPost()) { 627 return false; 628 } 629 630 return $request->validateCSRF(); 631 } 632 633 final protected function loadConfigurationsForProvider( 634 PhabricatorAuthFactorProvider $provider, 635 PhabricatorUser $user) { 636 637 return id(new PhabricatorAuthFactorConfigQuery()) 638 ->setViewer($user) 639 ->withUserPHIDs(array($user->getPHID())) 640 ->withFactorProviderPHIDs(array($provider->getPHID())) 641 ->execute(); 642 } 643 644 final protected function isAuthResult($object) { 645 return ($object instanceof PhabricatorAuthFactorResult); 646 } 647 648}