diff --git a/service/config/sample.yml b/service/config/sample.yml
index e28e7d9e1..db4d877a7 100644
--- a/service/config/sample.yml
+++ b/service/config/sample.yml
@@ -370,7 +370,13 @@ oneTimeDonations:
registrationService:
host: registration.example.com
+ port: 443
apiKey: EXAMPLE
+ credentialConfigurationJson: |
+ {
+ "example": "example"
+ }
+ identityTokenAudience: https://registration.example.com
registrationCaCertificate: | # Registration service TLS certificate trust root
-----BEGIN CERTIFICATE-----
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
diff --git a/service/pom.xml b/service/pom.xml
index f9ac2f2b4..2aaaf516b 100644
--- a/service/pom.xml
+++ b/service/pom.xml
@@ -451,6 +451,11 @@
test
+
+ com.google.auth
+ google-auth-library-oauth2-http
+
+
com.google.cloud
google-cloud-recaptchaenterprise
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
index f9bb61b4d..9c6e3ade7 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
@@ -472,9 +472,13 @@ public class WhisperServerService extends Application API_KEY_METADATA_KEY =
- Metadata.Key.of("x-signal-api-key", Metadata.ASCII_STRING_MARSHALLER);
-
- ApiKeyCallCredentials(final String apiKey) {
- this.apiKey = apiKey;
- }
-
- @Override
- public void applyRequestMetadata(final RequestInfo requestInfo,
- final Executor appExecutor,
- final MetadataApplier applier) {
-
- final Metadata metadata = new Metadata();
- metadata.put(API_KEY_METADATA_KEY, apiKey);
-
- applier.apply(metadata);
- }
-
- @Override
- public void thisUsesUnstableApi() {
- }
-}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/registration/IdentityTokenCallCredentials.java b/service/src/main/java/org/whispersystems/textsecuregcm/registration/IdentityTokenCallCredentials.java
new file mode 100644
index 000000000..cb8ecc9d5
--- /dev/null
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/registration/IdentityTokenCallCredentials.java
@@ -0,0 +1,83 @@
+package org.whispersystems.textsecuregcm.registration;
+
+import com.google.auth.oauth2.ExternalAccountCredentials;
+import com.google.auth.oauth2.ImpersonatedCredentials;
+import com.google.common.base.Suppliers;
+import io.grpc.CallCredentials;
+import io.grpc.Metadata;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import javax.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class IdentityTokenCallCredentials extends CallCredentials {
+
+ private final String apiKey;
+ private final Supplier identityTokenSupplier;
+
+ private static final Duration IDENTITY_TOKEN_LIFETIME = Duration.ofHours(1);
+ private static final Duration IDENTITY_TOKEN_REFRESH_BUFFER = Duration.ofMinutes(10);
+
+ private static final Metadata.Key API_KEY_METADATA_KEY =
+ Metadata.Key.of("x-signal-api-key", Metadata.ASCII_STRING_MARSHALLER);
+ private static final Metadata.Key AUTHORIZATION_METADATA_KEY =
+ Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER);
+
+ private static final Logger logger = LoggerFactory.getLogger(IdentityTokenCallCredentials.class);
+
+ IdentityTokenCallCredentials(final String apiKey, final Supplier identityTokenSupplier) {
+ this.apiKey = apiKey;
+ this.identityTokenSupplier = identityTokenSupplier;
+ }
+
+ static IdentityTokenCallCredentials fromApiKeyAndCredentialConfig(final String apiKey, final String credentialConfigJson, final String audience) throws IOException {
+ try (final InputStream configInputStream = new ByteArrayInputStream(credentialConfigJson.getBytes(StandardCharsets.UTF_8))) {
+ final ExternalAccountCredentials credentials = ExternalAccountCredentials.fromStream(configInputStream);
+ final ImpersonatedCredentials impersonatedCredentials = ImpersonatedCredentials.create(credentials,
+ credentials.getServiceAccountEmail(), null, List.of(), (int) IDENTITY_TOKEN_LIFETIME.toSeconds());
+
+ final Supplier idTokenSupplier = Suppliers.memoizeWithExpiration(() -> {
+ try {
+ impersonatedCredentials.getSourceCredentials().refresh();
+ return impersonatedCredentials.idTokenWithAudience(audience, null).getTokenValue();
+ } catch (final IOException e) {
+ logger.warn("Failed to retrieve identity token", e);
+ throw new UncheckedIOException(e);
+ }
+ },
+ IDENTITY_TOKEN_LIFETIME.minus(IDENTITY_TOKEN_REFRESH_BUFFER).toMillis(),
+ TimeUnit.MILLISECONDS);
+
+ return new IdentityTokenCallCredentials(apiKey, idTokenSupplier);
+ }
+ }
+
+ @Override
+ public void applyRequestMetadata(final RequestInfo requestInfo,
+ final Executor appExecutor,
+ final MetadataApplier applier) {
+
+ @Nullable final String identityTokenValue = identityTokenSupplier.get();
+
+ if (identityTokenValue != null) {
+ final Metadata metadata = new Metadata();
+ metadata.put(API_KEY_METADATA_KEY, apiKey);
+ metadata.put(AUTHORIZATION_METADATA_KEY, "Bearer " + identityTokenValue);
+
+ applier.apply(metadata);
+ }
+ }
+
+ @Override
+ public void thisUsesUnstableApi() {
+ }
+}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceClient.java b/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceClient.java
index 89284f500..68d9e8050 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceClient.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceClient.java
@@ -55,12 +55,13 @@ public class RegistrationServiceClient implements Managed {
} catch (final NumberParseException e) {
throw new IllegalArgumentException("could not parse to phone number", e);
}
-
}
public RegistrationServiceClient(final String host,
final int port,
final String apiKey,
+ final String credentialConfigJson,
+ final String identityTokenAudience,
final String caCertificatePem,
final Executor callbackExecutor) throws IOException {
@@ -73,7 +74,7 @@ public class RegistrationServiceClient implements Managed {
}
this.stub = RegistrationServiceGrpc.newFutureStub(channel)
- .withCallCredentials(new ApiKeyCallCredentials(apiKey));
+ .withCallCredentials(IdentityTokenCallCredentials.fromApiKeyAndCredentialConfig(apiKey, credentialConfigJson, identityTokenAudience));
this.callbackExecutor = callbackExecutor;
}