Add Twilio Verify experiment to AccountController

This commit is contained in:
Chris Eager
2021-03-15 21:22:00 -05:00
committed by Chris Eager
parent 4c019aef15
commit 13053da97f
10 changed files with 442 additions and 93 deletions

View File

@@ -138,6 +138,7 @@ import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.AbusiveHostRules;
import org.whispersystems.textsecuregcm.storage.Account;
@@ -382,6 +383,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager(dynamicConfigurationManager);
TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager = new TwilioVerifyExperimentEnrollmentManager(
config.getVoiceVerificationConfiguration(), experimentEnrollmentManager);
ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false);
ExternalServiceCredentialGenerator backupCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false);
ExternalServiceCredentialGenerator paymentsCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getPaymentsServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false);
@@ -485,7 +489,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.jersey().register(new TimestampResponseFilter());
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, usernamesManager, abusiveHostRules, rateLimiters, smsSender, directoryQueue, messagesManager, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient, gcmSender, apnSender, backupCredentialsGenerator));
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, usernamesManager, abusiveHostRules, rateLimiters, smsSender, directoryQueue, messagesManager, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient, gcmSender, apnSender, backupCredentialsGenerator, verifyExperimentEnrollmentManager));
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, directoryQueue, rateLimiters, config.getMaxDevices()));
environment.jersey().register(new DirectoryController(directoryCredentialsGenerator));
environment.jersey().register(new ProvisioningController(rateLimiters, provisioningManager));

View File

