Parameterize sitekey
This commit is contained in:
parent
3a1c716c73
commit
935e268dec
|
@ -246,7 +246,6 @@ recaptcha:
|
||||||
secret: unset
|
secret: unset
|
||||||
|
|
||||||
recaptchaV2:
|
recaptchaV2:
|
||||||
siteKey: unset
|
|
||||||
scoreFloor: 1.0
|
scoreFloor: 1.0
|
||||||
projectPath: projects/example
|
projectPath: projects/example
|
||||||
credentialConfigurationJson: "{ }" # service account configuration for backend authentication
|
credentialConfigurationJson: "{ }" # service account configuration for backend authentication
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2013-2021 Signal Messenger, LLC
|
* Copyright 2013-2022 Signal Messenger, LLC
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm;
|
package org.whispersystems.textsecuregcm;
|
||||||
|
@ -477,7 +477,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
LegacyRecaptchaClient legacyRecaptchaClient = new LegacyRecaptchaClient(config.getRecaptchaConfiguration().getSecret());
|
LegacyRecaptchaClient legacyRecaptchaClient = new LegacyRecaptchaClient(config.getRecaptchaConfiguration().getSecret());
|
||||||
EnterpriseRecaptchaClient enterpriseRecaptchaClient = new EnterpriseRecaptchaClient(
|
EnterpriseRecaptchaClient enterpriseRecaptchaClient = new EnterpriseRecaptchaClient(
|
||||||
config.getRecaptchaV2Configuration().getScoreFloor().doubleValue(),
|
config.getRecaptchaV2Configuration().getScoreFloor().doubleValue(),
|
||||||
config.getRecaptchaV2Configuration().getSiteKey(),
|
|
||||||
config.getRecaptchaV2Configuration().getProjectPath(),
|
config.getRecaptchaV2Configuration().getProjectPath(),
|
||||||
config.getRecaptchaV2Configuration().getCredentialConfigurationJson());
|
config.getRecaptchaV2Configuration().getCredentialConfigurationJson());
|
||||||
TransitionalRecaptchaClient transitionalRecaptchaClient = new TransitionalRecaptchaClient(legacyRecaptchaClient, enterpriseRecaptchaClient);
|
TransitionalRecaptchaClient transitionalRecaptchaClient = new TransitionalRecaptchaClient(legacyRecaptchaClient, enterpriseRecaptchaClient);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2021 Signal Messenger, LLC
|
* Copyright 2021-2022 Signal Messenger, LLC
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ public class RecaptchaV2Configuration {
|
||||||
|
|
||||||
private BigDecimal scoreFloor;
|
private BigDecimal scoreFloor;
|
||||||
private String projectPath;
|
private String projectPath;
|
||||||
private String siteKey;
|
|
||||||
private String credentialConfigurationJson;
|
private String credentialConfigurationJson;
|
||||||
|
|
||||||
@DecimalMin("0")
|
@DecimalMin("0")
|
||||||
|
@ -30,11 +29,6 @@ public class RecaptchaV2Configuration {
|
||||||
return projectPath;
|
return projectPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotEmpty
|
|
||||||
public String getSiteKey() {
|
|
||||||
return siteKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotEmpty
|
@NotEmpty
|
||||||
public String getCredentialConfigurationJson() {
|
public String getCredentialConfigurationJson() {
|
||||||
return credentialConfigurationJson;
|
return credentialConfigurationJson;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2021 Signal Messenger, LLC
|
* Copyright 2021-2022 Signal Messenger, LLC
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -9,33 +9,34 @@ import com.google.api.gax.core.FixedCredentialsProvider;
|
||||||
import com.google.auth.oauth2.GoogleCredentials;
|
import com.google.auth.oauth2.GoogleCredentials;
|
||||||
import com.google.cloud.recaptchaenterprise.v1.RecaptchaEnterpriseServiceClient;
|
import com.google.cloud.recaptchaenterprise.v1.RecaptchaEnterpriseServiceClient;
|
||||||
import com.google.cloud.recaptchaenterprise.v1.RecaptchaEnterpriseServiceSettings;
|
import com.google.cloud.recaptchaenterprise.v1.RecaptchaEnterpriseServiceSettings;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.recaptchaenterprise.v1.Assessment;
|
import com.google.recaptchaenterprise.v1.Assessment;
|
||||||
import com.google.recaptchaenterprise.v1.Event;
|
import com.google.recaptchaenterprise.v1.Event;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.ws.rs.BadRequestException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class EnterpriseRecaptchaClient implements RecaptchaClient {
|
public class EnterpriseRecaptchaClient implements RecaptchaClient {
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String SEPARATOR = ".";
|
||||||
private static final Logger logger = LoggerFactory.getLogger(EnterpriseRecaptchaClient.class);
|
private static final Logger logger = LoggerFactory.getLogger(EnterpriseRecaptchaClient.class);
|
||||||
|
|
||||||
private final double scoreFloor;
|
private final double scoreFloor;
|
||||||
private final String siteKey;
|
|
||||||
private final String projectPath;
|
private final String projectPath;
|
||||||
private final RecaptchaEnterpriseServiceClient client;
|
private final RecaptchaEnterpriseServiceClient client;
|
||||||
|
|
||||||
public EnterpriseRecaptchaClient(
|
public EnterpriseRecaptchaClient(
|
||||||
final double scoreFloor,
|
final double scoreFloor,
|
||||||
@Nonnull final String siteKey,
|
|
||||||
@Nonnull final String projectPath,
|
@Nonnull final String projectPath,
|
||||||
@Nonnull final String recaptchaCredentialConfigurationJson) {
|
@Nonnull final String recaptchaCredentialConfigurationJson) {
|
||||||
try {
|
try {
|
||||||
this.scoreFloor = scoreFloor;
|
this.scoreFloor = scoreFloor;
|
||||||
this.siteKey = Objects.requireNonNull(siteKey);
|
|
||||||
this.projectPath = Objects.requireNonNull(projectPath);
|
this.projectPath = Objects.requireNonNull(projectPath);
|
||||||
this.client = RecaptchaEnterpriseServiceClient.create(RecaptchaEnterpriseServiceSettings.newBuilder()
|
this.client = RecaptchaEnterpriseServiceClient.create(RecaptchaEnterpriseServiceSettings.newBuilder()
|
||||||
.setCredentialsProvider(FixedCredentialsProvider.create(GoogleCredentials.fromStream(
|
.setCredentialsProvider(FixedCredentialsProvider.create(GoogleCredentials.fromStream(
|
||||||
|
@ -46,14 +47,38 @@ public class EnterpriseRecaptchaClient implements RecaptchaClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public boolean verify(final String token, final String ip) {
|
* Parses the token and action (if any) from {@code input}. The expected input format is: {@code [action:]token}.
|
||||||
return verify(token, ip, null);
|
* <p>
|
||||||
|
* For action to be optional, there is a strong assumption that the token will never contain a {@value SEPARATOR}.
|
||||||
|
* Observation suggests {@code token} is base-64 encoded. In practice, an action should always be present, but we
|
||||||
|
* don’t need to be strict.
|
||||||
|
*/
|
||||||
|
static String[] parseInputToken(final String input) {
|
||||||
|
String[] keyActionAndToken = input.split("\\" + SEPARATOR, 3);
|
||||||
|
|
||||||
|
if (keyActionAndToken.length == 1) {
|
||||||
|
throw new BadRequestException("too few parts");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyActionAndToken.length == 2) {
|
||||||
|
// there was no ":" delimiter; assume we only have a token
|
||||||
|
return new String[]{keyActionAndToken[0], null, keyActionAndToken[1]};
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyActionAndToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean verify(final String token, final String ip, @Nullable final String expectedAction) {
|
@Override
|
||||||
|
public boolean verify(final String input, final String ip) {
|
||||||
|
final String[] parts = parseInputToken(input);
|
||||||
|
|
||||||
|
final String sitekey = parts[0];
|
||||||
|
final String expectedAction = parts[1];
|
||||||
|
final String token = parts[2];
|
||||||
|
|
||||||
Event.Builder eventBuilder = Event.newBuilder()
|
Event.Builder eventBuilder = Event.newBuilder()
|
||||||
.setSiteKey(siteKey)
|
.setSiteKey(sitekey)
|
||||||
.setToken(token)
|
.setToken(token)
|
||||||
.setUserIpAddress(ip);
|
.setUserIpAddress(ip);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2021 Signal Messenger, LLC
|
* Copyright 2021-2022 Signal Messenger, LLC
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -12,9 +12,7 @@ import javax.annotation.Nonnull;
|
||||||
public class TransitionalRecaptchaClient implements RecaptchaClient {
|
public class TransitionalRecaptchaClient implements RecaptchaClient {
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final String SEPARATOR = ".";
|
static final String V2_PREFIX = "signal-recaptcha-v2" + EnterpriseRecaptchaClient.SEPARATOR;
|
||||||
@VisibleForTesting
|
|
||||||
static final String V2_PREFIX = "signal-recaptcha-v2" + SEPARATOR;
|
|
||||||
|
|
||||||
private final LegacyRecaptchaClient legacyRecaptchaClient;
|
private final LegacyRecaptchaClient legacyRecaptchaClient;
|
||||||
private final EnterpriseRecaptchaClient enterpriseRecaptchaClient;
|
private final EnterpriseRecaptchaClient enterpriseRecaptchaClient;
|
||||||
|
@ -29,28 +27,10 @@ public class TransitionalRecaptchaClient implements RecaptchaClient {
|
||||||
@Override
|
@Override
|
||||||
public boolean verify(@Nonnull final String token, @Nonnull final String ip) {
|
public boolean verify(@Nonnull final String token, @Nonnull final String ip) {
|
||||||
if (token.startsWith(V2_PREFIX)) {
|
if (token.startsWith(V2_PREFIX)) {
|
||||||
final String[] actionAndToken = parseV2ActionAndToken(token.substring(V2_PREFIX.length()));
|
return enterpriseRecaptchaClient.verify(token.substring(V2_PREFIX.length()), ip);
|
||||||
return enterpriseRecaptchaClient.verify(actionAndToken[1], ip, actionAndToken[0]);
|
|
||||||
} else {
|
} else {
|
||||||
return legacyRecaptchaClient.verify(token, ip);
|
return legacyRecaptchaClient.verify(token, ip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the token and action (if any) from {@code input}. The expected input format is: {@code [action:]token}.
|
|
||||||
* <p>
|
|
||||||
* For action to be optional, there is a strong assumption that the token will never contain a {@value SEPARATOR}.
|
|
||||||
* Observation suggests {@code token} is base-64 encoded. In practice, an action should always be present, but we
|
|
||||||
* don’t need to be strict.
|
|
||||||
*/
|
|
||||||
static String[] parseV2ActionAndToken(final String input) {
|
|
||||||
String[] actionAndToken = input.split("\\" + SEPARATOR, 2);
|
|
||||||
|
|
||||||
if (actionAndToken.length == 1) {
|
|
||||||
// there was no ":" delimiter; assume we only have a token
|
|
||||||
return new String[]{null, actionAndToken[0]};
|
|
||||||
}
|
|
||||||
|
|
||||||
return actionAndToken;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.recaptcha;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.whispersystems.textsecuregcm.recaptcha.EnterpriseRecaptchaClient.SEPARATOR;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.ws.rs.BadRequestException;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
|
||||||
|
class EnterpriseRecaptchaClientTest {
|
||||||
|
|
||||||
|
private static final String SITE_KEY = "site-key";
|
||||||
|
private static final String TOKEN = "some-token";
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource
|
||||||
|
void parseInputToken(final String input, final String expectedToken, final String siteKey,
|
||||||
|
@Nullable final String expectedAction) {
|
||||||
|
|
||||||
|
final String[] parts = EnterpriseRecaptchaClient.parseInputToken(input);
|
||||||
|
|
||||||
|
assertEquals(siteKey, parts[0]);
|
||||||
|
assertEquals(expectedAction, parts[1]);
|
||||||
|
assertEquals(expectedToken, parts[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseInputTokenBadRequest() {
|
||||||
|
assertThrows(BadRequestException.class, () -> {
|
||||||
|
EnterpriseRecaptchaClient.parseInputToken(TOKEN);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static Stream<Arguments> parseInputToken() {
|
||||||
|
return Stream.of(
|
||||||
|
Arguments.of(
|
||||||
|
String.join(SEPARATOR, SITE_KEY, TOKEN),
|
||||||
|
TOKEN,
|
||||||
|
SITE_KEY,
|
||||||
|
null),
|
||||||
|
Arguments.of(
|
||||||
|
String.join(SEPARATOR, SITE_KEY, "an-action", TOKEN),
|
||||||
|
TOKEN,
|
||||||
|
SITE_KEY,
|
||||||
|
"an-action"),
|
||||||
|
Arguments.of(
|
||||||
|
String.join(SEPARATOR, SITE_KEY, "an-action", TOKEN, "something-else"),
|
||||||
|
TOKEN + SEPARATOR + "something-else",
|
||||||
|
SITE_KEY,
|
||||||
|
"an-action")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ package org.whispersystems.textsecuregcm.recaptcha;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.verifyNoInteractions;
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
import static org.whispersystems.textsecuregcm.recaptcha.TransitionalRecaptchaClient.SEPARATOR;
|
import static org.whispersystems.textsecuregcm.recaptcha.EnterpriseRecaptchaClient.SEPARATOR;
|
||||||
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
@ -36,8 +36,7 @@ class TransitionalRecaptchaClientTest {
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource
|
@MethodSource
|
||||||
void testVerify(final String inputToken, final boolean expectLegacy, final String expectedToken,
|
void testVerify(final String inputToken, final boolean expectLegacy, final String expectedToken) {
|
||||||
final String expectedAction) {
|
|
||||||
|
|
||||||
transitionalRecaptchaClient.verify(inputToken, IP_ADDRESS);
|
transitionalRecaptchaClient.verify(inputToken, IP_ADDRESS);
|
||||||
|
|
||||||
|
@ -46,7 +45,7 @@ class TransitionalRecaptchaClientTest {
|
||||||
verify(legacyRecaptchaClient).verify(expectedToken, IP_ADDRESS);
|
verify(legacyRecaptchaClient).verify(expectedToken, IP_ADDRESS);
|
||||||
} else {
|
} else {
|
||||||
verifyNoInteractions(legacyRecaptchaClient);
|
verifyNoInteractions(legacyRecaptchaClient);
|
||||||
verify(enterpriseRecaptchaClient).verify(expectedToken, IP_ADDRESS, expectedAction);
|
verify(enterpriseRecaptchaClient).verify(expectedToken, IP_ADDRESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -56,23 +55,20 @@ class TransitionalRecaptchaClientTest {
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
TOKEN,
|
TOKEN,
|
||||||
true,
|
true,
|
||||||
TOKEN,
|
TOKEN),
|
||||||
null),
|
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
String.join(SEPARATOR, PREFIX, TOKEN),
|
String.join(SEPARATOR, PREFIX, TOKEN),
|
||||||
false,
|
false,
|
||||||
TOKEN,
|
TOKEN),
|
||||||
null),
|
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
String.join(SEPARATOR, PREFIX, "an-action", TOKEN),
|
String.join(SEPARATOR, PREFIX, "site-key", "an-action", TOKEN),
|
||||||
false,
|
false,
|
||||||
TOKEN,
|
String.join(SEPARATOR, "site-key", "an-action", TOKEN),
|
||||||
"an-action"),
|
"an-action"),
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
String.join(SEPARATOR, PREFIX, "an-action", TOKEN, "something-else"),
|
String.join(SEPARATOR, PREFIX, "site-key", "an-action", TOKEN, "something-else"),
|
||||||
false,
|
false,
|
||||||
TOKEN + SEPARATOR + "something-else",
|
"site-key" + SEPARATOR + "an-action" + SEPARATOR + TOKEN + SEPARATOR + "something-else")
|
||||||
"an-action")
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue