From 8d0d0d61f1f68a9b306c0060fe20873125405421 Mon Sep 17 00:00:00 2001 From: ravi-signal <99042880+ravi-signal@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:13:04 -0600 Subject: [PATCH] Add reregistration flag to account creation response --- .../controllers/RegistrationController.java | 7 ++-- .../entities/AccountCreationResponse.java | 32 ++++++++++++++++++ .../RegistrationControllerTest.java | 33 +++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountCreationResponse.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java index 8ace819c9..fd4ec02fd 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java @@ -36,6 +36,7 @@ import java.util.Optional; import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader; import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager; import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager; +import org.whispersystems.textsecuregcm.entities.AccountCreationResponse; import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse; import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest; import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure; @@ -100,7 +101,7 @@ public class RegistrationController { @ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header( name = "Retry-After", description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed")) - public AccountIdentityResponse register( + public AccountCreationResponse register( @HeaderParam(HttpHeaders.AUTHORIZATION) @NotNull final BasicAuthorizationHeader authorizationHeader, @HeaderParam(HeaderUtils.X_SIGNAL_AGENT) final String signalAgent, @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, @@ -166,12 +167,14 @@ public class RegistrationController { Tag.of(VERIFICATION_TYPE_TAG_NAME, verificationType.name()))) .increment(); - return new AccountIdentityResponseBuilder(account) + final AccountIdentityResponse identityResponse = new AccountIdentityResponseBuilder(account) // If there was an existing account, return whether it could have had something in the storage service .storageCapable(existingAccount .map(a -> a.hasCapability(DeviceCapability.STORAGE)) .orElse(false)) .build(); + + return new AccountCreationResponse(identityResponse, existingAccount.isPresent()); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountCreationResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountCreationResponse.java new file mode 100644 index 000000000..8ad56962d --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountCreationResponse.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.swagger.v3.oas.annotations.media.Schema; + +// Note, this class cannot be converted into a record because @JsonUnwrapped does not work with records until 2.19. We are on 2.18 until Dropwizard’s BOM updates. +// https://github.com/FasterXML/jackson-databind/issues/1467 +public class AccountCreationResponse { + + @JsonUnwrapped + private AccountIdentityResponse identityResponse; + + @Schema(description = "If true, there was an existing account registered for this number") + private boolean reregistration; + + public AccountCreationResponse() { + } + + public AccountCreationResponse(AccountIdentityResponse identityResponse, boolean reregistration) { + this.identityResponse = identityResponse; + this.reregistration = reregistration; + } + + public boolean isReregistration() { + return reregistration; + } +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java index bb5abbd7b..c053c4b1e 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java @@ -50,6 +50,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.junitpioneer.jupiter.cartesian.ArgumentSets; import org.junitpioneer.jupiter.cartesian.CartesianTest; import org.signal.libsignal.protocol.IdentityKey; @@ -59,6 +60,8 @@ import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager; import org.whispersystems.textsecuregcm.auth.RegistrationLockError; import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager; import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.entities.AccountCreationResponse; +import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse; import org.whispersystems.textsecuregcm.entities.ApnRegistrationId; import org.whispersystems.textsecuregcm.entities.DeviceActivationRequest; import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; @@ -748,6 +751,8 @@ class RegistrationControllerTest { try (Response response = request.post(Entity.json(registrationRequest))) { assertEquals(200, response.getStatus()); + final AccountIdentityResponse identityResponse = response.readEntity(AccountIdentityResponse.class); + assertEquals(accountIdentifier, identityResponse.uuid()); } verify(accountsManager).create( @@ -760,6 +765,34 @@ class RegistrationControllerTest { any()); } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void reregistrationFlag(final boolean existingAccount) throws InterruptedException { + final RegistrationServiceSession registrationSession = + new RegistrationServiceSession(new byte[16], NUMBER, true, null, null, null, SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture(Optional.of(registrationSession))); + + final Optional maybeAccount = Optional.ofNullable(existingAccount ? mock(Account.class) : null); + when(accountsManager.getByE164(any())).thenReturn(maybeAccount); + + final Account account = mock(Account.class); + when(account.getPrimaryDevice()).thenReturn(mock(Device.class)); + + when(accountsManager.create(any(), 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)); + try (Response response = request.post(Entity.json(requestJson("sessionId")))) { + assertEquals(200, response.getStatus()); + final AccountCreationResponse creationResponse = response.readEntity(AccountCreationResponse.class); + assertEquals(existingAccount, creationResponse.isReregistration()); + } + } + private static boolean accountAttributesEqual(final AccountAttributes a, final AccountAttributes b) { return a.getFetchesMessages() == b.getFetchesMessages() && a.getRegistrationId() == b.getRegistrationId()