Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14088666
D20022.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
13 KB
Referenced Files
None
Subscribers
None
D20022.diff
View Options
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -4297,6 +4297,7 @@
'PhabricatorResourceSite' => 'aphront/site/PhabricatorResourceSite.php',
'PhabricatorRobotsController' => 'applications/system/controller/PhabricatorRobotsController.php',
'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php',
+ 'PhabricatorSMSAuthFactor' => 'applications/auth/factor/PhabricatorSMSAuthFactor.php',
'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php',
'PhabricatorSSHKeyGenerator' => 'infrastructure/util/PhabricatorSSHKeyGenerator.php',
'PhabricatorSSHKeysSettingsPanel' => 'applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php',
@@ -10393,6 +10394,7 @@
'PhabricatorResourceSite' => 'PhabricatorSite',
'PhabricatorRobotsController' => 'PhabricatorController',
'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine',
+ 'PhabricatorSMSAuthFactor' => 'PhabricatorAuthFactor',
'PhabricatorSQLPatchList' => 'Phobject',
'PhabricatorSSHKeyGenerator' => 'Phobject',
'PhabricatorSSHKeysSettingsPanel' => 'PhabricatorSettingsPanel',
diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php
--- a/src/applications/auth/factor/PhabricatorAuthFactor.php
+++ b/src/applications/auth/factor/PhabricatorAuthFactor.php
@@ -33,7 +33,8 @@
protected function newConfigForUser(PhabricatorUser $user) {
return id(new PhabricatorAuthFactorConfig())
- ->setUserPHID($user->getPHID());
+ ->setUserPHID($user->getPHID())
+ ->setFactorSecret('');
}
protected function newResult() {
@@ -107,6 +108,10 @@
$now = PhabricatorTime::getNow();
+ // Factor implementations may need to perform writes in order to issue
+ // challenges, particularly push factors like SMS.
+ $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+
$new_challenges = $this->newIssuedChallenges(
$config,
$viewer,
@@ -131,10 +136,10 @@
}
}
- $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
- foreach ($new_challenges as $challenge) {
- $challenge->save();
- }
+ foreach ($new_challenges as $challenge) {
+ $challenge->save();
+ }
+
unset($unguarded);
return $new_challenges;
@@ -351,4 +356,36 @@
return phutil_units('1 hour in seconds');
}
+ final protected function getChallengeForCurrentContext(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ array $challenges) {
+
+ $session_phid = $viewer->getSession()->getPHID();
+ $engine = $config->getSessionEngine();
+ $workflow_key = $engine->getWorkflowKey();
+
+ foreach ($challenges as $challenge) {
+ if ($challenge->getSessionPHID() !== $session_phid) {
+ continue;
+ }
+
+ if ($challenge->getWorkflowKey() !== $workflow_key) {
+ continue;
+ }
+
+ if ($challenge->getIsCompleted()) {
+ continue;
+ }
+
+ if ($challenge->getIsReusedChallenge()) {
+ continue;
+ }
+
+ return $challenge;
+ }
+
+ return null;
+ }
+
}
diff --git a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php
new file mode 100644
--- /dev/null
+++ b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php
@@ -0,0 +1,367 @@
+<?php
+
+final class PhabricatorSMSAuthFactor
+ extends PhabricatorAuthFactor {
+
+ public function getFactorKey() {
+ return 'sms';
+ }
+
+ public function getFactorName() {
+ return pht('SMS');
+ }
+
+ public function getFactorCreateHelp() {
+ return pht(
+ 'Allow users to receive a code via SMS.');
+ }
+
+ public function getFactorDescription() {
+ return pht(
+ 'When you need to authenticate, a text message with a code will '.
+ 'be sent to your phone.');
+ }
+
+ public function getFactorOrder() {
+ // Sort this factor toward the end of the list because SMS is relatively
+ // weak.
+ return 2000;
+ }
+
+ public function canCreateNewProvider() {
+ return $this->isSMSMailerConfigured();
+ }
+
+ public function getProviderCreateDescription() {
+ $messages = array();
+
+ if (!$this->isSMSMailerConfigured()) {
+ $messages[] = id(new PHUIInfoView())
+ ->setErrors(
+ array(
+ pht(
+ 'You have not configured an outbound SMS mailer. You must '.
+ 'configure one before you can set up SMS. See: %s',
+ phutil_tag(
+ 'a',
+ array(
+ 'href' => '/config/edit/cluster.mailers/',
+ ),
+ 'cluster.mailers')),
+ ));
+ }
+
+ $messages[] = id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+ ->setErrors(
+ array(
+ pht(
+ 'SMS is weak, and relatively easy for attackers to compromise. '.
+ 'Strongly consider using a different MFA provider.'),
+ ));
+
+ return $messages;
+ }
+
+ public function canCreateNewConfiguration(PhabricatorUser $user) {
+ if (!$this->loadUserContactNumber($user)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getConfigurationCreateDescription(PhabricatorUser $user) {
+
+ $messages = array();
+
+ if (!$this->loadUserContactNumber($user)) {
+ $messages[] = id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+ ->setErrors(
+ array(
+ pht(
+ 'You have not configured a primary contact number. Configure '.
+ 'a contact number before adding SMS as an authentication '.
+ 'factor.'),
+ ));
+ }
+
+ return $messages;
+ }
+
+ public function getEnrollDescription(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+ return pht(
+ 'To verify your phone as an authentication factor, a text message with '.
+ 'a secret code will be sent to the phone number you have listed as '.
+ 'your primary contact number.');
+ }
+
+ public function getEnrollButtonText(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+ $contact_number = $this->loadUserContactNumber($user);
+
+ return pht('Send SMS: %s', $contact_number->getDisplayName());
+ }
+
+ public function processAddFactorForm(
+ PhabricatorAuthFactorProvider $provider,
+ AphrontFormView $form,
+ AphrontRequest $request,
+ PhabricatorUser $user) {
+
+ $token = $this->loadMFASyncToken($request, $form, $user);
+ $code = $request->getStr('sms.code');
+
+ $e_code = true;
+ if (!$token->getIsNewTemporaryToken()) {
+ $expect_code = $token->getTemporaryTokenProperty('code');
+
+ $okay = phutil_hashes_are_identical(
+ $this->normalizeSMSCode($code),
+ $this->normalizeSMSCode($expect_code));
+
+ if ($okay) {
+ $config = $this->newConfigForUser($user)
+ ->setFactorName(pht('SMS'));
+
+ return $config;
+ } else {
+ if (!strlen($code)) {
+ $e_code = pht('Required');
+ } else {
+ $e_code = pht('Invalid');
+ }
+ }
+ }
+
+ $form->appendRemarkupInstructions(
+ pht(
+ 'Enter the code from the text message which was sent to your '.
+ 'primary contact number.'));
+
+ $form->appendChild(
+ id(new PHUIFormNumberControl())
+ ->setLabel(pht('SMS Code'))
+ ->setName('sms.code')
+ ->setValue($code)
+ ->setError($e_code));
+ }
+
+ protected function newIssuedChallenges(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ array $challenges) {
+
+ // If we already issued a valid challenge for this workflow and session,
+ // don't issue a new one.
+
+ $challenge = $this->getChallengeForCurrentContext(
+ $config,
+ $viewer,
+ $challenges);
+ if ($challenge) {
+ return array();
+ }
+
+ // Otherwise, issue a new challenge.
+
+ $challenge_code = $this->newSMSChallengeCode();
+ $envelope = new PhutilOpaqueEnvelope($challenge_code);
+ $this->sendSMSCodeToUser($envelope, $viewer);
+
+ $ttl_seconds = phutil_units('15 minutes in seconds');
+
+ return array(
+ $this->newChallenge($config, $viewer)
+ ->setChallengeKey($challenge_code)
+ ->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds),
+ );
+ }
+
+ protected function newResultFromIssuedChallenges(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ array $challenges) {
+
+ $challenge = $this->getChallengeForCurrentContext(
+ $config,
+ $viewer,
+ $challenges);
+
+ if ($challenge->getIsAnsweredChallenge()) {
+ return $this->newResult()
+ ->setAnsweredChallenge($challenge);
+ }
+
+ return null;
+ }
+
+ public function renderValidateFactorForm(
+ PhabricatorAuthFactorConfig $config,
+ AphrontFormView $form,
+ PhabricatorUser $viewer,
+ PhabricatorAuthFactorResult $result) {
+
+ $control = $this->newAutomaticControl($result);
+ if (!$control) {
+ $value = $result->getValue();
+ $error = $result->getErrorMessage();
+ $name = $this->getChallengeResponseParameterName($config);
+
+ $control = id(new PHUIFormNumberControl())
+ ->setName($name)
+ ->setDisableAutocomplete(true)
+ ->setValue($value)
+ ->setError($error);
+ }
+
+ $control
+ ->setLabel(pht('SMS Code'))
+ ->setCaption(pht('Factor Name: %s', $config->getFactorName()));
+
+ $form->appendChild($control);
+ }
+
+ public function getRequestHasChallengeResponse(
+ PhabricatorAuthFactorConfig $config,
+ AphrontRequest $request) {
+ $value = $this->getChallengeResponseFromRequest($config, $request);
+ return (bool)strlen($value);
+ }
+
+ protected function newResultFromChallengeResponse(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ AphrontRequest $request,
+ array $challenges) {
+
+ $challenge = $this->getChallengeForCurrentContext(
+ $config,
+ $viewer,
+ $challenges);
+
+ $code = $this->getChallengeResponseFromRequest(
+ $config,
+ $request);
+
+ $result = $this->newResult()
+ ->setValue($code);
+
+ if ($challenge->getIsAnsweredChallenge()) {
+ return $result->setAnsweredChallenge($challenge);
+ }
+
+ if (phutil_hashes_are_identical($code, $challenge->getChallengeKey())) {
+ $ttl = PhabricatorTime::getNow() + phutil_units('15 minutes in seconds');
+
+ $challenge
+ ->markChallengeAsAnswered($ttl);
+
+ return $result->setAnsweredChallenge($challenge);
+ }
+
+ if (strlen($code)) {
+ $error_message = pht('Invalid');
+ } else {
+ $error_message = pht('Required');
+ }
+
+ $result->setErrorMessage($error_message);
+
+ return $result;
+ }
+
+ private function newSMSChallengeCode() {
+ $value = Filesystem::readRandomInteger(0, 99999999);
+ $value = sprintf('%08d', $value);
+ return $value;
+ }
+
+ private function isSMSMailerConfigured() {
+ $mailers = PhabricatorMetaMTAMail::newMailers(
+ array(
+ 'outbound' => true,
+ 'media' => array(
+ PhabricatorMailSMSMessage::MESSAGETYPE,
+ ),
+ ));
+
+ return (bool)$mailers;
+ }
+
+ private function loadUserContactNumber(PhabricatorUser $user) {
+ $contact_numbers = id(new PhabricatorAuthContactNumberQuery())
+ ->setViewer($user)
+ ->withObjectPHIDs(array($user->getPHID()))
+ ->withStatuses(
+ array(
+ PhabricatorAuthContactNumber::STATUS_ACTIVE,
+ ))
+ ->withIsPrimary(true)
+ ->execute();
+
+ if (count($contact_numbers) !== 1) {
+ return null;
+ }
+
+ return head($contact_numbers);
+ }
+
+ protected function newMFASyncTokenProperties(PhabricatorUser $user) {
+ $sms_code = $this->newSMSChallengeCode();
+
+ $envelope = new PhutilOpaqueEnvelope($sms_code);
+ $this->sendSMSCodeToUser($envelope, $user);
+
+ return array(
+ 'code' => $sms_code,
+ );
+ }
+
+ private function sendSMSCodeToUser(
+ PhutilOpaqueEnvelope $envelope,
+ PhabricatorUser $user) {
+
+ $uri = PhabricatorEnv::getURI('/');
+ $uri = new PhutilURI($uri);
+
+ return id(new PhabricatorMetaMTAMail())
+ ->setMessageType(PhabricatorMailSMSMessage::MESSAGETYPE)
+ ->addTos(array($user->getPHID()))
+ ->setForceDelivery(true)
+ ->setSensitiveContent(true)
+ ->setBody(
+ pht(
+ 'Phabricator (%s) MFA Code: %s',
+ $uri->getDomain(),
+ $envelope->openEnvelope()))
+ ->save();
+ }
+
+ private function normalizeSMSCode($code) {
+ return trim($code);
+ }
+
+ private function getChallengeResponseParameterName(
+ PhabricatorAuthFactorConfig $config) {
+ return $this->getParameterName($config, 'sms.code');
+ }
+
+ private function getChallengeResponseFromRequest(
+ PhabricatorAuthFactorConfig $config,
+ AphrontRequest $request) {
+
+ $name = $this->getChallengeResponseParameterName($config);
+
+ $value = $request->getStr($name);
+ $value = (string)$value;
+ $value = trim($value);
+
+ return $value;
+ }
+
+}
diff --git a/src/applications/auth/phid/PhabricatorAuthMessagePHIDType.php b/src/applications/auth/phid/PhabricatorAuthMessagePHIDType.php
--- a/src/applications/auth/phid/PhabricatorAuthMessagePHIDType.php
+++ b/src/applications/auth/phid/PhabricatorAuthMessagePHIDType.php
@@ -19,7 +19,9 @@
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
- return new PhabricatorAuthMessageQuery();
+
+ return id(new PhabricatorAuthMessageQuery())
+ ->withPHIDs($phids);
}
public function loadHandles(
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mon, Nov 25, 5:31 AM (36 m, 21 s)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6785033
Default Alt Text
D20022.diff (13 KB)
Attached To
Mode
D20022: Implement SMS MFA
Attached
Detach File
Event Timeline
Log In to Comment