Skip to content

Commit 7dafa5d

Browse files
Benmarkharding
authored andcommitted
Migrate to Twilio Verify API
1 parent 7654ffe commit 7dafa5d

File tree

9 files changed

+325
-56
lines changed

9 files changed

+325
-56
lines changed

Controllers/api/v1/twofactor.php

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Minds\Core\Security;
1515
use Minds\Core\Security\TwoFactor\TwoFactorRequiredException;
1616
use Minds\Core\SMS\Exceptions\VoIpPhoneException;
17+
use Minds\Helpers\FormatPhoneNumber;
1718
use Minds\Entities;
1819
use Minds\Interfaces;
1920
use Zend\Diactoros\ServerRequestFactory;
@@ -53,9 +54,21 @@ public function post($pages)
5354
$pages[0] = '';
5455
}
5556

57+
$featuresManager = Di::_()->get('Features\Manager');
58+
$twilioVerify = Di::_()->get('SMS\Twilio\Verify');
5659

5760
switch ($pages[0]) {
5861
case "setup":
62+
if ($featuresManager->has('twilio-verify')) {
63+
$number = FormatPhoneNumber::format($_POST['tel']);
64+
65+
if (!$twilioVerify->verify($number)) {
66+
throw new VoIpPhoneException();
67+
}
68+
69+
$twilioVerify->send($number, '');
70+
break;
71+
}
5972

6073
try {
6174
$twoFactorManager = Di::_()->get('Security\TwoFactor\Manager');
@@ -86,11 +99,7 @@ public function post($pages)
8699
}
87100

88101
$message = 'Minds TwoFactor code: '. $twofactor->getCode($secret);
89-
$number = $_POST['tel'];
90-
91-
if ($number[0] !== '+') {
92-
$number = '+'.$number;
93-
}
102+
$number = FormatPhoneNumber::format($_POST['tel']);
94103

95104
if ($sms->send($number, $message)) {
96105
$response['secret'] = $secret;
@@ -103,7 +112,21 @@ public function post($pages)
103112
case "check":
104113
$secret = $_POST['secret'];
105114
$code = $_POST['code'];
106-
$telno = $_POST['telno'];
115+
$telno = FormatPhoneNumber::format($_POST['telno']);
116+
117+
if ($featuresManager->has('twilio-verify')) {
118+
if ($twilioVerify->verifyCode($code, $telno)) {
119+
$user->twofactor = true;
120+
$user->telno = $telno;
121+
} else {
122+
$response['status'] = "error";
123+
$response['message'] = "2factor code failed";
124+
$user->twofactor = false;
125+
}
126+
$user->save();
127+
break;
128+
}
129+
107130
if ($twofactor->verifyCode($secret, $code, 1)) {
108131
$response['status'] = "success";
109132
$response['message'] = "2factor now setup";

Controllers/api/v2/blockchain/rewards.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ public function post($pages)
2626
if (!isset($_POST['number'])) {
2727
return Factory::response(['status' => 'error', 'message' => 'phone field is required']);
2828
}
29-
$number = $_POST['number'];
30-
$resend = $_POST['retry'];
29+
30+
$number = filter_var($_POST['number'] ?? '', FILTER_SANITIZE_STRING);
31+
$resend = filter_var($_POST['retry'] ?? false, FILTER_VALIDATE_BOOLEAN);
3132

3233
try {
3334
$join = new Join();
@@ -61,9 +62,10 @@ public function post($pages)
6162
}
6263
$code = $_POST['code'];
6364

64-
if (!isset($_POST['secret'])) {
65-
return Factory::response(['status' => 'error', 'message' => 'code field is required']);
65+
if (!Core\Di\Di::_()->get('Features\Manager')->has('twilio-verify') && !isset($_POST['secret'])) {
66+
return Factory::response(['status' => 'error', 'message' => 'secret field is required']);
6667
}
68+
6769
$secret = $_POST['secret'];
6870

6971
$user = Session::getLoggedInUser();

Core/Features/Provider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public function register()
8282
'cloudflare-streams',
8383
'notifications-v3',
8484
'withdrawal-console',
85+
'twilio-verify',
8586
'helpdesk-2021',
8687
'plus-discovery-filter',
8788
'twitter-sync',

Core/Rewards/Join.php

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ class Join
6262
/** @var JoinedValidator */
6363
private $joinedValidator;
6464

65+
/** @var Features\Manager */
66+
private $featuresManager;
67+
68+
/** @var TwilioVerify */
69+
private $twilioVerify;
70+
6571
public function __construct(
6672
$twofactor = null,
6773
$sms = null,
@@ -73,7 +79,9 @@ public function __construct(
7379
$ofacBlacklist = null,
7480
$testnetBalance = null,
7581
$referralDelegate = null,
76-
KeyValueLimiter $kvLimiter = null
82+
KeyValueLimiter $kvLimiter = null,
83+
$featuresManager = null,
84+
$twilioVerify = null
7785
) {
7886
$this->twofactor = $twofactor ?: Di::_()->get('Security\TwoFactor');
7987
$this->sms = $sms ?: Di::_()->get('SMS');
@@ -86,6 +94,8 @@ public function __construct(
8694
$this->testnetBalance = $testnetBalance ?: Di::_()->get('Blockchain\Wallets\OffChain\TestnetBalance');
8795
$this->referralDelegate = $referralDelegate ?: new Delegates\ReferralDelegate;
8896
$this->kvLimiter = $kvLimiter ?? Di::_()->get("Security\RateLimits\KeyValueLimiter");
97+
$this->featuresManager = $featuresManager ?? Di::_()->get('Features\Manager');
98+
$this->twilioVerify = $twilioVerify ?? Di::_()->get('SMS\Twilio\Verify');
8999
}
90100

91101
public function setUser(&$user)
@@ -127,8 +137,6 @@ public function setSecret($secret)
127137
*/
128138
public function verify()
129139
{
130-
$secret = $this->twofactor->createSecret();
131-
$code = $this->twofactor->getCode($secret);
132140

133141
// Limit a single account to 3 attempts per day
134142
$this->kvLimiter
@@ -138,6 +146,23 @@ public function verify()
138146
->setMax(3) // 2 per day
139147
->checkAndIncrement(); // Will throw exception
140148

149+
150+
if ($this->featuresManager->has('twilio-verify')) {
151+
if (!$this->twilioVerify->verify($this->number)) {
152+
throw new VoIpPhoneException();
153+
}
154+
155+
if (!$this->user->isEmailConfirmed()) {
156+
throw new UnverifiedEmailException();
157+
}
158+
159+
$this->twilioVerify->send($this->number, '');
160+
return;
161+
}
162+
163+
$secret = $this->twofactor->createSecret();
164+
$code = $this->twofactor->getCode($secret);
165+
141166
$user_guid = $this->user->guid;
142167
$this->db->insert("rewards:verificationcode:$user_guid", compact('code', 'secret'));
143168

@@ -158,6 +183,11 @@ public function verify()
158183

159184
public function resendCode()
160185
{
186+
if ($this->featuresManager->has('twilio-verify')) {
187+
$this->verify();
188+
return;
189+
}
190+
161191
$user_guid = $this->user->guid;
162192
$username = $this->user->getUsername();
163193
$row = $this->db->getRow("rewards:verificationcode:$user_guid");
@@ -180,7 +210,15 @@ public function confirm()
180210
return false; //already joined
181211
}
182212

183-
if ($this->twofactor->verifyCode($this->secret, $this->code, 8)) {
213+
$valid = false;
214+
215+
if ($this->featuresManager->has('twilio-verify')) {
216+
$valid = $this->twilioVerify->verifyCode($this->code, $this->number);
217+
} else {
218+
$valid = $this->twofactor->verifyCode($this->secret, $this->code, 8);
219+
}
220+
221+
if ($valid) {
184222
$hash = hash('sha256', $this->number . $this->config->get('phone_number_hash_salt'));
185223
$this->user->setPhoneNumberHash($hash);
186224
$this->user->save();

Core/SMS/SMSProvider.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
use Minds\Core\Di\Provider;
99
use Minds\Core\SMS\Services\Twilio;
10+
use Minds\Core\SMS\Services\TwilioVerify;
1011

1112
class SMSProvider extends Provider
1213
{
@@ -15,6 +16,9 @@ public function register()
1516
$this->di->bind('SMS', function ($di) {
1617
return new Twilio();
1718
}, ['useFactory' => true]);
19+
$this->di->bind('SMS\Twilio\Verify', function ($di) {
20+
return new TwilioVerify();
21+
}, ['useFactory' => true]);
1822
$this->di->bind('SMS\SNS', function ($di) {
1923
return new Services\SNS();
2024
}, ['useFactory' => true]);

Core/SMS/Services/TwilioVerify.php

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
3+
/**
4+
* Minds Twilio Verify Service
5+
*/
6+
7+
namespace Minds\Core\SMS\Services;
8+
9+
use Minds\Common\IpAddress;
10+
use Minds\Core\Di\Di;
11+
use Minds\Core\Config;
12+
use Minds\Core\SMS\Exceptions\InvalidPhoneException;
13+
use Minds\Core\SMS\SMSServiceInterface;
14+
use Twilio\Rest\Client as TwilioClient;
15+
use Minds\Core\Security\RateLimits\KeyValueLimiter;
16+
17+
class TwilioVerify implements SMSServiceInterface
18+
{
19+
/** @var TwilioClient */
20+
protected $client;
21+
22+
/** @var Config */
23+
protected $config;
24+
25+
/** @var string */
26+
protected $from;
27+
28+
/** @var KeyValueLimiter */
29+
protected $kvLimiter;
30+
31+
/** @var IpAddress */
32+
protected $ipAddress;
33+
34+
public function __construct($client = null, $config = null, $kvLimiter = null, IpAddress $ipAddress = null)
35+
{
36+
$this->config = $config ?? Di::_()->get('Config');
37+
$this->client = $client;
38+
$this->kvLimiter = $kvLimiter ?? Di::_()->get("Security\RateLimits\KeyValueLimiter");
39+
$this->ipAddress = $ipAddress ?? new IpAddress();
40+
}
41+
42+
/**
43+
* Verifies the number isn't a voip line.
44+
* @param $number
45+
* @return boolean
46+
* @throws InvalidPhoneException
47+
*/
48+
public function verify($number): bool
49+
{
50+
try {
51+
$phone_number = $this->getClient()->lookups->v1->phoneNumbers($number)
52+
->fetch(["type" => "carrier"]);
53+
54+
return $phone_number->carrier['type'] !== 'voip';
55+
} catch (\Exception $e) {
56+
error_log("[guard] Twilio error: {$e->getMessage()}");
57+
throw new InvalidPhoneException('Invalid Phone Number', 0, $e);
58+
}
59+
}
60+
61+
/**
62+
* Send a verification request.
63+
* @param string $number - users phone number.
64+
* @param string $number - depreciated.
65+
*/
66+
public function send($number, $message = ''): bool
67+
{
68+
$result = null;
69+
70+
// // Only allow 5 messages sent to a number per day
71+
// // To prevent malicious users flooding the system
72+
$phoneNumberHash = hash('sha256', $number . $this->config->get('phone_number_hash_salt'));
73+
74+
$this->kvLimiter
75+
->setKey('sms-sender-twilio')
76+
->setValue($phoneNumberHash)
77+
->setSeconds(86400) // Day
78+
->setMax(5) // 5 per day
79+
->checkAndIncrement(); // Will throw exception
80+
81+
// Only allow 10 SMS messages per IP address per day
82+
$this->kvLimiter
83+
->setKey('sms-sender-twilio-ip')
84+
->setValue($this->ipAddress->get())
85+
->setSeconds(86400) // Day
86+
->setMax(10) // 10 per day
87+
->checkAndIncrement(); // Will throw exception
88+
89+
try {
90+
// Send SMS
91+
$result = $this->getClient()->verify->v2->services($this->getConfig()['verify']['service_sid'])
92+
->verifications
93+
->create($number, "sms");
94+
} catch (\Exception $e) {
95+
error_log("[guard] TwilioVerify error: {$e->getMessage()}");
96+
}
97+
98+
return $result ? $result->sid : false;
99+
}
100+
101+
/**
102+
* Verify a given code.
103+
* @param string $code - code received by user.
104+
* @param string $number - users phone number.
105+
* @return bool - true if code is valid.
106+
*/
107+
public function verifyCode(string $code, string $number): bool
108+
{
109+
$result = $this->getClient()->verify->v2->services(
110+
$this->getConfig()['verify']['service_sid']
111+
)
112+
->verificationChecks
113+
->create(
114+
$code,
115+
["to" => $number]
116+
);
117+
118+
return $result->status === 'approved';
119+
}
120+
121+
/**
122+
* Get the Twilio client
123+
* @return TwilioClient
124+
*/
125+
private function getClient(): TwilioClient
126+
{
127+
if (!$this->client) {
128+
$AccountSid = $this->getConfig()['account_sid'] ?: 'not set';
129+
$AuthToken = $this->getConfig()['auth_token'] ?: 'not set';
130+
$this->client = new TwilioClient($AccountSid, $AuthToken);
131+
}
132+
return $this->client;
133+
}
134+
135+
/**
136+
* Get Twilio config
137+
* @return array
138+
*/
139+
private function getConfig(): array
140+
{
141+
return $this->config->get('twilio');
142+
}
143+
}

0 commit comments

Comments
 (0)