mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 06:48:07 +01:00
Support configurable verification code sender overrides
This commit is contained in:
committed by
ravi-signal
parent
db4aa99ce0
commit
f5080f9bd6
@@ -172,6 +172,7 @@ import org.whispersystems.textsecuregcm.spam.PushChallengeConfigProvider;
|
||||
import org.whispersystems.textsecuregcm.spam.RateLimitChallengeListener;
|
||||
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
|
||||
import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider;
|
||||
import org.whispersystems.textsecuregcm.spam.SenderOverrideProvider;
|
||||
import org.whispersystems.textsecuregcm.spam.SpamFilter;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountLockManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||
@@ -886,8 +887,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment,
|
||||
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment) {
|
||||
List.of(
|
||||
ScoreThresholdProvider.ScoreThresholdFeature.class,
|
||||
PushChallengeConfigProvider.PushChallengeConfigFeature.class)
|
||||
ScoreThresholdProvider.ScoreThresholdFeature.class,
|
||||
SenderOverrideProvider.SenderOverrideFeature.class,
|
||||
PushChallengeConfigProvider.PushChallengeConfigFeature.class)
|
||||
.forEach(feature -> {
|
||||
environment.jersey().register(feature);
|
||||
webSocketEnvironment.jersey().register(feature);
|
||||
|
||||
@@ -82,6 +82,7 @@ import org.whispersystems.textsecuregcm.registration.VerificationSession;
|
||||
import org.whispersystems.textsecuregcm.spam.Extract;
|
||||
import org.whispersystems.textsecuregcm.spam.FilterSpam;
|
||||
import org.whispersystems.textsecuregcm.spam.ScoreThreshold;
|
||||
import org.whispersystems.textsecuregcm.spam.SenderOverride;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
||||
@@ -184,7 +185,7 @@ public class VerificationController {
|
||||
}
|
||||
|
||||
VerificationSession verificationSession = new VerificationSession(null, new ArrayList<>(),
|
||||
Collections.emptyList(), false,
|
||||
Collections.emptyList(), null, null, false,
|
||||
clock.millis(), clock.millis(), registrationServiceSession.expiration());
|
||||
|
||||
verificationSession = handlePushToken(pushTokenAndType, verificationSession);
|
||||
@@ -207,7 +208,8 @@ public class VerificationController {
|
||||
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
||||
@Context HttpServletRequest request,
|
||||
@NotNull @Valid final UpdateVerificationSessionRequest updateVerificationSessionRequest,
|
||||
@NotNull @Extract final ScoreThreshold captchaScoreThreshold) {
|
||||
@NotNull @Extract final ScoreThreshold scoreThreshold,
|
||||
@NotNull @Extract final SenderOverride senderOverride) {
|
||||
|
||||
final String sourceHost = useRemoteAddress
|
||||
? request.getRemoteAddr()
|
||||
@@ -221,13 +223,16 @@ public class VerificationController {
|
||||
|
||||
try {
|
||||
// these handle* methods ordered from least likely to fail to most, so take care when considering a change
|
||||
|
||||
verificationSession = handleSenderOverrides(verificationSession, senderOverride);
|
||||
|
||||
verificationSession = handlePushToken(pushTokenAndType, verificationSession);
|
||||
|
||||
verificationSession = handlePushChallenge(updateVerificationSessionRequest, registrationServiceSession,
|
||||
verificationSession);
|
||||
|
||||
verificationSession = handleCaptcha(sourceHost, updateVerificationSessionRequest, registrationServiceSession,
|
||||
verificationSession, userAgent, captchaScoreThreshold.getScoreThreshold());
|
||||
verificationSession, userAgent, scoreThreshold.getScoreThreshold());
|
||||
} catch (final RateLimitExceededException e) {
|
||||
|
||||
final Response response = buildResponseForRateLimitExceeded(verificationSession, registrationServiceSession,
|
||||
@@ -280,7 +285,8 @@ public class VerificationController {
|
||||
requestedInformation.addAll(verificationSession.requestedInformation());
|
||||
|
||||
verificationSession = new VerificationSession(generatePushChallenge(), requestedInformation,
|
||||
verificationSession.submittedInformation(), verificationSession.allowedToRequestCode(),
|
||||
verificationSession.submittedInformation(), verificationSession.smsSenderOverride(),
|
||||
verificationSession.voiceSenderOverride(), verificationSession.allowedToRequestCode(),
|
||||
verificationSession.createdTimestamp(), clock.millis(), verificationSession.remoteExpirationSeconds()
|
||||
);
|
||||
}
|
||||
@@ -348,7 +354,8 @@ public class VerificationController {
|
||||
&& requestedInformation.isEmpty();
|
||||
|
||||
verificationSession = new VerificationSession(verificationSession.pushChallenge(), requestedInformation,
|
||||
submittedInformation, allowedToRequestCode, verificationSession.createdTimestamp(), clock.millis(),
|
||||
submittedInformation, verificationSession.smsSenderOverride(), verificationSession.voiceSenderOverride(),
|
||||
allowedToRequestCode, verificationSession.createdTimestamp(), clock.millis(),
|
||||
verificationSession.remoteExpirationSeconds());
|
||||
|
||||
} else if (pushChallengePresent) {
|
||||
@@ -413,7 +420,8 @@ public class VerificationController {
|
||||
&& requestedInformation.isEmpty();
|
||||
|
||||
verificationSession = new VerificationSession(verificationSession.pushChallenge(), requestedInformation,
|
||||
submittedInformation, allowedToRequestCode, verificationSession.createdTimestamp(), clock.millis(),
|
||||
submittedInformation, verificationSession.smsSenderOverride(), verificationSession.voiceSenderOverride(),
|
||||
allowedToRequestCode, verificationSession.createdTimestamp(), clock.millis(),
|
||||
verificationSession.remoteExpirationSeconds());
|
||||
} else {
|
||||
throw new ForbiddenException();
|
||||
@@ -422,6 +430,31 @@ public class VerificationController {
|
||||
return verificationSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the verification session with explicit sender overrides if present. When the session is used to send
|
||||
* verification codes, these overrides will be used.
|
||||
*
|
||||
* @param verificationSession the session to update
|
||||
* @param senderOverride configured sender overrides
|
||||
* @return An updated {@link VerificationSession}
|
||||
*/
|
||||
private VerificationSession handleSenderOverrides(
|
||||
VerificationSession verificationSession,
|
||||
SenderOverride senderOverride) {
|
||||
return new VerificationSession(
|
||||
verificationSession.pushChallenge(),
|
||||
verificationSession.requestedInformation(),
|
||||
verificationSession.submittedInformation(),
|
||||
Optional.ofNullable(verificationSession.smsSenderOverride())
|
||||
.or(senderOverride::getSmsSenderOverride).orElse(null),
|
||||
Optional.ofNullable(verificationSession.voiceSenderOverride())
|
||||
.or(senderOverride::getVoiceSenderOverride)
|
||||
.orElse(null), verificationSession.allowedToRequestCode(),
|
||||
verificationSession.createdTimestamp(),
|
||||
clock.millis(),
|
||||
verificationSession.remoteExpirationSeconds());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/session/{sessionId}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@@ -476,12 +509,19 @@ public class VerificationController {
|
||||
}
|
||||
};
|
||||
|
||||
final String senderOverride = switch (messageTransport) {
|
||||
case SMS -> verificationSession.smsSenderOverride();
|
||||
case VOICE -> verificationSession.voiceSenderOverride();
|
||||
};
|
||||
|
||||
final RegistrationServiceSession resultSession;
|
||||
try {
|
||||
resultSession = registrationServiceClient.sendVerificationCode(registrationServiceSession.id(),
|
||||
messageTransport,
|
||||
clientType,
|
||||
acceptLanguage.orElse(null), REGISTRATION_RPC_TIMEOUT).join();
|
||||
acceptLanguage.orElse(null),
|
||||
senderOverride,
|
||||
REGISTRATION_RPC_TIMEOUT).join();
|
||||
} catch (final CancellationException e) {
|
||||
throw new ServerErrorException("registration service unavailable", Response.Status.SERVICE_UNAVAILABLE);
|
||||
} catch (final CompletionException e) {
|
||||
|
||||
@@ -114,6 +114,7 @@ public class RegistrationServiceClient implements Managed {
|
||||
final MessageTransport messageTransport,
|
||||
final ClientType clientType,
|
||||
@Nullable final String acceptLanguage,
|
||||
@Nullable final String senderOverride,
|
||||
final Duration timeout) {
|
||||
|
||||
final SendVerificationCodeRequest.Builder requestBuilder = SendVerificationCodeRequest.newBuilder()
|
||||
@@ -125,6 +126,10 @@ public class RegistrationServiceClient implements Managed {
|
||||
requestBuilder.setAcceptLanguage(acceptLanguage);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(senderOverride)) {
|
||||
requestBuilder.setSenderName(senderOverride);
|
||||
}
|
||||
|
||||
return toCompletableFuture(stub.withDeadline(toDeadline(timeout))
|
||||
.sendVerificationCode(requestBuilder.build()))
|
||||
.thenApply(response -> {
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.registration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
import org.whispersystems.textsecuregcm.storage.SerializedExpireableJsonDynamoStore;
|
||||
@@ -20,6 +21,10 @@ import org.whispersystems.textsecuregcm.storage.SerializedExpireableJsonDynamoSt
|
||||
* @param pushChallenge the value of a push challenge sent to a client, after it submitted a push token
|
||||
* @param requestedInformation information requested that a client send to the server
|
||||
* @param submittedInformation information that a client has submitted and that the server has verified
|
||||
* @param smsSenderOverride if present, indicates a sender override argument that should be forwarded to the
|
||||
* Registration Service when requesting a code
|
||||
* @param voiceSenderOverride if present, indicates a sender override argument that should be forwarded to the
|
||||
* Registration Service when requesting a code
|
||||
* @param allowedToRequestCode whether the client is allowed to request a code. This request will be forwarded to
|
||||
* Registration Service
|
||||
* @param createdTimestamp when this session was created
|
||||
@@ -29,11 +34,16 @@ import org.whispersystems.textsecuregcm.storage.SerializedExpireableJsonDynamoSt
|
||||
* @see org.whispersystems.textsecuregcm.entities.RegistrationServiceSession
|
||||
* @see org.whispersystems.textsecuregcm.entities.VerificationSessionResponse
|
||||
*/
|
||||
public record VerificationSession(@Nullable String pushChallenge,
|
||||
List<Information> requestedInformation, List<Information> submittedInformation,
|
||||
boolean allowedToRequestCode, long createdTimestamp, long updatedTimestamp,
|
||||
long remoteExpirationSeconds) implements
|
||||
SerializedExpireableJsonDynamoStore.Expireable {
|
||||
public record VerificationSession(
|
||||
@Nullable String pushChallenge,
|
||||
List<Information> requestedInformation,
|
||||
List<Information> submittedInformation,
|
||||
@Nullable String smsSenderOverride,
|
||||
@Nullable String voiceSenderOverride,
|
||||
boolean allowedToRequestCode,
|
||||
long createdTimestamp,
|
||||
long updatedTimestamp,
|
||||
long remoteExpirationSeconds) implements SerializedExpireableJsonDynamoStore.Expireable {
|
||||
|
||||
@Override
|
||||
public long getExpirationEpochSeconds() {
|
||||
|
||||
@@ -12,10 +12,11 @@ import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A ScoreThreshold may be provided by an upstream request filter. If request contains a property for
|
||||
* SCORE_THRESHOLD_PROPERTY_NAME it can be forwarded to a downstream filter to indicate it can use
|
||||
* a more or less strict score threshold when evaluating whether a request should be allowed to continue.
|
||||
* {@link #PROPERTY_NAME} it can be forwarded to a downstream filter to indicate it can use a more or less strict
|
||||
* score threshold when evaluating whether a request should be allowed to continue.
|
||||
*/
|
||||
public class ScoreThreshold {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ScoreThreshold.class);
|
||||
|
||||
public static final String PROPERTY_NAME = "scoreThreshold";
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.spam;
|
||||
|
||||
import java.util.function.Function;
|
||||
@@ -11,11 +15,11 @@ import org.glassfish.jersey.server.spi.internal.ValueParamProvider;
|
||||
|
||||
/**
|
||||
* Parses a {@link ScoreThreshold} out of a {@link ContainerRequest} to provide to jersey resources.
|
||||
*
|
||||
* <p>
|
||||
* A request filter may enrich a ContainerRequest with a scoreThreshold by providing a float property with the name
|
||||
* {@link ScoreThreshold#PROPERTY_NAME}. This indicates the desired scoreThreshold to use when evaluating whether a
|
||||
* request should proceed.
|
||||
*
|
||||
* <p>
|
||||
* A resource can consume a ScoreThreshold with by annotating a ScoreThreshold parameter with {@link Extract}
|
||||
*/
|
||||
public class ScoreThresholdProvider implements ValueParamProvider {
|
||||
@@ -24,6 +28,7 @@ public class ScoreThresholdProvider implements ValueParamProvider {
|
||||
* Configures the ScoreThresholdProvider
|
||||
*/
|
||||
public static class ScoreThresholdFeature implements Feature {
|
||||
|
||||
@Override
|
||||
public boolean configure(FeatureContext context) {
|
||||
context.register(new AbstractBinder() {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.spam;
|
||||
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A SenderOverride may be provided by an upstream request filter. If request contains a property for
|
||||
* {@link #SMS_SENDER_OVERRIDE_PROPERTY_NAME} or {@link #VOICE_SENDER_OVERRIDE_PROPERTY_NAME} it can be
|
||||
* forwarded to a downstream filter to indicate a specific sender should be used when sending verification codes.
|
||||
*/
|
||||
public class SenderOverride {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SenderOverride.class);
|
||||
public static final String SMS_SENDER_OVERRIDE_PROPERTY_NAME = "smsSenderOverride";
|
||||
public static final String VOICE_SENDER_OVERRIDE_PROPERTY_NAME = "voiceSenderOverride";
|
||||
|
||||
/**
|
||||
* The name of the sender to use to deliver a verification code via SMS
|
||||
*/
|
||||
private final Optional<String> smsSenderOverride;
|
||||
|
||||
/**
|
||||
* The name of the sender to use to deliver a verification code via voice
|
||||
*/
|
||||
private final Optional<String> voiceSenderOverride;
|
||||
|
||||
public SenderOverride(final ContainerRequest containerRequest) {
|
||||
this.smsSenderOverride = parse(String.class, SMS_SENDER_OVERRIDE_PROPERTY_NAME, containerRequest);
|
||||
this.voiceSenderOverride = parse(String.class, VOICE_SENDER_OVERRIDE_PROPERTY_NAME, containerRequest);
|
||||
}
|
||||
|
||||
private static <T> Optional<T> parse(Class<T> type, final String propertyName,
|
||||
final ContainerRequest containerRequest) {
|
||||
return Optional
|
||||
.ofNullable(containerRequest.getProperty(propertyName))
|
||||
.flatMap(obj -> {
|
||||
if (type.isInstance(obj)) {
|
||||
return Optional.of(type.cast(obj));
|
||||
}
|
||||
logger.warn("invalid format for filter provided property {}: {}", propertyName, obj);
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public Optional<String> getSmsSenderOverride() {
|
||||
return smsSenderOverride;
|
||||
}
|
||||
|
||||
public Optional<String> getVoiceSenderOverride() {
|
||||
return voiceSenderOverride;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.spam;
|
||||
|
||||
import java.util.function.Function;
|
||||
import javax.inject.Singleton;
|
||||
import javax.ws.rs.core.Feature;
|
||||
import javax.ws.rs.core.FeatureContext;
|
||||
import org.glassfish.jersey.internal.inject.AbstractBinder;
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.glassfish.jersey.server.model.Parameter;
|
||||
import org.glassfish.jersey.server.spi.internal.ValueParamProvider;
|
||||
|
||||
/**
|
||||
* Parses a {@link SenderOverride} out of a {@link ContainerRequest} to provide to jersey resources.
|
||||
* <p>
|
||||
* A request filter may enrich a ContainerRequest with senderOverrides by providing a string property names defined in
|
||||
* {@link SenderOverride}. This indicates the desired senderOverride to use when sending verification codes.
|
||||
* <p>
|
||||
* A resource can consume a SenderOverride with by annotating a SenderOverride parameter with {@link Extract}
|
||||
*/
|
||||
public class SenderOverrideProvider implements ValueParamProvider {
|
||||
|
||||
/**
|
||||
* Configures the SenderOverrideProvider
|
||||
*/
|
||||
public static class SenderOverrideFeature implements Feature {
|
||||
|
||||
@Override
|
||||
public boolean configure(FeatureContext context) {
|
||||
context.register(new AbstractBinder() {
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(SenderOverrideProvider.class)
|
||||
.to(ValueParamProvider.class)
|
||||
.in(Singleton.class);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<ContainerRequest, ?> getValueProvider(final Parameter parameter) {
|
||||
if (parameter.getRawType().equals(SenderOverride.class)
|
||||
&& parameter.isAnnotationPresent(Extract.class)) {
|
||||
return SenderOverride::new;
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public PriorityType getPriority() {
|
||||
return Priority.HIGH;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user