Authenticate with the registration service using OIDC identity tokens in addition to shared API keys

This commit is contained in:
Jon Chambers 2023-04-21 10:40:46 -04:00 committed by Jon Chambers
parent a83fd1d3fe
commit 2be2b4ff23
7 changed files with 110 additions and 81 deletions

View File

@ -370,7 +370,13 @@ oneTimeDonations:
registrationService: registrationService:
host: registration.example.com host: registration.example.com
port: 443
apiKey: EXAMPLE apiKey: EXAMPLE
credentialConfigurationJson: |
{
"example": "example"
}
identityTokenAudience: https://registration.example.com
registrationCaCertificate: | # Registration service TLS certificate trust root registrationCaCertificate: | # Registration service TLS certificate trust root
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz

View File

@ -451,6 +451,11 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.google.cloud</groupId> <groupId>com.google.cloud</groupId>
<artifactId>google-cloud-recaptchaenterprise</artifactId> <artifactId>google-cloud-recaptchaenterprise</artifactId>

View File

@ -472,9 +472,13 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
UsernameHashZkProofVerifier usernameHashZkProofVerifier = new UsernameHashZkProofVerifier(); UsernameHashZkProofVerifier usernameHashZkProofVerifier = new UsernameHashZkProofVerifier();
RegistrationServiceClient registrationServiceClient = new RegistrationServiceClient( RegistrationServiceClient registrationServiceClient = new RegistrationServiceClient(
config.getRegistrationServiceConfiguration().getHost(), config.getRegistrationServiceConfiguration().getPort(), config.getRegistrationServiceConfiguration().host(),
config.getRegistrationServiceConfiguration().getApiKey(), config.getRegistrationServiceConfiguration().port(),
config.getRegistrationServiceConfiguration().getRegistrationCaCertificate(), registrationCallbackExecutor); config.getRegistrationServiceConfiguration().apiKey(),
config.getRegistrationServiceConfiguration().credentialConfigurationJson(),
config.getRegistrationServiceConfiguration().identityTokenAudience(),
config.getRegistrationServiceConfiguration().registrationCaCertificate(),
registrationCallbackExecutor);
SecureBackupClient secureBackupClient = new SecureBackupClient(backupCredentialsGenerator, SecureBackupClient secureBackupClient = new SecureBackupClient(backupCredentialsGenerator,
secureValueRecoveryServiceExecutor, config.getSecureBackupServiceConfiguration()); secureValueRecoveryServiceExecutor, config.getSecureBackupServiceConfiguration());
SecureValueRecovery2Client secureValueRecovery2Client = new SecureValueRecovery2Client(svr2CredentialsGenerator, SecureValueRecovery2Client secureValueRecovery2Client = new SecureValueRecovery2Client(svr2CredentialsGenerator,

View File

@ -2,48 +2,10 @@ package org.whispersystems.textsecuregcm.configuration;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
public class RegistrationServiceConfiguration { public record RegistrationServiceConfiguration(@NotBlank String host,
int port,
@NotBlank @NotBlank String apiKey,
private String host; @NotBlank String credentialConfigurationJson,
@NotBlank String identityTokenAudience,
private int port = 443; @NotBlank String registrationCaCertificate) {
@NotBlank
private String apiKey;
@NotBlank
private String registrationCaCertificate;
public String getHost() {
return host;
}
public void setHost(final String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(final int port) {
this.port = port;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(final String apiKey) {
this.apiKey = apiKey;
}
public String getRegistrationCaCertificate() {
return registrationCaCertificate;
}
public void setRegistrationCaCertificate(final String registrationCaCertificate) {
this.registrationCaCertificate = registrationCaCertificate;
}
} }

View File

@ -1,32 +0,0 @@
package org.whispersystems.textsecuregcm.registration;
import io.grpc.CallCredentials;
import io.grpc.Metadata;
import java.util.concurrent.Executor;
class ApiKeyCallCredentials extends CallCredentials {
private final String apiKey;
private static final Metadata.Key<String> 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() {
}
}

View File

@ -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<String> 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<String> API_KEY_METADATA_KEY =
Metadata.Key.of("x-signal-api-key", Metadata.ASCII_STRING_MARSHALLER);
private static final Metadata.Key<String> 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<String> 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<String> 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() {
}
}

View File

@ -55,12 +55,13 @@ public class RegistrationServiceClient implements Managed {
} catch (final NumberParseException e) { } catch (final NumberParseException e) {
throw new IllegalArgumentException("could not parse to phone number", e); throw new IllegalArgumentException("could not parse to phone number", e);
} }
} }
public RegistrationServiceClient(final String host, public RegistrationServiceClient(final String host,
final int port, final int port,
final String apiKey, final String apiKey,
final String credentialConfigJson,
final String identityTokenAudience,
final String caCertificatePem, final String caCertificatePem,
final Executor callbackExecutor) throws IOException { final Executor callbackExecutor) throws IOException {
@ -73,7 +74,7 @@ public class RegistrationServiceClient implements Managed {
} }
this.stub = RegistrationServiceGrpc.newFutureStub(channel) this.stub = RegistrationServiceGrpc.newFutureStub(channel)
.withCallCredentials(new ApiKeyCallCredentials(apiKey)); .withCallCredentials(IdentityTokenCallCredentials.fromApiKeyAndCredentialConfig(apiKey, credentialConfigJson, identityTokenAudience));
this.callbackExecutor = callbackExecutor; this.callbackExecutor = callbackExecutor;
} }