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

Track MFA "challenges" so we can bind challenges to sessions and support SMS and other push MFA

Summary:
Ref T13222. See PHI873. Ref T9770.

Currently, we support only TOTP MFA. For some MFA (SMS and "push-to-app"-style MFA) we may need to keep track of MFA details (e.g., the code we SMS'd you). There isn't much support for that yet.

We also currently allow free reuse of TOTP responses across sessions and workflows. This hypothetically enables some "spyglass" attacks where you look at someone's phone and type the code in before they do. T9770 discusses this in more detail, but is focused on an attack window starting when the user submits the form. I claim the attack window opens when the TOTP code is shown on their phone, and the window between the code being shown and being submitted is //much// more interesting than the window after it is submitted.

To address both of these cases, start tracking MFA "Challenges". These are basically a record that we asked you to give us MFA credentials.

For TOTP, the challenge binds a particular timestep to a given session, so an attacker can't look at your phone and type the code into their browser before (or after) you do -- they have a different session. For now, this means that codes are reusable in the same session, but that will be refined in the future.

For SMS / push, the "Challenge" would store the code we sent you so we could validate it.

This is mostly a step on the way toward one-shot MFA, ad-hoc MFA in comment action stacks, and figuring out what's going on with Duo.

Test Plan:
- Passed MFA normally.
- Passed MFA normally, simultaneously, as two different users.
- With two different sessions for the same user:
- Opened MFA in A, opened MFA in B. B got a "wait".
- Submitted MFA in A.
- Clicked "Wait" a bunch in B.
- Submitted MFA in B when prompted.
- Passed MFA normally, then passed MFA normally again with the same code in the same session. (This change does not prevent code reuse.)

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T13222, T9770

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

+572 -50
+12
resources/sql/autopatches/20181213.auth.06.challenge.sql
··· 1 + CREATE TABLE {$NAMESPACE}_auth.auth_challenge ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + phid VARBINARY(64) NOT NULL, 4 + userPHID VARBINARY(64) NOT NULL, 5 + factorPHID VARBINARY(64) NOT NULL, 6 + sessionPHID VARBINARY(64) NOT NULL, 7 + challengeKey VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}, 8 + challengeTTL INT UNSIGNED NOT NULL, 9 + properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, 10 + dateCreated INT UNSIGNED NOT NULL, 11 + dateModified INT UNSIGNED NOT NULL 12 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+9
src/__phutil_library_map__.php
··· 2187 2187 'PhabricatorAuthApplication' => 'applications/auth/application/PhabricatorAuthApplication.php', 2188 2188 'PhabricatorAuthAuthFactorPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php', 2189 2189 'PhabricatorAuthAuthProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php', 2190 + 'PhabricatorAuthChallenge' => 'applications/auth/storage/PhabricatorAuthChallenge.php', 2191 + 'PhabricatorAuthChallengePHIDType' => 'applications/auth/phid/PhabricatorAuthChallengePHIDType.php', 2192 + 'PhabricatorAuthChallengeQuery' => 'applications/auth/query/PhabricatorAuthChallengeQuery.php', 2190 2193 'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php', 2191 2194 'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php', 2192 2195 'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php', ··· 7823 7826 'PhabricatorAuthApplication' => 'PhabricatorApplication', 7824 7827 'PhabricatorAuthAuthFactorPHIDType' => 'PhabricatorPHIDType', 7825 7828 'PhabricatorAuthAuthProviderPHIDType' => 'PhabricatorPHIDType', 7829 + 'PhabricatorAuthChallenge' => array( 7830 + 'PhabricatorAuthDAO', 7831 + 'PhabricatorPolicyInterface', 7832 + ), 7833 + 'PhabricatorAuthChallengePHIDType' => 'PhabricatorPHIDType', 7834 + 'PhabricatorAuthChallengeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 7826 7835 'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction', 7827 7836 'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod', 7828 7837 'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker',
+17 -2
src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php
··· 29 29 $throwable) { 30 30 31 31 $viewer = $this->getViewer($request); 32 + $results = $throwable->getFactorValidationResults(); 32 33 33 34 $form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm( 34 35 $throwable->getFactors(), 35 - $throwable->getFactorValidationResults(), 36 + $results, 36 37 $viewer, 37 38 $request); 39 + 40 + $is_wait = false; 41 + foreach ($results as $result) { 42 + if ($result->getIsWait()) { 43 + $is_wait = true; 44 + break; 45 + } 46 + } 47 + 48 + if ($is_wait) { 49 + $submit = pht('Wait Patiently'); 50 + } else { 51 + $submit = pht('Enter High Security'); 52 + } 38 53 39 54 $dialog = id(new AphrontDialogView()) 40 55 ->setUser($viewer) ··· 62 77 'actions, you should leave high security.')) 63 78 ->setSubmitURI($request->getPath()) 64 79 ->addCancelButton($throwable->getCancelURI()) 65 - ->addSubmitButton(pht('Enter High Security')); 80 + ->addSubmitButton($submit); 66 81 67 82 $request_parameters = $request->getPassthroughRequestParameters( 68 83 $respect_quicksand = true);
+77 -15
src/applications/auth/engine/PhabricatorAuthSessionEngine.php
··· 480 480 new PhabricatorAuthTryFactorAction(), 481 481 0); 482 482 483 + $now = PhabricatorTime::getNow(); 484 + 485 + // We need to do challenge validation first, since this happens whether you 486 + // submitted responses or not. You can't get a "bad response" error before 487 + // you actually submit a response, but you can get a "wait, we can't 488 + // issue a challenge yet" response. Load all issued challenges which are 489 + // currently valid. 490 + $challenges = id(new PhabricatorAuthChallengeQuery()) 491 + ->setViewer($viewer) 492 + ->withFactorPHIDs(mpull($factors, 'getPHID')) 493 + ->withUserPHIDs(array($viewer->getPHID())) 494 + ->withChallengeTTLBetween($now, null) 495 + ->execute(); 496 + $challenge_map = mgroup($challenges, 'getFactorPHID'); 497 + 483 498 $validation_results = array(); 499 + $ok = true; 500 + 501 + // Validate each factor against issued challenges. For example, this 502 + // prevents you from receiving or responding to a TOTP challenge if another 503 + // challenge was recently issued to a different session. 504 + foreach ($factors as $factor) { 505 + $factor_phid = $factor->getPHID(); 506 + $issued_challenges = idx($challenge_map, $factor_phid, array()); 507 + $impl = $factor->requireImplementation(); 508 + 509 + $new_challenges = $impl->getNewIssuedChallenges( 510 + $factor, 511 + $viewer, 512 + $issued_challenges); 513 + 514 + foreach ($new_challenges as $new_challenge) { 515 + $issued_challenges[] = $new_challenge; 516 + } 517 + $challenge_map[$factor_phid] = $issued_challenges; 518 + 519 + if (!$issued_challenges) { 520 + continue; 521 + } 522 + 523 + $result = $impl->getResultFromIssuedChallenges( 524 + $factor, 525 + $viewer, 526 + $issued_challenges); 527 + 528 + if (!$result) { 529 + continue; 530 + } 531 + 532 + $ok = false; 533 + $validation_results[$factor_phid] = $result; 534 + } 535 + 484 536 if ($request->isHTTPPost()) { 485 537 $request->validateCSRF(); 486 538 if ($request->getExists(AphrontRequest::TYPE_HISEC)) { ··· 491 543 new PhabricatorAuthTryFactorAction(), 492 544 1); 493 545 494 - $ok = true; 495 546 foreach ($factors as $factor) { 496 - $id = $factor->getID(); 547 + $factor_phid = $factor->getPHID(); 548 + 549 + // If we already have a validation result from previously issued 550 + // challenges, skip validating this factor. 551 + if (isset($validation_results[$factor_phid])) { 552 + continue; 553 + } 554 + 497 555 $impl = $factor->requireImplementation(); 498 556 499 - $validation_result = $impl->processValidateFactorForm( 557 + $validation_result = $impl->getResultFromChallengeResponse( 500 558 $factor, 501 559 $viewer, 502 - $request); 503 - 504 - if (!($validation_result instanceof PhabricatorAuthFactorResult)) { 505 - throw new Exception( 506 - pht( 507 - 'Expected "processValidateFactorForm()" to return an object '. 508 - 'of class "%s"; got something else (from "%s").', 509 - 'PhabricatorAuthFactorResult', 510 - get_class($impl))); 511 - } 560 + $request, 561 + $issued_challenges); 512 562 513 563 if (!$validation_result->getIsValid()) { 514 564 $ok = false; 515 565 } 516 566 517 - $validation_results[$id] = $validation_result; 567 + $validation_results[$factor_phid] = $validation_result; 518 568 } 519 569 520 570 if ($ok) { ··· 566 616 return $token; 567 617 } 568 618 619 + // If we don't have a validation result for some factors yet, fill them 620 + // in with an empty result so form rendering doesn't have to care if the 621 + // results exist or not. This happens when you first load the form and have 622 + // not submitted any responses yet. 623 + foreach ($factors as $factor) { 624 + $factor_phid = $factor->getPHID(); 625 + if (isset($validation_results[$factor_phid])) { 626 + continue; 627 + } 628 + $validation_results[$factor_phid] = new PhabricatorAuthFactorResult(); 629 + } 630 + 569 631 throw id(new PhabricatorAuthHighSecurityRequiredException()) 570 632 ->setCancelURI($cancel_uri) 571 633 ->setFactors($factors) ··· 613 675 ->appendRemarkupInstructions(''); 614 676 615 677 foreach ($factors as $factor) { 616 - $result = idx($validation_results, $factor->getID()); 678 + $result = $validation_results[$factor->getPHID()]; 617 679 618 680 $factor->requireImplementation()->renderValidateFactorForm( 619 681 $factor,
+128 -6
src/applications/auth/factor/PhabricatorAuthFactor.php
··· 14 14 PhabricatorAuthFactorConfig $config, 15 15 AphrontFormView $form, 16 16 PhabricatorUser $viewer, 17 - PhabricatorAuthFactorResult $validation_result = null); 18 - 19 - abstract public function processValidateFactorForm( 20 - PhabricatorAuthFactorConfig $config, 21 - PhabricatorUser $viewer, 22 - AphrontRequest $request); 17 + PhabricatorAuthFactorResult $validation_result); 23 18 24 19 public function getParameterName( 25 20 PhabricatorAuthFactorConfig $config, ··· 39 34 ->setUserPHID($user->getPHID()) 40 35 ->setFactorKey($this->getFactorKey()); 41 36 } 37 + 38 + protected function newResult() { 39 + return new PhabricatorAuthFactorResult(); 40 + } 41 + 42 + protected function newChallenge( 43 + PhabricatorAuthFactorConfig $config, 44 + PhabricatorUser $viewer) { 45 + 46 + return id(new PhabricatorAuthChallenge()) 47 + ->setUserPHID($viewer->getPHID()) 48 + ->setSessionPHID($viewer->getSession()->getPHID()) 49 + ->setFactorPHID($config->getPHID()); 50 + } 51 + 52 + final public function getNewIssuedChallenges( 53 + PhabricatorAuthFactorConfig $config, 54 + PhabricatorUser $viewer, 55 + array $challenges) { 56 + assert_instances_of($challenges, 'PhabricatorAuthChallenge'); 57 + 58 + $now = PhabricatorTime::getNow(); 59 + 60 + $new_challenges = $this->newIssuedChallenges( 61 + $config, 62 + $viewer, 63 + $challenges); 64 + 65 + assert_instances_of($new_challenges, 'PhabricatorAuthChallenge'); 66 + 67 + foreach ($new_challenges as $new_challenge) { 68 + $ttl = $new_challenge->getChallengeTTL(); 69 + if (!$ttl) { 70 + throw new Exception( 71 + pht('Newly issued MFA challenges must have a valid TTL!')); 72 + } 73 + 74 + if ($ttl < $now) { 75 + throw new Exception( 76 + pht( 77 + 'Newly issued MFA challenges must have a future TTL. This '. 78 + 'factor issued a bad TTL ("%s"). (Did you use a relative '. 79 + 'time instead of an epoch?)', 80 + $ttl)); 81 + } 82 + } 83 + 84 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 85 + foreach ($new_challenges as $challenge) { 86 + $challenge->save(); 87 + } 88 + unset($unguarded); 89 + 90 + return $new_challenges; 91 + } 92 + 93 + abstract protected function newIssuedChallenges( 94 + PhabricatorAuthFactorConfig $config, 95 + PhabricatorUser $viewer, 96 + array $challenges); 97 + 98 + final public function getResultFromIssuedChallenges( 99 + PhabricatorAuthFactorConfig $config, 100 + PhabricatorUser $viewer, 101 + array $challenges) { 102 + assert_instances_of($challenges, 'PhabricatorAuthChallenge'); 103 + 104 + $result = $this->newResultFromIssuedChallenges( 105 + $config, 106 + $viewer, 107 + $challenges); 108 + 109 + if ($result === null) { 110 + return $result; 111 + } 112 + 113 + if (!($result instanceof PhabricatorAuthFactorResult)) { 114 + throw new Exception( 115 + pht( 116 + 'Expected "newResultFromIssuedChallenges()" to return null or '. 117 + 'an object of class "%s"; got something else (in "%s").', 118 + 'PhabricatorAuthFactorResult', 119 + get_class($this))); 120 + } 121 + 122 + $result->setIssuedChallenges($challenges); 123 + 124 + return $result; 125 + } 126 + 127 + abstract protected function newResultFromIssuedChallenges( 128 + PhabricatorAuthFactorConfig $config, 129 + PhabricatorUser $viewer, 130 + array $challenges); 131 + 132 + final public function getResultFromChallengeResponse( 133 + PhabricatorAuthFactorConfig $config, 134 + PhabricatorUser $viewer, 135 + AphrontRequest $request, 136 + array $challenges) { 137 + assert_instances_of($challenges, 'PhabricatorAuthChallenge'); 138 + 139 + $result = $this->newResultFromChallengeResponse( 140 + $config, 141 + $viewer, 142 + $request, 143 + $challenges); 144 + 145 + if (!($result instanceof PhabricatorAuthFactorResult)) { 146 + throw new Exception( 147 + pht( 148 + 'Expected "newResultFromChallengeResponse()" to return an object '. 149 + 'of class "%s"; got something else (in "%s").', 150 + 'PhabricatorAuthFactorResult', 151 + get_class($this))); 152 + } 153 + 154 + $result->setIssuedChallenges($challenges); 155 + 156 + return $result; 157 + } 158 + 159 + abstract protected function newResultFromChallengeResponse( 160 + PhabricatorAuthFactorConfig $config, 161 + PhabricatorUser $viewer, 162 + AphrontRequest $request, 163 + array $challenges); 42 164 43 165 }
+26 -5
src/applications/auth/factor/PhabricatorAuthFactorResult.php
··· 4 4 extends Phobject { 5 5 6 6 private $isValid = false; 7 - private $hint; 7 + private $isWait = false; 8 + private $errorMessage; 8 9 private $value; 10 + private $issuedChallenges = array(); 9 11 10 12 public function setIsValid($is_valid) { 11 13 $this->isValid = $is_valid; ··· 16 18 return $this->isValid; 17 19 } 18 20 19 - public function setHint($hint) { 20 - $this->hint = $hint; 21 + public function setIsWait($is_wait) { 22 + $this->isWait = $is_wait; 23 + return $this; 24 + } 25 + 26 + public function getIsWait() { 27 + return $this->isWait; 28 + } 29 + 30 + public function setErrorMessage($error_message) { 31 + $this->errorMessage = $error_message; 21 32 return $this; 22 33 } 23 34 24 - public function getHint() { 25 - return $this->hint; 35 + public function getErrorMessage() { 36 + return $this->errorMessage; 26 37 } 27 38 28 39 public function setValue($value) { ··· 32 43 33 44 public function getValue() { 34 45 return $this->value; 46 + } 47 + 48 + public function setIssuedChallenges(array $issued_challenges) { 49 + assert_instances_of($issued_challenges, 'PhabricatorAuthChallenge'); 50 + $this->issuedChallenges = $issued_challenges; 51 + return $this; 52 + } 53 + 54 + public function getIssuedChallenges() { 55 + return $this->issuedChallenges; 35 56 } 36 57 37 58 }
+118 -22
src/applications/auth/factor/PhabricatorTOTPAuthFactor.php
··· 77 77 78 78 $e_code = true; 79 79 if ($request->getExists('totp')) { 80 - $okay = self::verifyTOTPCode( 80 + $okay = $this->verifyTOTPCode( 81 81 $user, 82 82 new PhutilOpaqueEnvelope($key), 83 83 $code); ··· 150 150 151 151 } 152 152 153 + protected function newIssuedChallenges( 154 + PhabricatorAuthFactorConfig $config, 155 + PhabricatorUser $viewer, 156 + array $challenges) { 157 + 158 + $now = $this->getCurrentTimestep(); 159 + 160 + // If we already issued a valid challenge, don't issue a new one. 161 + if ($challenges) { 162 + return array(); 163 + } 164 + 165 + // Otherwise, generate a new challenge for the current timestep. It TTLs 166 + // after it would fall off the bottom of the window. 167 + $timesteps = $this->getAllowedTimesteps(); 168 + $min_step = min($timesteps); 169 + 170 + $step_duration = $this->getTimestepDuration(); 171 + $ttl_steps = ($now - $min_step) + 1; 172 + $ttl_seconds = ($ttl_steps * $step_duration); 173 + 174 + return array( 175 + $this->newChallenge($config, $viewer) 176 + ->setChallengeKey($now) 177 + ->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds), 178 + ); 179 + } 180 + 153 181 public function renderValidateFactorForm( 154 182 PhabricatorAuthFactorConfig $config, 155 183 AphrontFormView $form, 156 184 PhabricatorUser $viewer, 157 - PhabricatorAuthFactorResult $validation_result = null) { 185 + PhabricatorAuthFactorResult $result) { 158 186 159 - if ($validation_result) { 160 - $value = $validation_result->getValue(); 161 - $hint = $validation_result->getHint(); 162 - } else { 163 - $value = null; 164 - $hint = true; 165 - } 187 + $value = $result->getValue(); 188 + $error = $result->getErrorMessage(); 189 + $is_wait = $result->getIsWait(); 166 190 167 - $form->appendChild( 168 - id(new PHUIFormNumberControl()) 191 + if ($is_wait) { 192 + $control = id(new AphrontFormMarkupControl()) 193 + ->setValue($error) 194 + ->setError(pht('Wait')); 195 + } else { 196 + $control = id(new PHUIFormNumberControl()) 169 197 ->setName($this->getParameterName($config, 'totpcode')) 170 - ->setLabel(pht('App Code')) 171 198 ->setDisableAutocomplete(true) 172 - ->setCaption(pht('Factor Name: %s', $config->getFactorName())) 173 199 ->setValue($value) 174 - ->setError($hint)); 200 + ->setError($error); 201 + } 202 + 203 + $control 204 + ->setLabel(pht('App Code')) 205 + ->setCaption(pht('Factor Name: %s', $config->getFactorName())); 206 + 207 + $form->appendChild($control); 175 208 } 176 209 177 - public function processValidateFactorForm( 210 + protected function newResultFromIssuedChallenges( 178 211 PhabricatorAuthFactorConfig $config, 179 212 PhabricatorUser $viewer, 180 - AphrontRequest $request) { 213 + array $challenges) { 214 + 215 + // If we've already issued a challenge at the current timestep or any 216 + // nearby timestep, require that it was issued to the current session. 217 + // This is defusing attacks where you (broadly) look at someone's phone 218 + // and type the code in more quickly than they do. 219 + 220 + $step_duration = $this->getTimestepDuration(); 221 + $now = $this->getCurrentTimestep(); 222 + $timesteps = $this->getAllowedTimesteps(); 223 + $timesteps = array_fuse($timesteps); 224 + $min_step = min($timesteps); 225 + 226 + $session_phid = $viewer->getSession()->getPHID(); 227 + 228 + foreach ($challenges as $challenge) { 229 + $challenge_timestep = (int)$challenge->getChallengeKey(); 230 + 231 + // This challenge isn't for one of the timesteps you'd be able to respond 232 + // to if you submitted the form right now, so we're good to keep going. 233 + if (!isset($timesteps[$challenge_timestep])) { 234 + continue; 235 + } 236 + 237 + // This is the number of timesteps you need to wait for the problem 238 + // timestep to leave the window, rounded up. 239 + $wait_steps = ($challenge_timestep - $min_step) + 1; 240 + $wait_duration = ($wait_steps * $step_duration); 241 + 242 + if ($challenge->getSessionPHID() !== $session_phid) { 243 + return $this->newResult() 244 + ->setIsWait(true) 245 + ->setErrorMessage( 246 + pht( 247 + 'This factor recently issued a challenge to a different login '. 248 + 'session. Wait %s seconds for the code to cycle, then try '. 249 + 'again.', 250 + new PhutilNumber($wait_duration))); 251 + } 252 + } 253 + 254 + return null; 255 + } 256 + 257 + protected function newResultFromChallengeResponse( 258 + PhabricatorAuthFactorConfig $config, 259 + PhabricatorUser $viewer, 260 + AphrontRequest $request, 261 + array $challenges) { 181 262 182 263 $code = $request->getStr($this->getParameterName($config, 'totpcode')); 183 264 $key = new PhutilOpaqueEnvelope($config->getFactorSecret()); 184 265 185 - $result = id(new PhabricatorAuthFactorResult()) 266 + $result = $this->newResult() 186 267 ->setValue($code); 187 268 188 - if (self::verifyTOTPCode($viewer, $key, $code)) { 269 + if ($this->verifyTOTPCode($viewer, $key, (string)$code)) { 189 270 $result->setIsValid(true); 190 271 } else { 191 272 if (strlen($code)) { 192 - $hint = pht('Invalid'); 273 + $error_message = pht('Invalid'); 193 274 } else { 194 - $hint = pht('Required'); 275 + $error_message = pht('Required'); 195 276 } 196 - $result->setHint($hint); 277 + $result->setErrorMessage($error_message); 197 278 } 198 279 199 280 return $result; ··· 203 284 return strtoupper(Filesystem::readRandomCharacters(32)); 204 285 } 205 286 206 - public static function verifyTOTPCode( 287 + private function verifyTOTPCode( 207 288 PhabricatorUser $user, 208 289 PhutilOpaqueEnvelope $key, 209 290 $code) { ··· 317 398 ), 318 399 $rows); 319 400 } 401 + 402 + private function getTimestepDuration() { 403 + return 30; 404 + } 405 + 406 + private function getCurrentTimestep() { 407 + $duration = $this->getTimestepDuration(); 408 + return (int)(PhabricatorTime::getNow() / $duration); 409 + } 410 + 411 + private function getAllowedTimesteps() { 412 + $now = $this->getCurrentTimestep(); 413 + return range($now - 2, $now + 2); 414 + } 415 + 320 416 321 417 }
+32
src/applications/auth/phid/PhabricatorAuthChallengePHIDType.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthChallengePHIDType extends PhabricatorPHIDType { 4 + 5 + const TYPECONST = 'CHAL'; 6 + 7 + public function getTypeName() { 8 + return pht('Auth Challenge'); 9 + } 10 + 11 + public function newObject() { 12 + return new PhabricatorAuthChallenge(); 13 + } 14 + 15 + public function getPHIDTypeApplicationClass() { 16 + return 'PhabricatorAuthApplication'; 17 + } 18 + 19 + protected function buildQueryForObjects( 20 + PhabricatorObjectQuery $query, 21 + array $phids) { 22 + return new PhabricatorAuthChallengeQuery(); 23 + } 24 + 25 + public function loadHandles( 26 + PhabricatorHandleQuery $query, 27 + array $handles, 28 + array $objects) { 29 + return; 30 + } 31 + 32 + }
+99
src/applications/auth/query/PhabricatorAuthChallengeQuery.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthChallengeQuery 4 + extends PhabricatorCursorPagedPolicyAwareQuery { 5 + 6 + private $ids; 7 + private $phids; 8 + private $userPHIDs; 9 + private $factorPHIDs; 10 + private $challengeTTLMin; 11 + private $challengeTTLMax; 12 + 13 + public function withIDs(array $ids) { 14 + $this->ids = $ids; 15 + return $this; 16 + } 17 + 18 + public function withPHIDs(array $phids) { 19 + $this->phids = $phids; 20 + return $this; 21 + } 22 + 23 + public function withUserPHIDs(array $user_phids) { 24 + $this->userPHIDs = $user_phids; 25 + return $this; 26 + } 27 + 28 + public function withFactorPHIDs(array $factor_phids) { 29 + $this->factorPHIDs = $factor_phids; 30 + return $this; 31 + } 32 + 33 + public function withChallengeTTLBetween($challenge_min, $challenge_max) { 34 + $this->challengeTTLMin = $challenge_min; 35 + $this->challengeTTLMax = $challenge_max; 36 + return $this; 37 + } 38 + 39 + public function newResultObject() { 40 + return new PhabricatorAuthChallenge(); 41 + } 42 + 43 + protected function loadPage() { 44 + return $this->loadStandardPage($this->newResultObject()); 45 + } 46 + 47 + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { 48 + $where = parent::buildWhereClauseParts($conn); 49 + 50 + if ($this->ids !== null) { 51 + $where[] = qsprintf( 52 + $conn, 53 + 'id IN (%Ld)', 54 + $this->ids); 55 + } 56 + 57 + if ($this->phids !== null) { 58 + $where[] = qsprintf( 59 + $conn, 60 + 'phid IN (%Ls)', 61 + $this->phids); 62 + } 63 + 64 + if ($this->userPHIDs !== null) { 65 + $where[] = qsprintf( 66 + $conn, 67 + 'userPHID IN (%Ls)', 68 + $this->userPHIDs); 69 + } 70 + 71 + if ($this->factorPHIDs !== null) { 72 + $where[] = qsprintf( 73 + $conn, 74 + 'factorPHID IN (%Ls)', 75 + $this->factorPHIDs); 76 + } 77 + 78 + if ($this->challengeTTLMin !== null) { 79 + $where[] = qsprintf( 80 + $conn, 81 + 'challengeTTL >= %d', 82 + $this->challengeTTLMin); 83 + } 84 + 85 + if ($this->challengeTTLMax !== null) { 86 + $where[] = qsprintf( 87 + $conn, 88 + 'challengeTTL <= %d', 89 + $this->challengeTTLMax); 90 + } 91 + 92 + return $where; 93 + } 94 + 95 + public function getQueryApplicationClass() { 96 + return 'PhabricatorAuthApplication'; 97 + } 98 + 99 + }
+54
src/applications/auth/storage/PhabricatorAuthChallenge.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthChallenge 4 + extends PhabricatorAuthDAO 5 + implements PhabricatorPolicyInterface { 6 + 7 + protected $userPHID; 8 + protected $factorPHID; 9 + protected $sessionPHID; 10 + protected $challengeKey; 11 + protected $challengeTTL; 12 + protected $properties = array(); 13 + 14 + protected function getConfiguration() { 15 + return array( 16 + self::CONFIG_SERIALIZATION => array( 17 + 'properties' => self::SERIALIZATION_JSON, 18 + ), 19 + self::CONFIG_AUX_PHID => true, 20 + self::CONFIG_COLUMN_SCHEMA => array( 21 + 'challengeKey' => 'text255', 22 + 'challengeTTL' => 'epoch', 23 + ), 24 + self::CONFIG_KEY_SCHEMA => array( 25 + 'key_issued' => array( 26 + 'columns' => array('userPHID', 'challengeTTL'), 27 + ), 28 + ), 29 + ) + parent::getConfiguration(); 30 + } 31 + 32 + public function getPHIDType() { 33 + return PhabricatorAuthChallengePHIDType::TYPECONST; 34 + } 35 + 36 + 37 + /* -( PhabricatorPolicyInterface )----------------------------------------- */ 38 + 39 + 40 + public function getCapabilities() { 41 + return array( 42 + PhabricatorPolicyCapability::CAN_VIEW, 43 + ); 44 + } 45 + 46 + public function getPolicy($capability) { 47 + return PhabricatorPolicies::POLICY_NOONE; 48 + } 49 + 50 + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 51 + return ($viewer->getPHID() === $this->getUserPHID()); 52 + } 53 + 54 + }