Changeset View
Changeset View
Standalone View
Standalone View
src/applications/auth/factor/PhabricatorTOTPAuthFactor.php
| <?php | <?php | ||||
| final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { | final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { | ||||
| const DIGEST_TEMPORARY_KEY = 'mfa.totp.sync'; | |||||
| public function getFactorKey() { | public function getFactorKey() { | ||||
| return 'totp'; | return 'totp'; | ||||
| } | } | ||||
| public function getFactorName() { | public function getFactorName() { | ||||
| return pht('Mobile Phone App (TOTP)'); | return pht('Mobile Phone App (TOTP)'); | ||||
| } | } | ||||
| Show All 11 Lines | final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { | ||||
| } | } | ||||
| public function processAddFactorForm( | public function processAddFactorForm( | ||||
| PhabricatorAuthFactorProvider $provider, | PhabricatorAuthFactorProvider $provider, | ||||
| AphrontFormView $form, | AphrontFormView $form, | ||||
| AphrontRequest $request, | AphrontRequest $request, | ||||
| PhabricatorUser $user) { | PhabricatorUser $user) { | ||||
| $totp_token_type = PhabricatorAuthTOTPKeyTemporaryTokenType::TOKENTYPE; | $sync_token = $this->loadMFASyncToken( | ||||
| $request, | |||||
| $key = $request->getStr('totpkey'); | $form, | ||||
| if (strlen($key)) { | $user); | ||||
| // If the user is providing a key, make sure it's a key we generated. | $secret = $sync_token->getTemporaryTokenProperty('secret'); | ||||
| // This raises the barrier to theoretical attacks where an attacker might | |||||
| // provide a known key (such attacks are already prevented by CSRF, but | |||||
| // this is a second barrier to overcome). | |||||
| // (We store and verify the hash of the key, not the key itself, to limit | |||||
| // how useful the data in the table is to an attacker.) | |||||
| $token_code = PhabricatorHash::digestWithNamedKey( | |||||
| $key, | |||||
| self::DIGEST_TEMPORARY_KEY); | |||||
| $temporary_token = id(new PhabricatorAuthTemporaryTokenQuery()) | |||||
| ->setViewer($user) | |||||
| ->withTokenResources(array($user->getPHID())) | |||||
| ->withTokenTypes(array($totp_token_type)) | |||||
| ->withExpired(false) | |||||
| ->withTokenCodes(array($token_code)) | |||||
| ->executeOne(); | |||||
| if (!$temporary_token) { | |||||
| // If we don't have a matching token, regenerate the key below. | |||||
| $key = null; | |||||
| } | |||||
| } | |||||
| if (!strlen($key)) { | |||||
| $key = self::generateNewTOTPKey(); | |||||
| // Mark this key as one we generated, so the user is allowed to submit | |||||
| // a response for it. | |||||
| $token_code = PhabricatorHash::digestWithNamedKey( | |||||
| $key, | |||||
| self::DIGEST_TEMPORARY_KEY); | |||||
| $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); | |||||
| id(new PhabricatorAuthTemporaryToken()) | |||||
| ->setTokenResource($user->getPHID()) | |||||
| ->setTokenType($totp_token_type) | |||||
| ->setTokenExpires(time() + phutil_units('1 hour in seconds')) | |||||
| ->setTokenCode($token_code) | |||||
| ->save(); | |||||
| unset($unguarded); | |||||
| } | |||||
| $code = $request->getStr('totpcode'); | $code = $request->getStr('totpcode'); | ||||
| $e_code = true; | $e_code = true; | ||||
| if ($request->getExists('totp')) { | if (!$sync_token->getIsNewTemporaryToken()) { | ||||
epriestley: This is slightly new. Before, we put `totp = true` in the form so we could tell if you… | |||||
| $okay = (bool)$this->getTimestepAtWhichResponseIsValid( | $okay = (bool)$this->getTimestepAtWhichResponseIsValid( | ||||
| $this->getAllowedTimesteps($this->getCurrentTimestep()), | $this->getAllowedTimesteps($this->getCurrentTimestep()), | ||||
| new PhutilOpaqueEnvelope($key), | new PhutilOpaqueEnvelope($secret), | ||||
| $code); | $code); | ||||
| if ($okay) { | if ($okay) { | ||||
| $config = $this->newConfigForUser($user) | $config = $this->newConfigForUser($user) | ||||
| ->setFactorName(pht('Mobile App (TOTP)')) | ->setFactorName(pht('Mobile App (TOTP)')) | ||||
| ->setFactorSecret($key); | ->setFactorSecret($secret) | ||||
| ->setMFASyncToken($sync_token); | |||||
| return $config; | return $config; | ||||
| } else { | } else { | ||||
| if (!strlen($code)) { | if (!strlen($code)) { | ||||
| $e_code = pht('Required'); | $e_code = pht('Required'); | ||||
| } else { | } else { | ||||
| $e_code = pht('Invalid'); | $e_code = pht('Invalid'); | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| $form->addHiddenInput('totp', true); | |||||
| $form->addHiddenInput('totpkey', $key); | |||||
Done Inline Actions
epriestley: - "totp" (which meant "did the user submit a guess?") has been simplified into "is new token?"… | |||||
| $form->appendRemarkupInstructions( | $form->appendRemarkupInstructions( | ||||
| pht( | pht( | ||||
| 'First, download an authenticator application on your phone. Two '. | 'First, download an authenticator application on your phone. Two '. | ||||
| 'applications which work well are **Authy** and **Google '. | 'applications which work well are **Authy** and **Google '. | ||||
| 'Authenticator**, but any other TOTP application should also work.')); | 'Authenticator**, but any other TOTP application should also work.')); | ||||
| $form->appendInstructions( | $form->appendInstructions( | ||||
| pht( | pht( | ||||
| 'Launch the application on your phone, and add a new entry for '. | 'Launch the application on your phone, and add a new entry for '. | ||||
| 'this Phabricator install. When prompted, scan the QR code or '. | 'this Phabricator install. When prompted, scan the QR code or '. | ||||
| 'manually enter the key shown below into the application.')); | 'manually enter the key shown below into the application.')); | ||||
| $prod_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/')); | $prod_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/')); | ||||
| $issuer = $prod_uri->getDomain(); | $issuer = $prod_uri->getDomain(); | ||||
| $uri = urisprintf( | $uri = urisprintf( | ||||
| 'otpauth://totp/%s:%s?secret=%s&issuer=%s', | 'otpauth://totp/%s:%s?secret=%s&issuer=%s', | ||||
| $issuer, | $issuer, | ||||
| $user->getUsername(), | $user->getUsername(), | ||||
| $key, | $secret, | ||||
| $issuer); | $issuer); | ||||
| $qrcode = $this->renderQRCode($uri); | $qrcode = $this->renderQRCode($uri); | ||||
| $form->appendChild($qrcode); | $form->appendChild($qrcode); | ||||
| $form->appendChild( | $form->appendChild( | ||||
| id(new AphrontFormStaticControl()) | id(new AphrontFormStaticControl()) | ||||
| ->setLabel(pht('Key')) | ->setLabel(pht('Key')) | ||||
| ->setValue(phutil_tag('strong', array(), $key))); | ->setValue(phutil_tag('strong', array(), $secret))); | ||||
| $form->appendInstructions( | $form->appendInstructions( | ||||
| pht( | pht( | ||||
| '(If given an option, select that this key is "Time Based", not '. | '(If given an option, select that this key is "Time Based", not '. | ||||
| '"Counter Based".)')); | '"Counter Based".)')); | ||||
| $form->appendInstructions( | $form->appendInstructions( | ||||
| pht( | pht( | ||||
| ▲ Show 20 Lines • Show All 374 Lines • ▼ Show 20 Lines | private function getChallengeResponseFromRequest( | ||||
| $name = $this->getChallengeResponseParameterName($config); | $name = $this->getChallengeResponseParameterName($config); | ||||
| $value = $request->getStr($name); | $value = $request->getStr($name); | ||||
| $value = (string)$value; | $value = (string)$value; | ||||
| $value = trim($value); | $value = trim($value); | ||||
| return $value; | return $value; | ||||
| } | } | ||||
| protected function newMFASyncTokenProperties(PhabricatorUser $user) { | |||||
| return array( | |||||
| 'secret' => self::generateNewTOTPKey(), | |||||
| ); | |||||
| } | |||||
| } | } | ||||
This is slightly new. Before, we put totp = true in the form so we could tell if you submitted it or not.
However, we can do this check in a slightly easier way by just testing if we generated or loaded a sync token. If we generated a new one, you can't have a response yet. If we loaded one, you definitely submitted the form so we expect a response.