3 abstract class PhabricatorAuthFactor
extends Phobject
{
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);
16 abstract public function renderValidateFactorForm(
17 PhabricatorAuthFactorConfig
$config,
18 AphrontFormView
$form,
19 PhabricatorUser
$viewer,
20 PhabricatorAuthFactorResult
$validation_result);
22 public function getParameterName(
23 PhabricatorAuthFactorConfig
$config,
25 return 'authfactor.'.$config->getID().'.'.$name;
28 public static function getAllFactors() {
29 return id(new PhutilClassMapQuery())
30 ->setAncestorClass(__CLASS__
)
31 ->setUniqueMethod('getFactorKey')
35 protected function newConfigForUser(PhabricatorUser
$user) {
36 return id(new PhabricatorAuthFactorConfig())
37 ->setUserPHID($user->getPHID())
38 ->setFactorSecret('');
41 protected function newResult() {
42 return new PhabricatorAuthFactorResult();
45 public function newIconView() {
46 return id(new PHUIIconView())
47 ->setIcon('fa-mobile');
50 public function canCreateNewProvider() {
54 public function getProviderCreateDescription() {
58 public function canCreateNewConfiguration(
59 PhabricatorAuthFactorProvider
$provider,
60 PhabricatorUser
$user) {
64 public function getConfigurationCreateDescription(
65 PhabricatorAuthFactorProvider
$provider,
66 PhabricatorUser
$user) {
70 public function getConfigurationListDetails(
71 PhabricatorAuthFactorConfig
$config,
72 PhabricatorAuthFactorProvider
$provider,
73 PhabricatorUser
$viewer) {
77 public function newEditEngineFields(
78 PhabricatorEditEngine
$engine,
79 PhabricatorAuthFactorProvider
$provider) {
83 public function newChallengeStatusView(
84 PhabricatorAuthFactorConfig
$config,
85 PhabricatorAuthFactorProvider
$provider,
86 PhabricatorUser
$viewer,
87 PhabricatorAuthChallenge
$challenge) {
92 * Is this a factor which depends on the user's contact number?
94 * If a user has a "contact number" factor configured, they can not modify
95 * or switch their primary contact number.
97 * @return bool True if this factor should lock contact numbers.
99 public function isContactNumberFactor() {
103 abstract public function getEnrollDescription(
104 PhabricatorAuthFactorProvider
$provider,
105 PhabricatorUser
$user);
107 public function getEnrollButtonText(
108 PhabricatorAuthFactorProvider
$provider,
109 PhabricatorUser
$user) {
110 return pht('Continue');
113 public function getFactorOrder() {
117 final public function newSortVector() {
118 return id(new PhutilSortVector())
119 ->addInt($this->canCreateNewProvider() ?
0 : 1)
120 ->addInt($this->getFactorOrder())
121 ->addString($this->getFactorName());
124 protected function newChallenge(
125 PhabricatorAuthFactorConfig
$config,
126 PhabricatorUser
$viewer) {
128 $engine = $config->getSessionEngine();
130 return PhabricatorAuthChallenge
::initializeNewChallenge()
131 ->setUserPHID($viewer->getPHID())
132 ->setSessionPHID($viewer->getSession()->getPHID())
133 ->setFactorPHID($config->getPHID())
134 ->setIsNewChallenge(true)
135 ->setWorkflowKey($engine->getWorkflowKey());
138 abstract public function getRequestHasChallengeResponse(
139 PhabricatorAuthFactorConfig
$config,
140 AphrontRequest
$response);
142 final public function getNewIssuedChallenges(
143 PhabricatorAuthFactorConfig
$config,
144 PhabricatorUser
$viewer,
146 assert_instances_of($challenges, 'PhabricatorAuthChallenge');
148 $now = PhabricatorTime
::getNow();
150 // Factor implementations may need to perform writes in order to issue
151 // challenges, particularly push factors like SMS.
152 $unguarded = AphrontWriteGuard
::beginScopedUnguardedWrites();
154 $new_challenges = $this->newIssuedChallenges(
159 if ($this->isAuthResult($new_challenges)) {
161 return $new_challenges;
164 assert_instances_of($new_challenges, 'PhabricatorAuthChallenge');
166 foreach ($new_challenges as $new_challenge) {
167 $ttl = $new_challenge->getChallengeTTL();
170 pht('Newly issued MFA challenges must have a valid TTL!'));
176 'Newly issued MFA challenges must have a future TTL. This '.
177 'factor issued a bad TTL ("%s"). (Did you use a relative '.
178 'time instead of an epoch?)',
183 foreach ($new_challenges as $challenge) {
189 return $new_challenges;
192 abstract protected function newIssuedChallenges(
193 PhabricatorAuthFactorConfig
$config,
194 PhabricatorUser
$viewer,
197 final public function getResultFromIssuedChallenges(
198 PhabricatorAuthFactorConfig
$config,
199 PhabricatorUser
$viewer,
201 assert_instances_of($challenges, 'PhabricatorAuthChallenge');
203 $result = $this->newResultFromIssuedChallenges(
208 if ($result === null) {
212 if (!$this->isAuthResult($result)) {
215 'Expected "newResultFromIssuedChallenges()" to return null or '.
216 'an object of class "%s"; got something else (in "%s").',
217 'PhabricatorAuthFactorResult',
224 final public function getResultForPrompt(
225 PhabricatorAuthFactorConfig
$config,
226 PhabricatorUser
$viewer,
227 AphrontRequest
$request,
229 assert_instances_of($challenges, 'PhabricatorAuthChallenge');
231 $result = $this->newResultForPrompt(
237 if (!$this->isAuthResult($result)) {
240 'Expected "newResultForPrompt()" to return an object of class "%s", '.
241 'but it returned something else ("%s"; in "%s").',
242 'PhabricatorAuthFactorResult',
243 phutil_describe_type($result),
250 protected function newResultForPrompt(
251 PhabricatorAuthFactorConfig
$config,
252 PhabricatorUser
$viewer,
253 AphrontRequest
$request,
255 return $this->newResult();
258 abstract protected function newResultFromIssuedChallenges(
259 PhabricatorAuthFactorConfig
$config,
260 PhabricatorUser
$viewer,
263 final public function getResultFromChallengeResponse(
264 PhabricatorAuthFactorConfig
$config,
265 PhabricatorUser
$viewer,
266 AphrontRequest
$request,
268 assert_instances_of($challenges, 'PhabricatorAuthChallenge');
270 $result = $this->newResultFromChallengeResponse(
276 if (!$this->isAuthResult($result)) {
279 'Expected "newResultFromChallengeResponse()" to return an object '.
280 'of class "%s"; got something else (in "%s").',
281 'PhabricatorAuthFactorResult',
288 abstract protected function newResultFromChallengeResponse(
289 PhabricatorAuthFactorConfig
$config,
290 PhabricatorUser
$viewer,
291 AphrontRequest
$request,
294 final protected function newAutomaticControl(
295 PhabricatorAuthFactorResult
$result) {
297 $is_error = $result->getIsError();
299 return $this->newErrorControl($result);
302 $is_continue = $result->getIsContinue();
304 return $this->newContinueControl($result);
307 $is_answered = (bool)$result->getAnsweredChallenge();
309 return $this->newAnsweredControl($result);
312 $is_wait = $result->getIsWait();
314 return $this->newWaitControl($result);
320 private function newWaitControl(
321 PhabricatorAuthFactorResult
$result) {
323 $error = $result->getErrorMessage();
325 $icon = $result->getIcon();
327 $icon = id(new PHUIIconView())
328 ->setIcon('fa-clock-o', 'red');
331 return id(new PHUIFormTimerControl())
333 ->appendChild($error)
334 ->setError(pht('Wait'));
337 private function newAnsweredControl(
338 PhabricatorAuthFactorResult
$result) {
340 $icon = $result->getIcon();
342 $icon = id(new PHUIIconView())
343 ->setIcon('fa-check-circle-o', 'green');
346 return id(new PHUIFormTimerControl())
349 pht('You responded to this challenge correctly.'));
352 private function newErrorControl(
353 PhabricatorAuthFactorResult
$result) {
355 $error = $result->getErrorMessage();
357 $icon = $result->getIcon();
359 $icon = id(new PHUIIconView())
360 ->setIcon('fa-times', 'red');
363 return id(new PHUIFormTimerControl())
365 ->appendChild($error)
366 ->setError(pht('Error'));
369 private function newContinueControl(
370 PhabricatorAuthFactorResult
$result) {
372 $error = $result->getErrorMessage();
374 $icon = $result->getIcon();
376 $icon = id(new PHUIIconView())
377 ->setIcon('fa-commenting', 'green');
380 $control = id(new PHUIFormTimerControl())
382 ->appendChild($error);
384 $status_challenge = $result->getStatusChallenge();
385 if ($status_challenge) {
386 $id = $status_challenge->getID();
387 $uri = "/auth/mfa/challenge/status/{$id}/";
388 $control->setUpdateURI($uri);
396 /* -( Synchronizing New Factors )------------------------------------------ */
399 final protected function loadMFASyncToken(
400 PhabricatorAuthFactorProvider
$provider,
401 AphrontRequest
$request,
402 AphrontFormView
$form,
403 PhabricatorUser
$user) {
405 // If the form included a synchronization key, load the corresponding
406 // token. The user must synchronize to a key we generated because this
407 // raises the barrier to theoretical attacks where an attacker might
408 // provide a known key for factors like TOTP.
410 // (We store and verify the hash of the key, not the key itself, to limit
411 // how useful the data in the table is to an attacker.)
413 $sync_type = PhabricatorAuthMFASyncTemporaryTokenType
::TOKENTYPE
;
416 $sync_key = $request->getStr($this->getMFASyncTokenFormKey());
417 if (strlen($sync_key)) {
418 $sync_key_digest = PhabricatorHash
::digestWithNamedKey(
420 PhabricatorAuthMFASyncTemporaryTokenType
::DIGEST_KEY
);
422 $sync_token = id(new PhabricatorAuthTemporaryTokenQuery())
424 ->withTokenResources(array($user->getPHID()))
425 ->withTokenTypes(array($sync_type))
427 ->withTokenCodes(array($sync_key_digest))
433 // Don't generate a new sync token if there are too many outstanding
434 // tokens already. This is mostly relevant for push factors like SMS,
435 // where generating a token has the side effect of sending a user a
438 $outstanding_limit = 10;
439 $outstanding_tokens = id(new PhabricatorAuthTemporaryTokenQuery())
441 ->withTokenResources(array($user->getPHID()))
442 ->withTokenTypes(array($sync_type))
445 if (count($outstanding_tokens) > $outstanding_limit) {
448 'Your account has too many outstanding, incomplete MFA '.
449 'synchronization attempts. Wait an hour and try again.'));
452 $now = PhabricatorTime
::getNow();
454 $sync_key = Filesystem
::readRandomCharacters(32);
455 $sync_key_digest = PhabricatorHash
::digestWithNamedKey(
457 PhabricatorAuthMFASyncTemporaryTokenType
::DIGEST_KEY
);
458 $sync_ttl = $this->getMFASyncTokenTTL();
460 $sync_token = id(new PhabricatorAuthTemporaryToken())
461 ->setIsNewTemporaryToken(true)
462 ->setTokenResource($user->getPHID())
463 ->setTokenType($sync_type)
464 ->setTokenCode($sync_key_digest)
465 ->setTokenExpires($now +
$sync_ttl);
467 $properties = $this->newMFASyncTokenProperties(
471 if ($this->isAuthResult($properties)) {
475 foreach ($properties as $key => $value) {
476 $sync_token->setTemporaryTokenProperty($key, $value);
482 $form->addHiddenInput($this->getMFASyncTokenFormKey(), $sync_key);
487 protected function newMFASyncTokenProperties(
488 PhabricatorAuthFactorProvider
$provider,
489 PhabricatorUser
$user) {
493 private function getMFASyncTokenFormKey() {
497 private function getMFASyncTokenTTL() {
498 return phutil_units('1 hour in seconds');
501 final protected function getChallengeForCurrentContext(
502 PhabricatorAuthFactorConfig
$config,
503 PhabricatorUser
$viewer,
506 $session_phid = $viewer->getSession()->getPHID();
507 $engine = $config->getSessionEngine();
508 $workflow_key = $engine->getWorkflowKey();
510 foreach ($challenges as $challenge) {
511 if ($challenge->getSessionPHID() !== $session_phid) {
515 if ($challenge->getWorkflowKey() !== $workflow_key) {
519 if ($challenge->getIsCompleted()) {
523 if ($challenge->getIsReusedChallenge()) {
535 * @phutil-external-symbol class QRcode
537 final protected function newQRCode($uri) {
538 $root = dirname(phutil_get_library_root('phabricator'));
539 require_once $root.'/externals/phpqrcode/phpqrcode.php';
541 $lines = QRcode
::text($uri);
544 $cell_size = floor($total_width / count($lines));
547 foreach ($lines as $line) {
549 for ($ii = 0; $ii < strlen($line); $ii++
) {
550 if ($line[$ii] == '1') {
556 $cells[] = phutil_tag(
559 'width' => $cell_size,
560 'height' => $cell_size,
561 'style' => 'background: '.$color,
565 $rows[] = phutil_tag('tr', array(), $cells);
571 'style' => 'margin: 24px auto;',
576 final protected function getInstallDisplayName() {
577 $uri = PhabricatorEnv
::getURI('/');
578 $uri = new PhutilURI($uri);
579 return $uri->getDomain();
582 final protected function getChallengeResponseParameterName(
583 PhabricatorAuthFactorConfig
$config) {
584 return $this->getParameterName($config, 'mfa.response');
587 final protected function getChallengeResponseFromRequest(
588 PhabricatorAuthFactorConfig
$config,
589 AphrontRequest
$request) {
591 $name = $this->getChallengeResponseParameterName($config);
593 $value = $request->getStr($name);
594 $value = (string)$value;
595 $value = trim($value);
600 final protected function hasCSRF(PhabricatorAuthFactorConfig
$config) {
601 $engine = $config->getSessionEngine();
602 $request = $engine->getRequest();
604 if (!$request->isHTTPPost()) {
608 return $request->validateCSRF();
611 final protected function loadConfigurationsForProvider(
612 PhabricatorAuthFactorProvider
$provider,
613 PhabricatorUser
$user) {
615 return id(new PhabricatorAuthFactorConfigQuery())
617 ->withUserPHIDs(array($user->getPHID()))
618 ->withFactorProviderPHIDs(array($provider->getPHID()))
622 final protected function isAuthResult($object) {
623 return ($object instanceof PhabricatorAuthFactorResult
);