handle new RegistrationService proto error

This commit is contained in:
Ravi Khadiwala 2023-12-19 13:37:04 -06:00 committed by ravi-signal
parent 9d3d4a3698
commit ca47a7b663
8 changed files with 102 additions and 6 deletions

View File

@ -844,7 +844,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
config.getCdnConfiguration().bucket()), config.getCdnConfiguration().bucket()),
new VerificationController(registrationServiceClient, new VerificationSessionManager(verificationSessions), new VerificationController(registrationServiceClient, new VerificationSessionManager(verificationSessions),
pushNotificationManager, registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters, pushNotificationManager, registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters,
accountsManager, useRemoteAddress, clock) accountsManager, useRemoteAddress, dynamicConfigurationManager, clock)
); );
if (config.getSubscription() != null && config.getOneTimeDonations() != null) { if (config.getSubscription() != null && config.getOneTimeDonations() != null) {
commonControllers.add(new SubscriptionController(clock, config.getSubscription(), config.getOneTimeDonations(), commonControllers.add(new SubscriptionController(clock, config.getSubscription(), config.getOneTimeDonations(),

View File

@ -59,6 +59,11 @@ public class DynamicConfiguration {
@Valid @Valid
DynamicInboundMessageByteLimitConfiguration inboundMessageByteLimit = new DynamicInboundMessageByteLimitConfiguration(true); DynamicInboundMessageByteLimitConfiguration inboundMessageByteLimit = new DynamicInboundMessageByteLimitConfiguration(true);
@JsonProperty
@Valid
DynamicRegistrationConfiguration registrationConfiguration = new DynamicRegistrationConfiguration(false);
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration( public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
final String experimentName) { final String experimentName) {
return Optional.ofNullable(experiments.get(experimentName)); return Optional.ofNullable(experiments.get(experimentName));
@ -104,4 +109,8 @@ public class DynamicConfiguration {
public DynamicInboundMessageByteLimitConfiguration getInboundMessageByteLimitConfiguration() { public DynamicInboundMessageByteLimitConfiguration getInboundMessageByteLimitConfiguration() {
return inboundMessageByteLimit; return inboundMessageByteLimit;
} }
public DynamicRegistrationConfiguration getRegistrationConfiguration() {
return registrationConfiguration;
}
} }

View File

@ -0,0 +1,7 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.dynamic;
public record DynamicRegistrationConfiguration(boolean squashDeclinedAttemptErrors) {}

View File

@ -59,6 +59,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.captcha.AssessmentResult; import org.whispersystems.textsecuregcm.captcha.AssessmentResult;
import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager; import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.entities.CreateVerificationSessionRequest; import org.whispersystems.textsecuregcm.entities.CreateVerificationSessionRequest;
import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession; import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession;
import org.whispersystems.textsecuregcm.entities.SubmitVerificationCodeRequest; import org.whispersystems.textsecuregcm.entities.SubmitVerificationCodeRequest;
@ -72,6 +73,7 @@ import org.whispersystems.textsecuregcm.push.PushNotification;
import org.whispersystems.textsecuregcm.push.PushNotificationManager; import org.whispersystems.textsecuregcm.push.PushNotificationManager;
import org.whispersystems.textsecuregcm.registration.ClientType; import org.whispersystems.textsecuregcm.registration.ClientType;
import org.whispersystems.textsecuregcm.registration.MessageTransport; import org.whispersystems.textsecuregcm.registration.MessageTransport;
import org.whispersystems.textsecuregcm.registration.RegistrationFraudException;
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
import org.whispersystems.textsecuregcm.registration.RegistrationServiceException; import org.whispersystems.textsecuregcm.registration.RegistrationServiceException;
import org.whispersystems.textsecuregcm.registration.RegistrationServiceSenderException; import org.whispersystems.textsecuregcm.registration.RegistrationServiceSenderException;
@ -81,6 +83,7 @@ import org.whispersystems.textsecuregcm.spam.Extract;
import org.whispersystems.textsecuregcm.spam.FilterSpam; import org.whispersystems.textsecuregcm.spam.FilterSpam;
import org.whispersystems.textsecuregcm.spam.ScoreThreshold; import org.whispersystems.textsecuregcm.spam.ScoreThreshold;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
import org.whispersystems.textsecuregcm.storage.VerificationSessionManager; import org.whispersystems.textsecuregcm.storage.VerificationSessionManager;
import org.whispersystems.textsecuregcm.util.ExceptionUtils; import org.whispersystems.textsecuregcm.util.ExceptionUtils;
@ -119,6 +122,7 @@ public class VerificationController {
private final AccountsManager accountsManager; private final AccountsManager accountsManager;
private final boolean useRemoteAddress; private final boolean useRemoteAddress;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private final Clock clock; private final Clock clock;
public VerificationController(final RegistrationServiceClient registrationServiceClient, public VerificationController(final RegistrationServiceClient registrationServiceClient,
@ -129,6 +133,7 @@ public class VerificationController {
final RateLimiters rateLimiters, final RateLimiters rateLimiters,
final AccountsManager accountsManager, final AccountsManager accountsManager,
final boolean useRemoteAddress, final boolean useRemoteAddress,
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
final Clock clock) { final Clock clock) {
this.registrationServiceClient = registrationServiceClient; this.registrationServiceClient = registrationServiceClient;
this.verificationSessionManager = verificationSessionManager; this.verificationSessionManager = verificationSessionManager;
@ -138,6 +143,7 @@ public class VerificationController {
this.rateLimiters = rateLimiters; this.rateLimiters = rateLimiters;
this.accountsManager = accountsManager; this.accountsManager = accountsManager;
this.useRemoteAddress = useRemoteAddress; this.useRemoteAddress = useRemoteAddress;
this.dynamicConfigurationManager = dynamicConfigurationManager;
this.clock = clock; this.clock = clock;
} }
@ -501,10 +507,14 @@ public class VerificationController {
}) })
.orElseGet(NotFoundException::new); .orElseGet(NotFoundException::new);
} else if (unwrappedException instanceof RegistrationFraudException) {
if (dynamicConfigurationManager.getConfiguration().getRegistrationConfiguration().squashDeclinedAttemptErrors()) {
return buildResponse(registrationServiceSession, verificationSession);
} else {
throw unwrappedException.getCause();
}
} else if (unwrappedException instanceof RegistrationServiceSenderException) { } else if (unwrappedException instanceof RegistrationServiceSenderException) {
throw unwrappedException; throw unwrappedException;
} else { } else {
logger.error("Registration service failure", unwrappedException); logger.error("Registration service failure", unwrappedException);
throw new ServerErrorException(Response.Status.INTERNAL_SERVER_ERROR); throw new ServerErrorException(Response.Status.INTERNAL_SERVER_ERROR);

View File

@ -0,0 +1,12 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.registration;
public class RegistrationFraudException extends Exception {
public RegistrationFraudException(final RegistrationServiceSenderException cause) {
super(null, cause, true, false);
}
}

View File

@ -146,6 +146,9 @@ public class RegistrationServiceClient implements Managed {
case SEND_VERIFICATION_CODE_ERROR_TYPE_SENDER_REJECTED -> throw new CompletionException( case SEND_VERIFICATION_CODE_ERROR_TYPE_SENDER_REJECTED -> throw new CompletionException(
RegistrationServiceSenderException.rejected(response.getError().getMayRetry())); RegistrationServiceSenderException.rejected(response.getError().getMayRetry()));
case SEND_VERIFICATION_CODE_ERROR_TYPE_SUSPECTED_FRAUD ->
throw new CompletionException(new RegistrationFraudException(
RegistrationServiceSenderException.rejected(response.getError().getMayRetry())));
case SEND_VERIFICATION_CODE_ERROR_TYPE_SENDER_ILLEGAL_ARGUMENT -> throw new CompletionException( case SEND_VERIFICATION_CODE_ERROR_TYPE_SENDER_ILLEGAL_ARGUMENT -> throw new CompletionException(
RegistrationServiceSenderException.illegalArgument(response.getError().getMayRetry())); RegistrationServiceSenderException.illegalArgument(response.getError().getMayRetry()));
case SEND_VERIFICATION_CODE_ERROR_TYPE_UNSPECIFIED -> throw new CompletionException( case SEND_VERIFICATION_CODE_ERROR_TYPE_UNSPECIFIED -> throw new CompletionException(

View File

@ -323,6 +323,12 @@ enum SendVerificationCodeErrorType {
* transport. * transport.
*/ */
SEND_VERIFICATION_CODE_ERROR_TYPE_TRANSPORT_NOT_ALLOWED = 6; SEND_VERIFICATION_CODE_ERROR_TYPE_TRANSPORT_NOT_ALLOWED = 6;
/**
* The sender declined to send the verification code due to suspected fraud
*/
SEND_VERIFICATION_CODE_ERROR_TYPE_SUSPECTED_FRAUD = 7;
} }
message CheckVerificationCodeRequest { message CheckVerificationCodeRequest {

View File

@ -56,6 +56,8 @@ import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.whispersystems.textsecuregcm.captcha.AssessmentResult; import org.whispersystems.textsecuregcm.captcha.AssessmentResult;
import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager; import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRegistrationConfiguration;
import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession; import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession;
import org.whispersystems.textsecuregcm.entities.VerificationSessionResponse; import org.whispersystems.textsecuregcm.entities.VerificationSessionResponse;
import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiter;
@ -65,6 +67,7 @@ import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptio
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper; import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper;
import org.whispersystems.textsecuregcm.push.PushNotificationManager; import org.whispersystems.textsecuregcm.push.PushNotificationManager;
import org.whispersystems.textsecuregcm.registration.RegistrationFraudException;
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
import org.whispersystems.textsecuregcm.registration.RegistrationServiceException; import org.whispersystems.textsecuregcm.registration.RegistrationServiceException;
import org.whispersystems.textsecuregcm.registration.RegistrationServiceSenderException; import org.whispersystems.textsecuregcm.registration.RegistrationServiceSenderException;
@ -73,6 +76,7 @@ import org.whispersystems.textsecuregcm.registration.VerificationSession;
import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider; import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
import org.whispersystems.textsecuregcm.storage.VerificationSessionManager; import org.whispersystems.textsecuregcm.storage.VerificationSessionManager;
import org.whispersystems.textsecuregcm.util.SystemMapper; import org.whispersystems.textsecuregcm.util.SystemMapper;
@ -97,6 +101,9 @@ class VerificationControllerTest {
private final RateLimiter captchaLimiter = mock(RateLimiter.class); private final RateLimiter captchaLimiter = mock(RateLimiter.class);
private final RateLimiter pushChallengeLimiter = mock(RateLimiter.class); private final RateLimiter pushChallengeLimiter = mock(RateLimiter.class);
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager = mock(
DynamicConfigurationManager.class);
private final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
private final ResourceExtension resources = ResourceExtension.builder() private final ResourceExtension resources = ResourceExtension.builder()
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
@ -110,7 +117,7 @@ class VerificationControllerTest {
.addResource( .addResource(
new VerificationController(registrationServiceClient, verificationSessionManager, pushNotificationManager, new VerificationController(registrationServiceClient, verificationSessionManager, pushNotificationManager,
registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters, accountsManager, true, registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters, accountsManager, true,
clock)) dynamicConfigurationManager, clock))
.build(); .build();
@BeforeEach @BeforeEach
@ -119,8 +126,12 @@ class VerificationControllerTest {
.thenReturn(captchaLimiter); .thenReturn(captchaLimiter);
when(rateLimiters.getVerificationPushChallengeLimiter()) when(rateLimiters.getVerificationPushChallengeLimiter())
.thenReturn(pushChallengeLimiter); .thenReturn(pushChallengeLimiter);
when(accountsManager.getByE164(any()))
when(accountsManager.getByE164(any())).thenReturn(Optional.empty()); .thenReturn(Optional.empty());
when(dynamicConfiguration.getRegistrationConfiguration())
.thenReturn(new DynamicRegistrationConfiguration(false));
when(dynamicConfigurationManager.getConfiguration())
.thenReturn(dynamicConfiguration);
} }
@ParameterizedTest @ParameterizedTest
@ -1089,6 +1100,44 @@ class VerificationControllerTest {
); );
} }
@ParameterizedTest
@ValueSource(booleans = {false, true})
void fraudError(boolean shadowFailure) {
if (shadowFailure) {
when(this.dynamicConfiguration.getRegistrationConfiguration())
.thenReturn(new DynamicRegistrationConfiguration(true));
}
final String encodedSessionId = encodeSessionId(SESSION_ID);
final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER,
false, null, null, 0L,
SESSION_EXPIRATION_SECONDS);
when(registrationServiceClient.getSession(any(), any()))
.thenReturn(CompletableFuture.completedFuture(Optional.of(registrationServiceSession)));
when(verificationSessionManager.findForId(any()))
.thenReturn(CompletableFuture.completedFuture(
Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), true,
clock.millis(), clock.millis(), registrationServiceSession.expiration()))));
when(registrationServiceClient.sendVerificationCode(any(), any(), any(), any(), any()))
.thenReturn(CompletableFuture.failedFuture(new CompletionException(
new RegistrationFraudException(RegistrationServiceSenderException.rejected(true)))));
final Invocation.Builder request = resources.getJerseyTest()
.target("/v1/verification/session/" + encodedSessionId + "/code")
.request()
.header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1");
try (Response response = request.post(Entity.json(requestVerificationCodeJson("voice", "ios")))) {
if (shadowFailure) {
assertEquals(200, response.getStatus());
} else {
assertEquals(RegistrationServiceSenderExceptionMapper.REMOTE_SERVICE_REJECTED_REQUEST_STATUS, response.getStatus());
final Map<String, Object> responseMap = response.readEntity(Map.class);
assertEquals("providerRejected", responseMap.get("reason"));
}
}
}
@Test @Test
void verifyCodeServerError() { void verifyCodeServerError() {
final String encodedSessionId = encodeSessionId(SESSION_ID); final String encodedSessionId = encodeSessionId(SESSION_ID);