Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/auth/factor/PhabricatorSMSAuthFactor.php
12256 views
1
<?php
2
3
final 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
private 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
}
403
404