@@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.util.Util;
import java.security.MessageDigest;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
public class StoredVerificationCode {
@@ -18,17 +19,26 @@ public class StoredVerificationCode {
private String code;
@JsonProperty
private long timestamp;
private long timestamp;
@JsonProperty
private String pushCode;
public StoredVerificationCode() {}
@JsonProperty
private String twilioVerificationSid;
public StoredVerificationCode() {
}
public StoredVerificationCode(String code, long timestamp, String pushCode) {
this.code = code;
this(code, timestamp, pushCode, null);
}
public StoredVerificationCode(String code, long timestamp, String pushCode, String twilioVerificationSid) {
this.code = code;
this.timestamp = timestamp;
this.pushCode = pushCode;
this.pushCode = pushCode;
this.twilioVerificationSid = twilioVerificationSid;
}
public String getCode() {
@@ -43,6 +53,10 @@ public class StoredVerificationCode {
return pushCode;
}
public Optional<String> getTwilioVerificationSid() {
return Optional.ofNullable(twilioVerificationSid);
}
public boolean isValid(String theirCodeString) {
if (timestamp + TimeUnit.MINUTES.toMillis(10) < System.currentTimeMillis()) {
return false;
@@ -52,10 +66,9 @@ public class StoredVerificationCode {
return false;
}
byte[] ourCode = code.getBytes();
byte[] ourCode = code.getBytes();
byte[] theirCode = theirCodeString.getBytes();
return MessageDigest.isEqual(ourCode, theirCode);
}
}

View File

@@ -22,6 +22,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@@ -64,6 +65,7 @@ import org.whispersystems.textsecuregcm.push.GCMSender;
import org.whispersystems.textsecuregcm.push.GcmMessage;
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.AbusiveHostRule;
import org.whispersystems.textsecuregcm.storage.AbusiveHostRules;
@@ -99,11 +101,15 @@ public class AccountController {
private static final String ACCOUNT_CREATE_COUNTER_NAME = name(AccountController.class, "create");
private static final String ACCOUNT_VERIFY_COUNTER_NAME = name(AccountController.class, "verify");
private static final String TWILIO_VERIFY_ERROR_COUNTER_NAME = name(AccountController.class, "twilioVerifyError");
private static final String CHALLENGE_PRESENT_TAG_NAME = "present";
private static final String CHALLENGE_MATCH_TAG_NAME = "matches";
private static final String COUNTRY_CODE_TAG_NAME = "countryCode";
private static final String VERFICATION_TRANSPORT_TAG_NAME = "transport";
private static final String VERIFY_EXPERIMENT_TAG_NAME = "twilioVerify";
private final PendingAccountsManager pendingAccounts;
private final AccountsManager accounts;
private final UsernamesManager usernames;
@@ -120,6 +126,8 @@ public class AccountController {
private final APNSender apnSender;
private final ExternalServiceCredentialGenerator backupServiceCredentialGenerator;
private final TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager;
public AccountController(PendingAccountsManager pendingAccounts,
AccountsManager accounts,
UsernamesManager usernames,
@@ -134,7 +142,8 @@ public class AccountController {
RecaptchaClient recaptchaClient,
GCMSender gcmSender,
APNSender apnSender,
ExternalServiceCredentialGenerator backupServiceCredentialGenerator)
ExternalServiceCredentialGenerator backupServiceCredentialGenerator,
TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager)
{
this.pendingAccounts = pendingAccounts;
this.accounts = accounts;
@@ -151,6 +160,7 @@ public class AccountController {
this.gcmSender = gcmSender;
this.apnSender = apnSender;
this.backupServiceCredentialGenerator = backupServiceCredentialGenerator;
this.verifyExperimentEnrollmentManager = verifyExperimentEnrollmentManager;
}
@Timed
@@ -246,27 +256,64 @@ public class AccountController {
VerificationCode verificationCode = generateVerificationCode(number);
StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(),
System.currentTimeMillis(),
storedChallenge.map(StoredVerificationCode::getPushCode).orElse(null));
System.currentTimeMillis(),
storedChallenge.map(StoredVerificationCode::getPushCode).orElse(null),
storedChallenge.flatMap(StoredVerificationCode::getTwilioVerificationSid).orElse(null));
pendingAccounts.store(number, storedVerificationCode);
final List<Locale.LanguageRange> languageRanges;
try {
languageRanges = acceptLanguage.map(Locale.LanguageRange::parse).orElse(Collections.emptyList());
} catch (final IllegalArgumentException e) {
return Response.status(400).build();
}
final boolean enrolledInVerifyExperiment = verifyExperimentEnrollmentManager.isEnrolled(client, number, languageRanges, transport);
final CompletableFuture<Optional<String>> sendVerificationWithTwilioVerifyFuture;
if (testDevices.containsKey(number)) {
// noop
sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty());
} else if (transport.equals("sms")) {
smsSender.deliverSmsVerification(number, client, verificationCode.getVerificationCodeDisplay());
} else if (transport.equals("voice")) {
final List<Locale.LanguageRange> languageRanges;
try {
languageRanges = acceptLanguage.map(Locale.LanguageRange::parse).orElse(Collections.emptyList());
} catch (final IllegalArgumentException e) {
return Response.status(400).build();
if (enrolledInVerifyExperiment) {
sendVerificationWithTwilioVerifyFuture = smsSender.deliverSmsVerificationWithTwilioVerify(number, client, verificationCode.getVerificationCode(), languageRanges);
} else {
smsSender.deliverSmsVerification(number, client, verificationCode.getVerificationCodeDisplay());
sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty());
}
} else if (transport.equals("voice")) {
if (enrolledInVerifyExperiment) {
sendVerificationWithTwilioVerifyFuture = smsSender.deliverVoxVerificationWithTwilioVerify(number, verificationCode.getVerificationCode(), languageRanges);
} else {
smsSender.deliverVoxVerification(number, verificationCode.getVerificationCode(), languageRanges);
sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty());
}
smsSender.deliverVoxVerification(number, verificationCode.getVerificationCode(), languageRanges);
} else {
sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty());
}
sendVerificationWithTwilioVerifyFuture.whenComplete((maybeVerificationSid, throwable) -> {
if (throwable != null) {
Metrics.counter(TWILIO_VERIFY_ERROR_COUNTER_NAME).increment();
logger.warn("Error with Twilio Verify", throwable);
return;
}
maybeVerificationSid.ifPresent(twilioVerificationSid -> {
StoredVerificationCode storedVerificationCodeWithVerificationSid = new StoredVerificationCode(
storedVerificationCode.getCode(),
storedVerificationCode.getTimestamp(),
storedVerificationCode.getPushCode(),
twilioVerificationSid);
pendingAccounts.store(number, storedVerificationCodeWithVerificationSid);
});
});
metricRegistry.meter(name(AccountController.class, "create", Util.getCountryCode(number))).mark();
{
@@ -274,6 +321,7 @@ public class AccountController {
tags.add(Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)));
tags.add(Tag.of(VERFICATION_TRANSPORT_TAG_NAME, transport));
tags.add(UserAgentTagUtil.getPlatformTag(userAgent));
tags.add(Tag.of(VERIFY_EXPERIMENT_TAG_NAME, String.valueOf(enrolledInVerifyExperiment)));
Metrics.counter(ACCOUNT_CREATE_COUNTER_NAME, tags).increment();
}
@@ -311,6 +359,9 @@ public class AccountController {
throw new WebApplicationException(Response.status(403).build());
}
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
.ifPresent(smsSender::reportVerificationSucceeded);
Optional<Account> existingAccount = accounts.get(number);
Optional<StoredRegistrationLock> existingRegistrationLock = existingAccount.map(Account::getRegistrationLock);
Optional<ExternalServiceCredentials> existingBackupCredentials = existingAccount.map(Account::getUuid)
@@ -345,6 +396,7 @@ public class AccountController {
final List<Tag> tags = new ArrayList<>();
tags.add(Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)));
tags.add(UserAgentTagUtil.getPlatformTag(userAgent));
tags.add(Tag.of(VERIFY_EXPERIMENT_TAG_NAME, String.valueOf(storedVerificationCode.get().getTwilioVerificationSid().isPresent())));
Metrics.counter(ACCOUNT_VERIFY_COUNTER_NAME, tags).increment();
}

