From 2be2b4ff23206cfaf151d2aecf2d758b8d6df1de Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Fri, 21 Apr 2023 10:40:46 -0400 Subject: [PATCH] Authenticate with the registration service using OIDC identity tokens in addition to shared API keys --- service/config/sample.yml | 6 ++ service/pom.xml | 5 ++ .../textsecuregcm/WhisperServerService.java | 10 ++- .../RegistrationServiceConfiguration.java | 50 ++--------- .../registration/ApiKeyCallCredentials.java | 32 ------- .../IdentityTokenCallCredentials.java | 83 +++++++++++++++++++ .../RegistrationServiceClient.java | 5 +- 7 files changed, 110 insertions(+), 81 deletions(-) delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/registration/ApiKeyCallCredentials.java create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/registration/IdentityTokenCallCredentials.java 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; }