Add registration recovery checker
This commit is contained in:
parent
0b1ec1e50b
commit
7cbbf73cc9
|
@ -204,6 +204,7 @@ import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
||||||
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
|
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
|
||||||
import org.whispersystems.textsecuregcm.spam.ChallengeConstraintChecker;
|
import org.whispersystems.textsecuregcm.spam.ChallengeConstraintChecker;
|
||||||
import org.whispersystems.textsecuregcm.spam.RegistrationFraudChecker;
|
import org.whispersystems.textsecuregcm.spam.RegistrationFraudChecker;
|
||||||
|
import org.whispersystems.textsecuregcm.spam.RegistrationRecoveryChecker;
|
||||||
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
|
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
|
||||||
import org.whispersystems.textsecuregcm.spam.SpamChecker;
|
import org.whispersystems.textsecuregcm.spam.SpamChecker;
|
||||||
import org.whispersystems.textsecuregcm.spam.SpamFilter;
|
import org.whispersystems.textsecuregcm.spam.SpamFilter;
|
||||||
|
@ -675,8 +676,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
final RegistrationLockVerificationManager registrationLockVerificationManager = new RegistrationLockVerificationManager(
|
final RegistrationLockVerificationManager registrationLockVerificationManager = new RegistrationLockVerificationManager(
|
||||||
accountsManager, clientPresenceManager, svr2CredentialsGenerator, svr3CredentialsGenerator,
|
accountsManager, clientPresenceManager, svr2CredentialsGenerator, svr3CredentialsGenerator,
|
||||||
registrationRecoveryPasswordsManager, pushNotificationManager, rateLimiters);
|
registrationRecoveryPasswordsManager, pushNotificationManager, rateLimiters);
|
||||||
final PhoneVerificationTokenManager phoneVerificationTokenManager = new PhoneVerificationTokenManager(
|
|
||||||
registrationServiceClient, registrationRecoveryPasswordsManager);
|
|
||||||
|
|
||||||
final ReportedMessageMetricsListener reportedMessageMetricsListener = new ReportedMessageMetricsListener(
|
final ReportedMessageMetricsListener reportedMessageMetricsListener = new ReportedMessageMetricsListener(
|
||||||
accountsManager);
|
accountsManager);
|
||||||
|
@ -1059,6 +1058,12 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
log.warn("No registration-fraud-checkers found; using default (no-op) provider as a default");
|
log.warn("No registration-fraud-checkers found; using default (no-op) provider as a default");
|
||||||
return RegistrationFraudChecker.noop();
|
return RegistrationFraudChecker.noop();
|
||||||
});
|
});
|
||||||
|
final RegistrationRecoveryChecker registrationRecoveryChecker = spamFilter
|
||||||
|
.map(SpamFilter::getRegistrationRecoveryChecker)
|
||||||
|
.orElseGet(() -> {
|
||||||
|
log.warn("No registration-recovery-checkers found; using default (no-op) provider as a default");
|
||||||
|
return RegistrationRecoveryChecker.noop();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
spamFilter.map(SpamFilter::getReportedMessageListener).ifPresent(reportMessageManager::addListener);
|
spamFilter.map(SpamFilter::getReportedMessageListener).ifPresent(reportMessageManager::addListener);
|
||||||
|
@ -1072,6 +1077,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
final PhoneVerificationTokenManager phoneVerificationTokenManager = new PhoneVerificationTokenManager(
|
||||||
|
registrationServiceClient, registrationRecoveryPasswordsManager, registrationRecoveryChecker);
|
||||||
final List<Object> commonControllers = Lists.newArrayList(
|
final List<Object> commonControllers = Lists.newArrayList(
|
||||||
new AccountController(accountsManager, rateLimiters, turnTokenGenerator, registrationRecoveryPasswordsManager,
|
new AccountController(accountsManager, rateLimiters, turnTokenGenerator, registrationRecoveryPasswordsManager,
|
||||||
usernameHashZkProofVerifier),
|
usernameHashZkProofVerifier),
|
||||||
|
|
|
@ -17,12 +17,14 @@ import javax.ws.rs.BadRequestException;
|
||||||
import javax.ws.rs.ForbiddenException;
|
import javax.ws.rs.ForbiddenException;
|
||||||
import javax.ws.rs.NotAuthorizedException;
|
import javax.ws.rs.NotAuthorizedException;
|
||||||
import javax.ws.rs.ServerErrorException;
|
import javax.ws.rs.ServerErrorException;
|
||||||
|
import javax.ws.rs.container.ContainerRequestContext;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest;
|
import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession;
|
import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession;
|
||||||
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
|
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
|
||||||
|
import org.whispersystems.textsecuregcm.spam.RegistrationRecoveryChecker;
|
||||||
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
||||||
|
|
||||||
public class PhoneVerificationTokenManager {
|
public class PhoneVerificationTokenManager {
|
||||||
|
@ -33,17 +35,21 @@ public class PhoneVerificationTokenManager {
|
||||||
|
|
||||||
private final RegistrationServiceClient registrationServiceClient;
|
private final RegistrationServiceClient registrationServiceClient;
|
||||||
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
|
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
|
||||||
|
private final RegistrationRecoveryChecker registrationRecoveryChecker;
|
||||||
|
|
||||||
public PhoneVerificationTokenManager(final RegistrationServiceClient registrationServiceClient,
|
public PhoneVerificationTokenManager(final RegistrationServiceClient registrationServiceClient,
|
||||||
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager) {
|
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
|
||||||
|
final RegistrationRecoveryChecker registrationRecoveryChecker) {
|
||||||
this.registrationServiceClient = registrationServiceClient;
|
this.registrationServiceClient = registrationServiceClient;
|
||||||
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
|
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
|
||||||
|
this.registrationRecoveryChecker = registrationRecoveryChecker;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a {@link PhoneVerificationRequest} has a token that verifies the caller has confirmed access to the e164
|
* Checks if a {@link PhoneVerificationRequest} has a token that verifies the caller has confirmed access to the e164
|
||||||
* number
|
* number
|
||||||
*
|
*
|
||||||
|
* @param requestContext the container request context
|
||||||
* @param number the e164 presented for verification
|
* @param number the e164 presented for verification
|
||||||
* @param request the request with exactly one verification token (RegistrationService sessionId or registration
|
* @param request the request with exactly one verification token (RegistrationService sessionId or registration
|
||||||
* recovery password)
|
* recovery password)
|
||||||
|
@ -54,13 +60,13 @@ public class PhoneVerificationTokenManager {
|
||||||
* @throws ForbiddenException if the recovery password is not valid
|
* @throws ForbiddenException if the recovery password is not valid
|
||||||
* @throws InterruptedException if verification did not complete before a timeout
|
* @throws InterruptedException if verification did not complete before a timeout
|
||||||
*/
|
*/
|
||||||
public PhoneVerificationRequest.VerificationType verify(final String number, final PhoneVerificationRequest request)
|
public PhoneVerificationRequest.VerificationType verify(final ContainerRequestContext requestContext, final String number, final PhoneVerificationRequest request)
|
||||||
throws InterruptedException {
|
throws InterruptedException {
|
||||||
|
|
||||||
final PhoneVerificationRequest.VerificationType verificationType = request.verificationType();
|
final PhoneVerificationRequest.VerificationType verificationType = request.verificationType();
|
||||||
switch (verificationType) {
|
switch (verificationType) {
|
||||||
case SESSION -> verifyBySessionId(number, request.decodeSessionId());
|
case SESSION -> verifyBySessionId(number, request.decodeSessionId());
|
||||||
case RECOVERY_PASSWORD -> verifyByRecoveryPassword(number, request.recoveryPassword());
|
case RECOVERY_PASSWORD -> verifyByRecoveryPassword(requestContext, number, request.recoveryPassword());
|
||||||
}
|
}
|
||||||
|
|
||||||
return verificationType;
|
return verificationType;
|
||||||
|
@ -97,8 +103,11 @@ public class PhoneVerificationTokenManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void verifyByRecoveryPassword(final String number, final byte[] recoveryPassword)
|
private void verifyByRecoveryPassword(final ContainerRequestContext requestContext, final String number, final byte[] recoveryPassword)
|
||||||
throws InterruptedException {
|
throws InterruptedException {
|
||||||
|
if (!registrationRecoveryChecker.checkRegistrationRecoveryAttempt(requestContext, number)) {
|
||||||
|
throw new ForbiddenException("recoveryPassword couldn't be verified");
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
final boolean verified = registrationRecoveryPasswordsManager.verify(number, recoveryPassword)
|
final boolean verified = registrationRecoveryPasswordsManager.verify(number, recoveryPassword)
|
||||||
.get(VERIFICATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
.get(VERIFICATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
|
|
@ -32,6 +32,8 @@ import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.WebApplicationException;
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.container.ContainerRequestContext;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||||
|
@ -98,7 +100,8 @@ public class AccountControllerV2 {
|
||||||
name = "Retry-After",
|
name = "Retry-After",
|
||||||
description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed"))
|
description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed"))
|
||||||
public AccountIdentityResponse changeNumber(@Mutable @Auth final AuthenticatedDevice authenticatedDevice,
|
public AccountIdentityResponse changeNumber(@Mutable @Auth final AuthenticatedDevice authenticatedDevice,
|
||||||
@NotNull @Valid final ChangeNumberRequest request, @HeaderParam(HttpHeaders.USER_AGENT) final String userAgentString)
|
@NotNull @Valid final ChangeNumberRequest request, @HeaderParam(HttpHeaders.USER_AGENT) final String userAgentString,
|
||||||
|
@Context final ContainerRequestContext requestContext)
|
||||||
throws RateLimitExceededException, InterruptedException {
|
throws RateLimitExceededException, InterruptedException {
|
||||||
|
|
||||||
if (!authenticatedDevice.getAuthenticatedDevice().isPrimary()) {
|
if (!authenticatedDevice.getAuthenticatedDevice().isPrimary()) {
|
||||||
|
@ -116,8 +119,8 @@ public class AccountControllerV2 {
|
||||||
|
|
||||||
rateLimiters.getRegistrationLimiter().validate(number);
|
rateLimiters.getRegistrationLimiter().validate(number);
|
||||||
|
|
||||||
final PhoneVerificationRequest.VerificationType verificationType =
|
final PhoneVerificationRequest.VerificationType verificationType = phoneVerificationTokenManager.verify(
|
||||||
phoneVerificationTokenManager.verify(number, request);
|
requestContext, number, request);
|
||||||
|
|
||||||
final Optional<Account> existingAccount = accountsManager.getByE164(number);
|
final Optional<Account> existingAccount = accountsManager.getByE164(number);
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,8 @@ import javax.ws.rs.POST;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.WebApplicationException;
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.container.ContainerRequestContext;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader;
|
import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader;
|
||||||
|
@ -102,7 +104,8 @@ public class RegistrationController {
|
||||||
@HeaderParam(HttpHeaders.AUTHORIZATION) @NotNull final BasicAuthorizationHeader authorizationHeader,
|
@HeaderParam(HttpHeaders.AUTHORIZATION) @NotNull final BasicAuthorizationHeader authorizationHeader,
|
||||||
@HeaderParam(HeaderUtils.X_SIGNAL_AGENT) final String signalAgent,
|
@HeaderParam(HeaderUtils.X_SIGNAL_AGENT) final String signalAgent,
|
||||||
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
||||||
@NotNull @Valid final RegistrationRequest registrationRequest) throws RateLimitExceededException, InterruptedException {
|
@NotNull @Valid final RegistrationRequest registrationRequest,
|
||||||
|
@Context final ContainerRequestContext requestContext) throws RateLimitExceededException, InterruptedException {
|
||||||
|
|
||||||
final String number = authorizationHeader.getUsername();
|
final String number = authorizationHeader.getUsername();
|
||||||
final String password = authorizationHeader.getPassword();
|
final String password = authorizationHeader.getPassword();
|
||||||
|
@ -113,8 +116,8 @@ public class RegistrationController {
|
||||||
|
|
||||||
rateLimiters.getRegistrationLimiter().validate(number);
|
rateLimiters.getRegistrationLimiter().validate(number);
|
||||||
|
|
||||||
final PhoneVerificationRequest.VerificationType verificationType = phoneVerificationTokenManager.verify(number,
|
final PhoneVerificationRequest.VerificationType verificationType = phoneVerificationTokenManager.verify(
|
||||||
registrationRequest);
|
requestContext, number, registrationRequest);
|
||||||
|
|
||||||
final Optional<Account> existingAccount = accounts.getByE164(number);
|
final Optional<Account> existingAccount = accounts.getByE164(number);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package org.whispersystems.textsecuregcm.spam;
|
||||||
|
|
||||||
|
import javax.ws.rs.container.ContainerRequestContext;
|
||||||
|
|
||||||
|
public interface RegistrationRecoveryChecker {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a registration recovery attempt should be allowed or not
|
||||||
|
*
|
||||||
|
* @param requestContext The container request context for a registration recovery attempt
|
||||||
|
* @param e164 The E164 formatted phone number of the requester
|
||||||
|
* @return true if the registration recovery attempt is allowed, false otherwise.
|
||||||
|
*/
|
||||||
|
boolean checkRegistrationRecoveryAttempt(final ContainerRequestContext requestContext, final String e164);
|
||||||
|
|
||||||
|
static RegistrationRecoveryChecker noop() {
|
||||||
|
return (ignoredCtx, ignoredE164) -> true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -79,4 +79,12 @@ public interface SpamFilter extends Managed {
|
||||||
* @return a {@link ChallengeConstraintChecker} controlled by the spam filter
|
* @return a {@link ChallengeConstraintChecker} controlled by the spam filter
|
||||||
*/
|
*/
|
||||||
ChallengeConstraintChecker getChallengeConstraintChecker();
|
ChallengeConstraintChecker getChallengeConstraintChecker();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a checker that will be called to determine if a user is allowed to use their
|
||||||
|
* registration recovery password to re-register
|
||||||
|
*
|
||||||
|
* @return a {@link RegistrationRecoveryChecker} controlled by the spam filter
|
||||||
|
*/
|
||||||
|
RegistrationRecoveryChecker getRegistrationRecoveryChecker();
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,7 @@ import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMa
|
||||||
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
|
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
|
||||||
|
import org.whispersystems.textsecuregcm.spam.RegistrationRecoveryChecker;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
@ -111,6 +112,7 @@ class AccountControllerV2Test {
|
||||||
RegistrationLockVerificationManager.class);
|
RegistrationLockVerificationManager.class);
|
||||||
private final RateLimiters rateLimiters = mock(RateLimiters.class);
|
private final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||||
private final RateLimiter registrationLimiter = mock(RateLimiter.class);
|
private final RateLimiter registrationLimiter = mock(RateLimiter.class);
|
||||||
|
private final RegistrationRecoveryChecker registrationRecoveryChecker = mock(RegistrationRecoveryChecker.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)
|
||||||
|
@ -123,7 +125,8 @@ class AccountControllerV2Test {
|
||||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||||
.addResource(
|
.addResource(
|
||||||
new AccountControllerV2(accountsManager, changeNumberManager,
|
new AccountControllerV2(accountsManager, changeNumberManager,
|
||||||
new PhoneVerificationTokenManager(registrationServiceClient, registrationRecoveryPasswordsManager),
|
new PhoneVerificationTokenManager(registrationServiceClient, registrationRecoveryPasswordsManager,
|
||||||
|
registrationRecoveryChecker),
|
||||||
registrationLockVerificationManager, rateLimiters))
|
registrationLockVerificationManager, rateLimiters))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@ -382,6 +385,8 @@ class AccountControllerV2Test {
|
||||||
void recoveryPasswordManagerVerificationTrue() throws Exception {
|
void recoveryPasswordManagerVerificationTrue() throws Exception {
|
||||||
when(registrationRecoveryPasswordsManager.verify(any(), any()))
|
when(registrationRecoveryPasswordsManager.verify(any(), any()))
|
||||||
.thenReturn(CompletableFuture.completedFuture(true));
|
.thenReturn(CompletableFuture.completedFuture(true));
|
||||||
|
when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any()))
|
||||||
|
.thenReturn(true);
|
||||||
|
|
||||||
final Invocation.Builder request = resources.getJerseyTest()
|
final Invocation.Builder request = resources.getJerseyTest()
|
||||||
.target("/v2/accounts/number")
|
.target("/v2/accounts/number")
|
||||||
|
@ -418,6 +423,40 @@ class AccountControllerV2Test {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void registrationRecoveryCheckerAllowsAttempt() {
|
||||||
|
when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any())).thenReturn(true);
|
||||||
|
when(registrationRecoveryPasswordsManager.verify(any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(true));
|
||||||
|
|
||||||
|
final Invocation.Builder request = resources.getJerseyTest()
|
||||||
|
.target("/v2/accounts/number")
|
||||||
|
.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION,
|
||||||
|
AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD));
|
||||||
|
final byte[] recoveryPassword = new byte[32];
|
||||||
|
try (Response response = request.put(Entity.json(requestJsonRecoveryPassword(recoveryPassword, NEW_NUMBER)))) {
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void registrationRecoveryCheckerDisallowsAttempt() {
|
||||||
|
when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any())).thenReturn(false);
|
||||||
|
when(registrationRecoveryPasswordsManager.verify(any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(true));
|
||||||
|
|
||||||
|
final Invocation.Builder request = resources.getJerseyTest()
|
||||||
|
.target("/v2/accounts/number")
|
||||||
|
.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION,
|
||||||
|
AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD));
|
||||||
|
final byte[] recoveryPassword = new byte[32];
|
||||||
|
try (Response response = request.put(Entity.json(requestJsonRecoveryPassword(recoveryPassword, NEW_NUMBER)))) {
|
||||||
|
assertEquals(403, response.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valid request JSON with the given Recovery Password
|
* Valid request JSON with the given Recovery Password
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -71,6 +71,7 @@ import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMa
|
||||||
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
|
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
|
||||||
|
import org.whispersystems.textsecuregcm.spam.RegistrationRecoveryChecker;
|
||||||
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.DeviceSpec;
|
import org.whispersystems.textsecuregcm.storage.DeviceSpec;
|
||||||
|
@ -97,6 +98,7 @@ class RegistrationControllerTest {
|
||||||
RegistrationLockVerificationManager.class);
|
RegistrationLockVerificationManager.class);
|
||||||
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = mock(
|
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = mock(
|
||||||
RegistrationRecoveryPasswordsManager.class);
|
RegistrationRecoveryPasswordsManager.class);
|
||||||
|
private final RegistrationRecoveryChecker registrationRecoveryChecker = mock(RegistrationRecoveryChecker.class);
|
||||||
private final RateLimiters rateLimiters = mock(RateLimiters.class);
|
private final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||||
|
|
||||||
private final RateLimiter registrationLimiter = mock(RateLimiter.class);
|
private final RateLimiter registrationLimiter = mock(RateLimiter.class);
|
||||||
|
@ -110,7 +112,8 @@ class RegistrationControllerTest {
|
||||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||||
.addResource(
|
.addResource(
|
||||||
new RegistrationController(accountsManager,
|
new RegistrationController(accountsManager,
|
||||||
new PhoneVerificationTokenManager(registrationServiceClient, registrationRecoveryPasswordsManager),
|
new PhoneVerificationTokenManager(registrationServiceClient, registrationRecoveryPasswordsManager,
|
||||||
|
registrationRecoveryChecker),
|
||||||
registrationLockVerificationManager, rateLimiters))
|
registrationLockVerificationManager, rateLimiters))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@ -239,6 +242,7 @@ class RegistrationControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void recoveryPasswordManagerVerificationFailureOrTimeout() {
|
void recoveryPasswordManagerVerificationFailureOrTimeout() {
|
||||||
|
when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any())).thenReturn(true);
|
||||||
when(registrationRecoveryPasswordsManager.verify(any(), any()))
|
when(registrationRecoveryPasswordsManager.verify(any(), any()))
|
||||||
.thenReturn(CompletableFuture.failedFuture(new RuntimeException()));
|
.thenReturn(CompletableFuture.failedFuture(new RuntimeException()));
|
||||||
|
|
||||||
|
@ -284,6 +288,7 @@ class RegistrationControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void recoveryPasswordManagerVerificationTrue() throws InterruptedException {
|
void recoveryPasswordManagerVerificationTrue() throws InterruptedException {
|
||||||
|
when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any())).thenReturn(true);
|
||||||
when(registrationRecoveryPasswordsManager.verify(any(), any()))
|
when(registrationRecoveryPasswordsManager.verify(any(), any()))
|
||||||
.thenReturn(CompletableFuture.completedFuture(true));
|
.thenReturn(CompletableFuture.completedFuture(true));
|
||||||
|
|
||||||
|
@ -317,6 +322,50 @@ class RegistrationControllerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void registrationRecoveryCheckerAllowsAttempt() throws InterruptedException {
|
||||||
|
when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any())).thenReturn(true);
|
||||||
|
when(registrationRecoveryPasswordsManager.verify(any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(true));
|
||||||
|
|
||||||
|
final Account account = mock(Account.class);
|
||||||
|
when(account.getPrimaryDevice()).thenReturn(mock(Device.class));
|
||||||
|
|
||||||
|
when(accountsManager.create(any(), any(), any(), any(), any(), any()))
|
||||||
|
.thenReturn(account);
|
||||||
|
|
||||||
|
final Invocation.Builder request = resources.getJerseyTest()
|
||||||
|
.target("/v1/registration")
|
||||||
|
.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD));
|
||||||
|
final byte[] recoveryPassword = new byte[32];
|
||||||
|
try (Response response = request.post(Entity.json(requestJsonRecoveryPassword(recoveryPassword)))) {
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void registrationRecoveryCheckerDisallowsAttempt() throws InterruptedException {
|
||||||
|
when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any())).thenReturn(false);
|
||||||
|
when(registrationRecoveryPasswordsManager.verify(any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(true));
|
||||||
|
|
||||||
|
final Account account = mock(Account.class);
|
||||||
|
when(account.getPrimaryDevice()).thenReturn(mock(Device.class));
|
||||||
|
|
||||||
|
when(accountsManager.create(any(), any(), any(), any(), any(), any()))
|
||||||
|
.thenReturn(account);
|
||||||
|
|
||||||
|
final Invocation.Builder request = resources.getJerseyTest()
|
||||||
|
.target("/v1/registration")
|
||||||
|
.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD));
|
||||||
|
final byte[] recoveryPassword = new byte[32];
|
||||||
|
try (Response response = request.post(Entity.json(requestJsonRecoveryPassword(recoveryPassword)))) {
|
||||||
|
assertEquals(403, response.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@CartesianTest
|
@CartesianTest
|
||||||
@CartesianTest.MethodFactory("registrationLockAndDeviceTransfer")
|
@CartesianTest.MethodFactory("registrationLockAndDeviceTransfer")
|
||||||
void registrationLockAndDeviceTransfer(
|
void registrationLockAndDeviceTransfer(
|
||||||
|
|
Loading…
Reference in New Issue