Add registration recovery checker

This commit is contained in:
Ameya Lokare 2024-08-16 10:33:44 -07:00
parent 0b1ec1e50b
commit 7cbbf73cc9
8 changed files with 151 additions and 14 deletions

View File

@ -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),

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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();
} }

View File

@ -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
*/ */

View File

@ -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(