View File

@@ -4,17 +4,17 @@
*/
package org.whispersystems.textsecuregcm.storage;
import static com.codahale.metrics.MetricRegistry.name;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
import java.util.Optional;
import com.google.common.annotations.VisibleForTesting;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.storage.mappers.StoredVerificationCodeRowMapper;
import org.whispersystems.textsecuregcm.util.Constants;
import java.util.Optional;
import static com.codahale.metrics.MetricRegistry.name;
public class PendingAccounts {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
@@ -30,17 +30,23 @@ public class PendingAccounts {
this.database.getDatabase().registerRowMapper(new StoredVerificationCodeRowMapper());
}
public void insert(String number, String verificationCode, long timestamp, String pushCode) {
@VisibleForTesting
public void insert (String number, String verificationCode, long timestamp, String pushCode) {
insert(number, verificationCode, timestamp, pushCode, null);
}
public void insert(String number, String verificationCode, long timestamp, String pushCode, String twilioVerificationSid) {
database.use(jdbi -> jdbi.useHandle(handle -> {
try (Timer.Context ignored = insertTimer.time()) {
handle.createUpdate("INSERT INTO pending_accounts (number, verification_code, timestamp, push_code) " +
"VALUES (:number, :verification_code, :timestamp, :push_code) " +
handle.createUpdate("INSERT INTO pending_accounts (number, verification_code, timestamp, push_code, twilio_verification_sid) " +
"VALUES (:number, :verification_code, :timestamp, :push_code, :twilio_verification_sid) " +
"ON CONFLICT(number) DO UPDATE " +
"SET verification_code = EXCLUDED.verification_code, timestamp = EXCLUDED.timestamp, push_code = EXCLUDED.push_code")
"SET verification_code = EXCLUDED.verification_code, timestamp = EXCLUDED.timestamp, push_code = EXCLUDED.push_code, twilio_verification_sid = EXCLUDED.twilio_verification_sid")
.bind("verification_code", verificationCode)
.bind("timestamp", timestamp)
.bind("number", number)
.bind("push_code", pushCode)
.bind("twilio_verification_sid", twilioVerificationSid)
.execute();
}
}));
@@ -49,7 +55,7 @@ public class PendingAccounts {
public Optional<StoredVerificationCode> getCodeForNumber(String number) {
return database.with(jdbi ->jdbi.withHandle(handle -> {
try (Timer.Context ignored = getCodeForNumberTimer.time()) {
return handle.createQuery("SELECT verification_code, timestamp, push_code FROM pending_accounts WHERE number = :number")
return handle.createQuery("SELECT verification_code, timestamp, push_code, twilio_verification_sid FROM pending_accounts WHERE number = :number")
.bind("number", number)
.mapTo(StoredVerificationCode.class)
.findFirst();

View File

@@ -34,7 +34,8 @@ public class PendingAccountsManager {
public void store(String number, StoredVerificationCode code) {
memcacheSet(number, code);
pendingAccounts.insert(number, code.getCode(), code.getTimestamp(), code.getPushCode());
pendingAccounts.insert(number, code.getCode(), code.getTimestamp(), code.getPushCode(),
code.getTwilioVerificationSid().orElse(null));
}
public void remove(String number) {

View File

@@ -4,17 +4,16 @@
*/
package org.whispersystems.textsecuregcm.storage;
import static com.codahale.metrics.MetricRegistry.name;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
import java.util.Optional;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.storage.mappers.StoredVerificationCodeRowMapper;
import org.whispersystems.textsecuregcm.util.Constants;
import java.util.Optional;
import static com.codahale.metrics.MetricRegistry.name;
public class PendingDevices {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
@@ -45,7 +44,7 @@ public class PendingDevices {
public Optional<StoredVerificationCode> getCodeForNumber(String number) {
return database.with(jdbi -> jdbi.withHandle(handle -> {
try (Timer.Context timer = getCodeForNumberTimer.time()) {
return handle.createQuery("SELECT verification_code, timestamp, NULL as push_code FROM pending_devices WHERE number = :number")
return handle.createQuery("SELECT verification_code, timestamp, NULL as push_code, NULL as twilio_verification_sid FROM pending_devices WHERE number = :number")
.bind("number", number)
.mapTo(StoredVerificationCode.class)
.findFirst();

View File

@@ -17,7 +17,8 @@ public class StoredVerificationCodeRowMapper implements RowMapper<StoredVerifica
@Override
public StoredVerificationCode map(ResultSet resultSet, StatementContext ctx) throws SQLException {
return new StoredVerificationCode(resultSet.getString("verification_code"),
resultSet.getLong("timestamp"),
resultSet.getString("push_code"));
resultSet.getLong("timestamp"),
resultSet.getString("push_code"),
resultSet.getString("twilio_verification_sid"));
}
}