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

at recaptime-dev/main 402 lines 10 kB view raw
1<?php 2 3final class PhabricatorSMSAuthFactor 4 extends PhabricatorAuthFactor { 5 6 public function getFactorKey() { 7 return 'sms'; 8 } 9 10 public function getFactorName() { 11 return pht('Text Message (SMS)'); 12 } 13 14 public function getFactorShortName() { 15 return pht('SMS'); 16 } 17 18 public function getFactorCreateHelp() { 19 return pht( 20 'Allow users to receive a code via SMS.'); 21 } 22 23 public function getFactorDescription() { 24 return pht( 25 'When you need to authenticate, a text message with a code will '. 26 'be sent to your phone.'); 27 } 28 29 public function getFactorOrder() { 30 // Sort this factor toward the end of the list because SMS is relatively 31 // weak. 32 return 2000; 33 } 34 35 public function isContactNumberFactor() { 36 return true; 37 } 38 39 public function canCreateNewProvider() { 40 return $this->isSMSMailerConfigured(); 41 } 42 43 public function getProviderCreateDescription() { 44 $messages = array(); 45 46 if (!$this->isSMSMailerConfigured()) { 47 $messages[] = id(new PHUIInfoView()) 48 ->setErrors( 49 array( 50 pht( 51 'You have not configured an outbound SMS mailer. You must '. 52 'configure one before you can set up SMS. See: %s', 53 phutil_tag( 54 'a', 55 array( 56 'href' => '/config/edit/cluster.mailers/', 57 ), 58 'cluster.mailers')), 59 )); 60 } 61 62 $messages[] = id(new PHUIInfoView()) 63 ->setSeverity(PHUIInfoView::SEVERITY_WARNING) 64 ->setErrors( 65 array( 66 pht( 67 'SMS is weak, and relatively easy for attackers to compromise. '. 68 'Strongly consider using a different MFA provider.'), 69 )); 70 71 return $messages; 72 } 73 74 public function canCreateNewConfiguration( 75 PhabricatorAuthFactorProvider $provider, 76 PhabricatorUser $user) { 77 78 if (!$this->loadUserContactNumber($user)) { 79 return false; 80 } 81 82 if ($this->loadConfigurationsForProvider($provider, $user)) { 83 return false; 84 } 85 86 return true; 87 } 88 89 public function getConfigurationCreateDescription( 90 PhabricatorAuthFactorProvider $provider, 91 PhabricatorUser $user) { 92 93 $messages = array(); 94 95 if (!$this->loadUserContactNumber($user)) { 96 $messages[] = id(new PHUIInfoView()) 97 ->setSeverity(PHUIInfoView::SEVERITY_WARNING) 98 ->setErrors( 99 array( 100 pht( 101 'You have not configured a primary contact number. Configure '. 102 'a contact number before adding SMS as an authentication '. 103 'factor.'), 104 )); 105 } 106 107 if ($this->loadConfigurationsForProvider($provider, $user)) { 108 $messages[] = id(new PHUIInfoView()) 109 ->setSeverity(PHUIInfoView::SEVERITY_WARNING) 110 ->setErrors( 111 array( 112 pht( 113 'You already have SMS authentication attached to your account.'), 114 )); 115 } 116 117 return $messages; 118 } 119 120 public function getEnrollDescription( 121 PhabricatorAuthFactorProvider $provider, 122 PhabricatorUser $user) { 123 return pht( 124 'To verify your phone as an authentication factor, a text message with '. 125 'a secret code will be sent to the phone number you have listed as '. 126 'your primary contact number.'); 127 } 128 129 public function getEnrollButtonText( 130 PhabricatorAuthFactorProvider $provider, 131 PhabricatorUser $user) { 132 $contact_number = $this->loadUserContactNumber($user); 133 134 return pht('Send SMS: %s', $contact_number->getDisplayName()); 135 } 136 137 public function processAddFactorForm( 138 PhabricatorAuthFactorProvider $provider, 139 AphrontFormView $form, 140 AphrontRequest $request, 141 PhabricatorUser $user) { 142 143 $token = $this->loadMFASyncToken($provider, $request, $form, $user); 144 $code = $request->getStr('sms.code'); 145 146 $e_code = true; 147 if (!$token->getIsNewTemporaryToken()) { 148 $expect_code = $token->getTemporaryTokenProperty('code'); 149 150 $okay = phutil_hashes_are_identical( 151 $this->normalizeSMSCode($code), 152 $this->normalizeSMSCode($expect_code)); 153 154 if ($okay) { 155 $config = $this->newConfigForUser($user) 156 ->setFactorName(pht('SMS')); 157 158 return $config; 159 } else { 160 if (!strlen($code)) { 161 $e_code = pht('Required'); 162 } else { 163 $e_code = pht('Invalid'); 164 } 165 } 166 } 167 168 $form->appendRemarkupInstructions( 169 pht( 170 'Enter the code from the text message which was sent to your '. 171 'primary contact number.')); 172 173 $form->appendChild( 174 id(new PHUIFormNumberControl()) 175 ->setLabel(pht('SMS Code')) 176 ->setName('sms.code') 177 ->setValue($code) 178 ->setError($e_code)); 179 } 180 181 protected function newIssuedChallenges( 182 PhabricatorAuthFactorConfig $config, 183 PhabricatorUser $viewer, 184 array $challenges) { 185 186 // If we already issued a valid challenge for this workflow and session, 187 // don't issue a new one. 188 189 $challenge = $this->getChallengeForCurrentContext( 190 $config, 191 $viewer, 192 $challenges); 193 if ($challenge) { 194 return array(); 195 } 196 197 if (!$this->loadUserContactNumber($viewer)) { 198 return $this->newResult() 199 ->setIsError(true) 200 ->setErrorMessage( 201 pht( 202 'Your account has no primary contact number.')); 203 } 204 205 if (!$this->isSMSMailerConfigured()) { 206 return $this->newResult() 207 ->setIsError(true) 208 ->setErrorMessage( 209 pht( 210 'No outbound mailer which can deliver SMS messages is '. 211 'configured.')); 212 } 213 214 if (!$this->hasCSRF($config)) { 215 return $this->newResult() 216 ->setIsContinue(true) 217 ->setErrorMessage( 218 pht( 219 'A text message with an authorization code will be sent to your '. 220 'primary contact number.')); 221 } 222 223 // Otherwise, issue a new challenge. 224 225 $challenge_code = $this->newSMSChallengeCode(); 226 $envelope = new PhutilOpaqueEnvelope($challenge_code); 227 $this->sendSMSCodeToUser($envelope, $viewer); 228 229 $ttl_seconds = phutil_units('15 minutes in seconds'); 230 231 return array( 232 $this->newChallenge($config, $viewer) 233 ->setChallengeKey($challenge_code) 234 ->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds), 235 ); 236 } 237 238 protected function newResultFromIssuedChallenges( 239 PhabricatorAuthFactorConfig $config, 240 PhabricatorUser $viewer, 241 array $challenges) { 242 243 $challenge = $this->getChallengeForCurrentContext( 244 $config, 245 $viewer, 246 $challenges); 247 248 if ($challenge->getIsAnsweredChallenge()) { 249 return $this->newResult() 250 ->setAnsweredChallenge($challenge); 251 } 252 253 return null; 254 } 255 256 public function renderValidateFactorForm( 257 PhabricatorAuthFactorConfig $config, 258 AphrontFormView $form, 259 PhabricatorUser $viewer, 260 PhabricatorAuthFactorResult $result) { 261 262 $control = $this->newAutomaticControl($result); 263 if (!$control) { 264 $value = $result->getValue(); 265 $error = $result->getErrorMessage(); 266 $name = $this->getChallengeResponseParameterName($config); 267 268 $control = id(new PHUIFormNumberControl()) 269 ->setName($name) 270 ->setDisableAutocomplete(true) 271 ->setValue($value) 272 ->setError($error); 273 } 274 275 $control 276 ->setLabel(pht('SMS Code')) 277 ->setCaption(pht('Factor Name: %s', $config->getFactorName())); 278 279 $form->appendChild($control); 280 } 281 282 public function getRequestHasChallengeResponse( 283 PhabricatorAuthFactorConfig $config, 284 AphrontRequest $request) { 285 $value = $this->getChallengeResponseFromRequest($config, $request); 286 return (bool)strlen($value); 287 } 288 289 protected function newResultFromChallengeResponse( 290 PhabricatorAuthFactorConfig $config, 291 PhabricatorUser $viewer, 292 AphrontRequest $request, 293 array $challenges) { 294 295 $challenge = $this->getChallengeForCurrentContext( 296 $config, 297 $viewer, 298 $challenges); 299 300 $code = $this->getChallengeResponseFromRequest( 301 $config, 302 $request); 303 304 $result = $this->newResult() 305 ->setValue($code); 306 307 if ($challenge->getIsAnsweredChallenge()) { 308 return $result->setAnsweredChallenge($challenge); 309 } 310 311 if (phutil_hashes_are_identical($code, $challenge->getChallengeKey())) { 312 $ttl = PhabricatorTime::getNow() + phutil_units('15 minutes in seconds'); 313 314 $challenge 315 ->markChallengeAsAnswered($ttl); 316 317 return $result->setAnsweredChallenge($challenge); 318 } 319 320 if (strlen($code)) { 321 $error_message = pht('Invalid'); 322 } else { 323 $error_message = pht('Required'); 324 } 325 326 $result->setErrorMessage($error_message); 327 328 return $result; 329 } 330 331 private function newSMSChallengeCode() { 332 $value = Filesystem::readRandomInteger(0, 99999999); 333 $value = sprintf('%08d', $value); 334 return $value; 335 } 336 337 public function isSMSMailerConfigured() { 338 $mailers = PhabricatorMetaMTAMail::newMailers( 339 array( 340 'outbound' => true, 341 'media' => array( 342 PhabricatorMailSMSMessage::MESSAGETYPE, 343 ), 344 )); 345 346 return (bool)$mailers; 347 } 348 349 private function loadUserContactNumber(PhabricatorUser $user) { 350 $contact_numbers = id(new PhabricatorAuthContactNumberQuery()) 351 ->setViewer($user) 352 ->withObjectPHIDs(array($user->getPHID())) 353 ->withStatuses( 354 array( 355 PhabricatorAuthContactNumber::STATUS_ACTIVE, 356 )) 357 ->withIsPrimary(true) 358 ->execute(); 359 360 if (count($contact_numbers) !== 1) { 361 return null; 362 } 363 364 return head($contact_numbers); 365 } 366 367 protected function newMFASyncTokenProperties( 368 PhabricatorAuthFactorProvider $providerr, 369 PhabricatorUser $user) { 370 371 $sms_code = $this->newSMSChallengeCode(); 372 373 $envelope = new PhutilOpaqueEnvelope($sms_code); 374 $this->sendSMSCodeToUser($envelope, $user); 375 376 return array( 377 'code' => $sms_code, 378 ); 379 } 380 381 private function sendSMSCodeToUser( 382 PhutilOpaqueEnvelope $envelope, 383 PhabricatorUser $user) { 384 return id(new PhabricatorMetaMTAMail()) 385 ->setMessageType(PhabricatorMailSMSMessage::MESSAGETYPE) 386 ->addTos(array($user->getPHID())) 387 ->setForceDelivery(true) 388 ->setSensitiveContent(true) 389 ->setBody( 390 pht( 391 '%s (%s) MFA Code: %s', 392 PlatformSymbols::getPlatformServerName(), 393 $this->getInstallDisplayName(), 394 $envelope->openEnvelope())) 395 ->save(); 396 } 397 398 private function normalizeSMSCode($code) { 399 return trim($code); 400 } 401 402}