Path: blob/master/src/applications/auth/factor/PhabricatorSMSAuthFactor.php
12256 views
<?php12final class PhabricatorSMSAuthFactor3extends PhabricatorAuthFactor {45public function getFactorKey() {6return 'sms';7}89public function getFactorName() {10return pht('Text Message (SMS)');11}1213public function getFactorShortName() {14return pht('SMS');15}1617public function getFactorCreateHelp() {18return pht(19'Allow users to receive a code via SMS.');20}2122public function getFactorDescription() {23return pht(24'When you need to authenticate, a text message with a code will '.25'be sent to your phone.');26}2728public function getFactorOrder() {29// Sort this factor toward the end of the list because SMS is relatively30// weak.31return 2000;32}3334public function isContactNumberFactor() {35return true;36}3738public function canCreateNewProvider() {39return $this->isSMSMailerConfigured();40}4142public function getProviderCreateDescription() {43$messages = array();4445if (!$this->isSMSMailerConfigured()) {46$messages[] = id(new PHUIInfoView())47->setErrors(48array(49pht(50'You have not configured an outbound SMS mailer. You must '.51'configure one before you can set up SMS. See: %s',52phutil_tag(53'a',54array(55'href' => '/config/edit/cluster.mailers/',56),57'cluster.mailers')),58));59}6061$messages[] = id(new PHUIInfoView())62->setSeverity(PHUIInfoView::SEVERITY_WARNING)63->setErrors(64array(65pht(66'SMS is weak, and relatively easy for attackers to compromise. '.67'Strongly consider using a different MFA provider.'),68));6970return $messages;71}7273public function canCreateNewConfiguration(74PhabricatorAuthFactorProvider $provider,75PhabricatorUser $user) {7677if (!$this->loadUserContactNumber($user)) {78return false;79}8081if ($this->loadConfigurationsForProvider($provider, $user)) {82return false;83}8485return true;86}8788public function getConfigurationCreateDescription(89PhabricatorAuthFactorProvider $provider,90PhabricatorUser $user) {9192$messages = array();9394if (!$this->loadUserContactNumber($user)) {95$messages[] = id(new PHUIInfoView())96->setSeverity(PHUIInfoView::SEVERITY_WARNING)97->setErrors(98array(99pht(100'You have not configured a primary contact number. Configure '.101'a contact number before adding SMS as an authentication '.102'factor.'),103));104}105106if ($this->loadConfigurationsForProvider($provider, $user)) {107$messages[] = id(new PHUIInfoView())108->setSeverity(PHUIInfoView::SEVERITY_WARNING)109->setErrors(110array(111pht(112'You already have SMS authentication attached to your account.'),113));114}115116return $messages;117}118119public function getEnrollDescription(120PhabricatorAuthFactorProvider $provider,121PhabricatorUser $user) {122return pht(123'To verify your phone as an authentication factor, a text message with '.124'a secret code will be sent to the phone number you have listed as '.125'your primary contact number.');126}127128public function getEnrollButtonText(129PhabricatorAuthFactorProvider $provider,130PhabricatorUser $user) {131$contact_number = $this->loadUserContactNumber($user);132133return pht('Send SMS: %s', $contact_number->getDisplayName());134}135136public function processAddFactorForm(137PhabricatorAuthFactorProvider $provider,138AphrontFormView $form,139AphrontRequest $request,140PhabricatorUser $user) {141142$token = $this->loadMFASyncToken($provider, $request, $form, $user);143$code = $request->getStr('sms.code');144145$e_code = true;146if (!$token->getIsNewTemporaryToken()) {147$expect_code = $token->getTemporaryTokenProperty('code');148149$okay = phutil_hashes_are_identical(150$this->normalizeSMSCode($code),151$this->normalizeSMSCode($expect_code));152153if ($okay) {154$config = $this->newConfigForUser($user)155->setFactorName(pht('SMS'));156157return $config;158} else {159if (!strlen($code)) {160$e_code = pht('Required');161} else {162$e_code = pht('Invalid');163}164}165}166167$form->appendRemarkupInstructions(168pht(169'Enter the code from the text message which was sent to your '.170'primary contact number.'));171172$form->appendChild(173id(new PHUIFormNumberControl())174->setLabel(pht('SMS Code'))175->setName('sms.code')176->setValue($code)177->setError($e_code));178}179180protected function newIssuedChallenges(181PhabricatorAuthFactorConfig $config,182PhabricatorUser $viewer,183array $challenges) {184185// If we already issued a valid challenge for this workflow and session,186// don't issue a new one.187188$challenge = $this->getChallengeForCurrentContext(189$config,190$viewer,191$challenges);192if ($challenge) {193return array();194}195196if (!$this->loadUserContactNumber($viewer)) {197return $this->newResult()198->setIsError(true)199->setErrorMessage(200pht(201'Your account has no primary contact number.'));202}203204if (!$this->isSMSMailerConfigured()) {205return $this->newResult()206->setIsError(true)207->setErrorMessage(208pht(209'No outbound mailer which can deliver SMS messages is '.210'configured.'));211}212213if (!$this->hasCSRF($config)) {214return $this->newResult()215->setIsContinue(true)216->setErrorMessage(217pht(218'A text message with an authorization code will be sent to your '.219'primary contact number.'));220}221222// Otherwise, issue a new challenge.223224$challenge_code = $this->newSMSChallengeCode();225$envelope = new PhutilOpaqueEnvelope($challenge_code);226$this->sendSMSCodeToUser($envelope, $viewer);227228$ttl_seconds = phutil_units('15 minutes in seconds');229230return array(231$this->newChallenge($config, $viewer)232->setChallengeKey($challenge_code)233->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds),234);235}236237protected function newResultFromIssuedChallenges(238PhabricatorAuthFactorConfig $config,239PhabricatorUser $viewer,240array $challenges) {241242$challenge = $this->getChallengeForCurrentContext(243$config,244$viewer,245$challenges);246247if ($challenge->getIsAnsweredChallenge()) {248return $this->newResult()249->setAnsweredChallenge($challenge);250}251252return null;253}254255public function renderValidateFactorForm(256PhabricatorAuthFactorConfig $config,257AphrontFormView $form,258PhabricatorUser $viewer,259PhabricatorAuthFactorResult $result) {260261$control = $this->newAutomaticControl($result);262if (!$control) {263$value = $result->getValue();264$error = $result->getErrorMessage();265$name = $this->getChallengeResponseParameterName($config);266267$control = id(new PHUIFormNumberControl())268->setName($name)269->setDisableAutocomplete(true)270->setValue($value)271->setError($error);272}273274$control275->setLabel(pht('SMS Code'))276->setCaption(pht('Factor Name: %s', $config->getFactorName()));277278$form->appendChild($control);279}280281public function getRequestHasChallengeResponse(282PhabricatorAuthFactorConfig $config,283AphrontRequest $request) {284$value = $this->getChallengeResponseFromRequest($config, $request);285return (bool)strlen($value);286}287288protected function newResultFromChallengeResponse(289PhabricatorAuthFactorConfig $config,290PhabricatorUser $viewer,291AphrontRequest $request,292array $challenges) {293294$challenge = $this->getChallengeForCurrentContext(295$config,296$viewer,297$challenges);298299$code = $this->getChallengeResponseFromRequest(300$config,301$request);302303$result = $this->newResult()304->setValue($code);305306if ($challenge->getIsAnsweredChallenge()) {307return $result->setAnsweredChallenge($challenge);308}309310if (phutil_hashes_are_identical($code, $challenge->getChallengeKey())) {311$ttl = PhabricatorTime::getNow() + phutil_units('15 minutes in seconds');312313$challenge314->markChallengeAsAnswered($ttl);315316return $result->setAnsweredChallenge($challenge);317}318319if (strlen($code)) {320$error_message = pht('Invalid');321} else {322$error_message = pht('Required');323}324325$result->setErrorMessage($error_message);326327return $result;328}329330private function newSMSChallengeCode() {331$value = Filesystem::readRandomInteger(0, 99999999);332$value = sprintf('%08d', $value);333return $value;334}335336private function isSMSMailerConfigured() {337$mailers = PhabricatorMetaMTAMail::newMailers(338array(339'outbound' => true,340'media' => array(341PhabricatorMailSMSMessage::MESSAGETYPE,342),343));344345return (bool)$mailers;346}347348private function loadUserContactNumber(PhabricatorUser $user) {349$contact_numbers = id(new PhabricatorAuthContactNumberQuery())350->setViewer($user)351->withObjectPHIDs(array($user->getPHID()))352->withStatuses(353array(354PhabricatorAuthContactNumber::STATUS_ACTIVE,355))356->withIsPrimary(true)357->execute();358359if (count($contact_numbers) !== 1) {360return null;361}362363return head($contact_numbers);364}365366protected function newMFASyncTokenProperties(367PhabricatorAuthFactorProvider $providerr,368PhabricatorUser $user) {369370$sms_code = $this->newSMSChallengeCode();371372$envelope = new PhutilOpaqueEnvelope($sms_code);373$this->sendSMSCodeToUser($envelope, $user);374375return array(376'code' => $sms_code,377);378}379380private function sendSMSCodeToUser(381PhutilOpaqueEnvelope $envelope,382PhabricatorUser $user) {383return id(new PhabricatorMetaMTAMail())384->setMessageType(PhabricatorMailSMSMessage::MESSAGETYPE)385->addTos(array($user->getPHID()))386->setForceDelivery(true)387->setSensitiveContent(true)388->setBody(389pht(390'%s (%s) MFA Code: %s',391PlatformSymbols::getPlatformServerName(),392$this->getInstallDisplayName(),393$envelope->openEnvelope()))394->save();395}396397private function normalizeSMSCode($code) {398return trim($code);399}400401}402403404