emptyList())
@@ -119,7 +129,8 @@ public static Builder newBuilder() {
.setStreamWatchdog(null)
.setStreamWatchdogCheckInterval(Duration.ZERO)
.setTracerFactory(BaseApiTracerFactory.getInstance())
- .setQuotaProjectId(null);
+ .setQuotaProjectId(null)
+ .setGdchApiAudience(null);
}
public abstract Builder toBuilder();
@@ -167,6 +178,30 @@ public static ClientContext create(StubSettings settings) throws IOException {
Credentials credentials = settings.getCredentialsProvider().getCredentials();
+ String settingsGdchApiAudience = settings.getGdchApiAudience();
+ if (credentials instanceof GdchCredentials) {
+ // We recompute the GdchCredentials with the audience
+ String audienceString;
+ if (!Strings.isNullOrEmpty(settingsGdchApiAudience)) {
+ audienceString = settingsGdchApiAudience;
+ } else if (!Strings.isNullOrEmpty(settings.getEndpoint())) {
+ audienceString = settings.getEndpoint();
+ } else {
+ throw new IllegalArgumentException("Could not infer GDCH api audience from settings");
+ }
+
+ URI gdchAudienceUri;
+ try {
+ gdchAudienceUri = URI.create(audienceString);
+ } catch (IllegalArgumentException ex) { // thrown when passing a malformed uri string
+ throw new IllegalArgumentException("The GDC-H API audience string is not a valid URI", ex);
+ }
+ credentials = ((GdchCredentials) credentials).createWithGdchAudience(gdchAudienceUri);
+ } else if (!Strings.isNullOrEmpty(settingsGdchApiAudience)) {
+ throw new IllegalArgumentException(
+ "GDC-H API audience can only be set when using GdchCredentials");
+ }
+
if (settings.getQuotaProjectId() != null && credentials != null) {
// If the quotaProjectId is set, wrap original credentials with correct quotaProjectId as
// QuotaProjectIdHidingCredentials.
@@ -325,6 +360,17 @@ public abstract static class Builder {
@BetaApi("The surface for tracing is not stable yet and may change in the future.")
public abstract Builder setTracerFactory(ApiTracerFactory tracerFactory);
+ /**
+ * Sets the API audience used by {@link com.google.auth.oauth2.GdchCredentials} It cannot be
+ * used if other type of {@link com.google.auth.Credentials} is used
+ *
+ * If the provided credentials already contain an api audience, it will be overriden by this
+ * one
+ *
+ * @param gdchApiAudience the audience to be used - must be a valid URI string
+ */
+ public abstract Builder setGdchApiAudience(String gdchApiAudience);
+
public abstract ClientContext build();
}
}
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java
index a1097a4100..04b2c9f55b 100644
--- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java
+++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java
@@ -111,6 +111,11 @@ public final Duration getWatchdogCheckInterval() {
return stubSettings.getStreamWatchdogCheckInterval();
}
+ /** Gets the GDCH API audience that was previously set in this Builder */
+ public final String getGdchApiAudience() {
+ return stubSettings.getGdchApiAudience();
+ }
+
public String toString() {
return MoreObjects.toStringHelper(this)
.add("executorProvider", getExecutorProvider())
@@ -124,6 +129,7 @@ public String toString() {
.add("quotaProjectId", getQuotaProjectId())
.add("watchdogProvider", getWatchdogProvider())
.add("watchdogCheckInterval", getWatchdogCheckInterval())
+ .add("gdchApiAudience", getGdchApiAudience())
.toString();
}
@@ -255,6 +261,18 @@ public B setWatchdogCheckInterval(@Nullable Duration checkInterval) {
return self();
}
+ /**
+ * Sets the GDC-H api audience. This is intended only to be used with {@link
+ * com.google.auth.oauth2.GdchCredentials} If this field is set and other type of {@link
+ * com.google.auth.Credentials} is used then an {@link IllegalArgumentException} will be thrown.
+ * If the provided credentials already have an api audience, then it will be overriden by this
+ * audience
+ */
+ public B setGdchApiAudience(@Nullable String gdchApiAudience) {
+ stubSettings.setGdchApiAudience(gdchApiAudience);
+ return self();
+ }
+
/**
* Gets the ExecutorProvider that was previously set on this Builder. This ExecutorProvider is
* to use for running asynchronous API call logic (such as retries and long-running operations),
@@ -322,6 +340,12 @@ public Duration getWatchdogCheckInterval() {
return stubSettings.getStreamWatchdogCheckInterval();
}
+ /** Gets the GDCH API audience that was previously set in this Builder */
+ @Nullable
+ public String getGdchApiAudience() {
+ return stubSettings.getGdchApiAudience();
+ }
+
/** Applies the given settings updater function to the given method settings builders. */
protected static void applyToAllUnaryMethods(
Iterable> methodSettingsBuilders,
@@ -344,6 +368,7 @@ public String toString() {
.add("quotaProjectId", getQuotaProjectId())
.add("watchdogProvider", getWatchdogProvider())
.add("watchdogCheckInterval", getWatchdogCheckInterval())
+ .add("gdchApiAudience", getGdchApiAudience())
.toString();
}
}
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java
index 877b03e29f..7ebbe327c8 100644
--- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java
+++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java
@@ -73,6 +73,7 @@ public abstract class StubSettings> {
private final String endpoint;
private final String mtlsEndpoint;
private final String quotaProjectId;
+ @Nullable private final String gdchApiAudience;
@Nullable private final WatchdogProvider streamWatchdogProvider;
@Nonnull private final Duration streamWatchdogCheckInterval;
@Nonnull private final ApiTracerFactory tracerFactory;
@@ -103,6 +104,7 @@ protected StubSettings(Builder builder) {
this.streamWatchdogCheckInterval = builder.streamWatchdogCheckInterval;
this.tracerFactory = builder.tracerFactory;
this.deprecatedExecutorProviderSet = builder.deprecatedExecutorProviderSet;
+ this.gdchApiAudience = builder.gdchApiAudience;
}
/** @deprecated Please use {@link #getBackgroundExecutorProvider()}. */
@@ -172,6 +174,12 @@ public ApiTracerFactory getTracerFactory() {
return tracerFactory;
}
+ /** Gets the GDCH API audience to be used with {@link com.google.auth.oauth2.GdchCredentials} */
+ @Nullable
+ public final String getGdchApiAudience() {
+ return gdchApiAudience;
+ }
+
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
@@ -188,6 +196,7 @@ public String toString() {
.add("streamWatchdogProvider", streamWatchdogProvider)
.add("streamWatchdogCheckInterval", streamWatchdogCheckInterval)
.add("tracerFactory", tracerFactory)
+ .add("gdchApiAudience", gdchApiAudience)
.toString();
}
@@ -205,6 +214,7 @@ public abstract static class Builder<
private String endpoint;
private String mtlsEndpoint;
private String quotaProjectId;
+ @Nullable private String gdchApiAudience;
@Nullable private WatchdogProvider streamWatchdogProvider;
@Nonnull private Duration streamWatchdogCheckInterval;
@Nonnull private ApiTracerFactory tracerFactory;
@@ -234,6 +244,7 @@ protected Builder(StubSettings settings) {
this.streamWatchdogCheckInterval = settings.streamWatchdogCheckInterval;
this.tracerFactory = settings.tracerFactory;
this.deprecatedExecutorProviderSet = settings.deprecatedExecutorProviderSet;
+ this.gdchApiAudience = settings.gdchApiAudience;
}
/** Get Quota Project ID from Client Context * */
@@ -268,6 +279,7 @@ protected Builder(ClientContext clientContext) {
this.streamWatchdogCheckInterval = Duration.ofSeconds(10);
this.tracerFactory = BaseApiTracerFactory.getInstance();
this.deprecatedExecutorProviderSet = false;
+ this.gdchApiAudience = null;
} else {
ExecutorProvider fixedExecutorProvider =
FixedExecutorProvider.create(clientContext.getExecutor());
@@ -289,6 +301,7 @@ protected Builder(ClientContext clientContext) {
this.streamWatchdogCheckInterval = clientContext.getStreamWatchdogCheckInterval();
this.tracerFactory = clientContext.getTracerFactory();
this.quotaProjectId = getQuotaProjectIdFromClientContext(clientContext);
+ this.gdchApiAudience = clientContext.getGdchApiAudience();
}
}
@@ -435,6 +448,18 @@ public B setStreamWatchdogCheckInterval(@Nonnull Duration checkInterval) {
return self();
}
+ /**
+ * Sets the API audience used by {@link com.google.auth.oauth2.GdchCredentials} It cannot be
+ * used if other type of {@link com.google.auth.Credentials} is used. If the provided
+ * credentials already have an api audience set, then it will be overriden by this audience
+ *
+ * @param gdchApiAudience the audience to be used - must be a valid URI string
+ */
+ public B setGdchApiAudience(String gdchApiAudience) {
+ this.gdchApiAudience = gdchApiAudience;
+ return self();
+ }
+
/**
* Configures the {@link ApiTracerFactory} that will be used to generate traces.
*
@@ -513,6 +538,11 @@ public ApiTracerFactory getTracerFactory() {
return tracerFactory;
}
+ /** Gets the GDCH API audience that was previously set in this Builder */
+ public String getGdchApiAudience() {
+ return gdchApiAudience;
+ }
+
/** Applies the given settings updater function to the given method settings builders. */
protected static void applyToAllUnaryMethods(
Iterable> methodSettingsBuilders,
@@ -540,6 +570,7 @@ public String toString() {
.add("streamWatchdogProvider", streamWatchdogProvider)
.add("streamWatchdogCheckInterval", streamWatchdogCheckInterval)
.add("tracerFactory", tracerFactory)
+ .add("gdchApiAudience", gdchApiAudience)
.toString();
}
}
diff --git a/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java b/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java
index 7b69e30feb..ebe7a66712 100644
--- a/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java
+++ b/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java
@@ -32,8 +32,13 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import com.google.api.core.ApiClock;
import com.google.api.gax.core.BackgroundResource;
@@ -50,10 +55,13 @@
import com.google.api.gax.rpc.testing.FakeStubSettings;
import com.google.api.gax.rpc.testing.FakeTransportChannel;
import com.google.auth.Credentials;
+import com.google.auth.oauth2.ComputeEngineCredentials;
+import com.google.auth.oauth2.GdchCredentials;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.collect.ImmutableMap;
import com.google.common.truth.Truth;
import java.io.IOException;
+import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -790,4 +798,156 @@ public void testExecutorSettings() throws Exception {
transportChannel = (FakeTransportChannel) context.getTransportChannel();
assertThat(transportChannel.getExecutor()).isSameInstanceAs(executorProvider.getExecutor());
}
+
+ private GdchCredentials getMockGdchCredentials() throws IOException {
+ GdchCredentials creds = Mockito.mock(GdchCredentials.class);
+
+ // GdchCredentials builder is mocked to accept a well-formed uri
+ GdchCredentials.Builder gdchCredsBuilder = Mockito.mock(GdchCredentials.Builder.class);
+ Mockito.when(gdchCredsBuilder.setGdchAudience(Mockito.any(URI.class)))
+ .thenReturn(gdchCredsBuilder);
+ Mockito.when(gdchCredsBuilder.build()).thenReturn(creds);
+ Mockito.when(creds.toBuilder()).thenReturn(gdchCredsBuilder);
+ Mockito.when(creds.createWithGdchAudience(Mockito.any()))
+ .thenAnswer((uri) -> getMockGdchCredentials());
+ return creds;
+ }
+
+ private TransportChannelProvider getFakeTransportChannelProvider() {
+ return new FakeTransportProvider(
+ FakeTransportChannel.create(new FakeChannel()), null, true, null, null);
+ }
+
+ @Test
+ public void testCreateClientContext_withGdchCredentialNoAudienceNoEndpoint_throws()
+ throws IOException {
+ TransportChannelProvider transportChannelProvider = getFakeTransportChannelProvider();
+ Credentials creds = getMockGdchCredentials();
+
+ CredentialsProvider provider = FixedCredentialsProvider.create(creds);
+ StubSettings settings = new FakeStubSettings.Builder().setGdchApiAudience(null).build();
+ FakeClientSettings.Builder clientSettingsBuilder = new FakeClientSettings.Builder(settings);
+ clientSettingsBuilder.setCredentialsProvider(provider);
+ clientSettingsBuilder.setTransportChannelProvider(transportChannelProvider);
+
+ // should throw
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ClientContext.create(clientSettingsBuilder.build()));
+ assertEquals("Could not infer GDCH api audience from settings", ex.getMessage());
+ }
+
+ @Test
+ public void testCreateClientContext_withGdchCredentialWithoutAudienceWithEndpoint_correct()
+ throws IOException {
+ TransportChannelProvider transportChannelProvider = getFakeTransportChannelProvider();
+ Credentials creds = getMockGdchCredentials();
+
+ // it should correctly create a client context with gdch creds and null audience
+ CredentialsProvider provider = FixedCredentialsProvider.create(creds);
+ StubSettings settings =
+ new FakeStubSettings.Builder()
+ .setGdchApiAudience(null)
+ .setEndpoint("test-endpoint")
+ .build();
+ FakeClientSettings.Builder clientSettingsBuilder = new FakeClientSettings.Builder(settings);
+ clientSettingsBuilder.setCredentialsProvider(provider);
+ clientSettingsBuilder.setTransportChannelProvider(transportChannelProvider);
+
+ // should not throw
+ ClientContext context = ClientContext.create(clientSettingsBuilder.build());
+
+ Credentials fromContext = context.getCredentials();
+ Credentials fromProvider = provider.getCredentials();
+ assertNotNull(fromProvider);
+ assertNotNull(fromContext);
+ assertThat(fromContext).isInstanceOf(GdchCredentials.class);
+ assertThat(fromProvider).isInstanceOf(GdchCredentials.class);
+ assertNotSame(fromContext, fromProvider);
+ verify((GdchCredentials) fromProvider, times(1))
+ .createWithGdchAudience(URI.create("test-endpoint"));
+ }
+
+ @Test
+ public void testCreateClientContext_withGdchCredentialAndValidAudience() throws IOException {
+ Credentials creds = getMockGdchCredentials();
+ CredentialsProvider provider = FixedCredentialsProvider.create(creds);
+ TransportChannelProvider transportChannelProvider = getFakeTransportChannelProvider();
+
+ // it should throw if both apiAudience and GDC-H creds are set but apiAudience is not a valid
+ // uri
+ StubSettings settings =
+ new FakeStubSettings.Builder()
+ .setEndpoint("test-endpoint")
+ .setGdchApiAudience("valid-uri")
+ .build();
+ ClientSettings.Builder clientSettingsBuilder = new FakeClientSettings.Builder(settings);
+ clientSettingsBuilder.setCredentialsProvider(provider);
+ clientSettingsBuilder.setTransportChannelProvider(transportChannelProvider);
+ ClientContext context = ClientContext.create(clientSettingsBuilder.build());
+ Credentials fromContext = context.getCredentials();
+ Credentials fromProvider = provider.getCredentials();
+ assertNotNull(fromProvider);
+ assertNotNull(fromContext);
+ // using an audience should have made the context to recreate the credentials
+ assertNotSame(fromContext, fromProvider);
+ verify((GdchCredentials) fromProvider, times(1))
+ .createWithGdchAudience(URI.create("valid-uri"));
+ verify((GdchCredentials) fromProvider, times(0))
+ .createWithGdchAudience(URI.create("test-endpoint"));
+ }
+
+ @Test
+ public void testCreateClientContext_withGdchCredentialAndInvalidAudience_throws()
+ throws IOException {
+ TransportChannelProvider transportChannelProvider = getFakeTransportChannelProvider();
+ Credentials creds = getMockGdchCredentials();
+ CredentialsProvider provider = FixedCredentialsProvider.create(creds);
+
+ // it should throw if both apiAudience and GDC-H creds are set but apiAudience is not a valid
+ // uri
+ StubSettings settings =
+ new FakeStubSettings.Builder()
+ .setGdchApiAudience("$invalid-uri:")
+ .setEndpoint("test-endpoint")
+ .build();
+ ClientSettings.Builder clientSettingsBuilder = new FakeClientSettings.Builder(settings);
+ clientSettingsBuilder.setCredentialsProvider(provider);
+ clientSettingsBuilder.setTransportChannelProvider(transportChannelProvider);
+ final ClientSettings withGdchCredentialsAndMalformedApiAudience = clientSettingsBuilder.build();
+ // should throw
+ String exMessage =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ClientContext.create(withGdchCredentialsAndMalformedApiAudience))
+ .getMessage();
+ assertThat(exMessage).contains("The GDC-H API audience string is not a valid URI");
+
+ Credentials fromProvider = provider.getCredentials();
+ verify((GdchCredentials) fromProvider, times(0))
+ .createWithGdchAudience(URI.create("test-endpoint"));
+ }
+
+ @Test
+ public void testCreateClientContext_withNonGdchCredentialAndAnyAudience_throws()
+ throws IOException {
+ TransportChannelProvider transportChannelProvider = getFakeTransportChannelProvider();
+
+ // it should throw if apiAudience is set but not using GDC-H creds
+ StubSettings settings =
+ new FakeStubSettings.Builder().setGdchApiAudience("audience:test").build();
+ Credentials creds = Mockito.mock(ComputeEngineCredentials.class);
+ CredentialsProvider provider = FixedCredentialsProvider.create(creds);
+ ClientSettings.Builder clientSettingsBuilder = new FakeClientSettings.Builder(settings);
+ clientSettingsBuilder.setCredentialsProvider(provider);
+ clientSettingsBuilder.setTransportChannelProvider(transportChannelProvider);
+ final ClientSettings withComputeCredentials = clientSettingsBuilder.build();
+ // should throw
+ String exMessage =
+ assertThrows(
+ IllegalArgumentException.class, () -> ClientContext.create(withComputeCredentials))
+ .getMessage();
+ assertThat(exMessage).contains("GDC-H API audience can only be set when using GdchCredentials");
+ }
}
diff --git a/showcase/gapic-showcase/pom.xml b/showcase/gapic-showcase/pom.xml
index 8663ade568..6112e56e9a 100644
--- a/showcase/gapic-showcase/pom.xml
+++ b/showcase/gapic-showcase/pom.xml
@@ -229,5 +229,13 @@
testlib
test
+
+ com.google.auth
+ google-auth-library-oauth2-http
+ ${google.auth.version}
+ test-jar
+ testlib
+ test
+
diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITGdch.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITGdch.java
new file mode 100644
index 0000000000..83dfb3d704
--- /dev/null
+++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITGdch.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.showcase.v1beta1.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.api.gax.core.FixedCredentialsProvider;
+import com.google.api.gax.rpc.ClientContext;
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.GdchCredentials;
+import com.google.auth.oauth2.GdchCredentialsTestUtil;
+import com.google.showcase.v1beta1.EchoClient;
+import com.google.showcase.v1beta1.EchoSettings;
+import com.google.showcase.v1beta1.it.util.InterceptingMockTokenServerTransportFactory;
+import com.google.showcase.v1beta1.stub.EchoStub;
+import com.google.showcase.v1beta1.stub.EchoStubSettings;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Test suite to confirm a client can be instantiated with GDCH credentials. No calls are made since
+ * it is not feasible to test against real GDCH servers (or replicate an environment)
+ */
+public class ITGdch {
+
+ private static final String TEST_GDCH_CREDENTIAL_FILE = "/test_gdch_credential.json";
+ private static final String CA_CERT_RESOURCE_PATH = "/fake_cert.pem";
+ private static final String CA_CERT_JSON_KEY = "ca_cert_path";
+ private static final String TEMP_CREDENTIAL_JSON_FILENAME = "temp_gdch_credential.json";
+ private static final String GDCH_TOKEN_STRING = "1/MkSJoj1xsli0AccessToken_NKPY2";
+ private static final String SID_NAME = "service-identity-name";
+
+ @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private EchoClient client;
+ private EchoSettings settings;
+ private EchoStubSettings stubSettings;
+ private Credentials initialCredentials;
+ private ClientContext context;
+ private EchoStub stub;
+ private InterceptingMockTokenServerTransportFactory transportFactory;
+ private String projectId;
+ private URI tokenUri;
+
+ @Before
+ public void setup() throws IOException, URISyntaxException {
+ transportFactory = new InterceptingMockTokenServerTransportFactory();
+ prepareCredentials();
+ tempFolder.create();
+ settings =
+ EchoSettings.newBuilder()
+ .setCredentialsProvider(FixedCredentialsProvider.create(initialCredentials))
+ .build();
+ }
+
+ @After
+ public void tearDown() {
+ if (client != null) {
+ client.close();
+ }
+ }
+
+ private void prepareCredentials() throws IOException, URISyntaxException {
+ // compute absolute path of the CA certificate
+ Path caCertPath = Paths.get(getClass().getResource(CA_CERT_RESOURCE_PATH).toURI());
+
+ // open gdch credential json (still needs its "ca_cert_path" to point to the CA certificate
+ // obtained from above)
+ JsonFactory factory = new GsonFactory();
+ GenericJson converted =
+ factory.fromInputStream(
+ getClass().getResourceAsStream(TEST_GDCH_CREDENTIAL_FILE), GenericJson.class);
+
+ // modify and save to a temporary folder
+ converted.set(CA_CERT_JSON_KEY, caCertPath.toAbsolutePath().toString());
+ projectId = converted.get("project").toString();
+ tokenUri = URI.create(converted.get("token_uri").toString());
+
+ File tempGdchCredentialFile = tempFolder.newFile(TEMP_CREDENTIAL_JSON_FILENAME);
+ try (FileWriter fileWriter = new FileWriter(tempGdchCredentialFile)) {
+ String preparedJson = converted.toPrettyString();
+ fileWriter.write(preparedJson);
+ }
+
+ // use temp location to instantiate credentials
+ initialCredentials = GdchCredentialsTestUtil.fromJson(converted, transportFactory);
+ }
+
+ /**
+ * {@link com.google.api.gax.rpc.ClientContext} will create a new {@link GdchCredentials} with an
+ * audience defaulted to the endpoint if the audience is not manually passed. This test confirms
+ * that a new credential is created from the context and can be refreshed
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testCreateClient_withGdchCredentialAndNoAudience_defaultsToEndpointBasedAudience()
+ throws IOException {
+
+ // we create the client as usual - no audience passed
+ String testEndpoint = "custom-endpoint:123";
+ settings = settings.toBuilder().setEndpoint(testEndpoint).build();
+ context = ClientContext.create(settings);
+ stubSettings = EchoStubSettings.newBuilder(context).build();
+ client = EchoClient.create(stubSettings.createStub());
+
+ // We retrieve from context and from client
+ // the client has only access to creds provider, which may differ from the actual credentials
+ // used in the Context
+ Credentials fromContext = context.getCredentials();
+ Credentials fromClient = initialCredentials;
+
+ // Since ClientContext.create() uses a modified version of GdchCredentials
+ // via GdchCredentials.createWithGdchAudience(), they should be different objects
+ assertNotSame(fromContext, fromClient);
+
+ // When credentials don't have an audience (such as the ones we passed to client creation and
+ // now stored in the
+ // provider) they will throw if we try to refresh them
+ NullPointerException expectedEx =
+ assertThrows(NullPointerException.class, () -> initialCredentials.refresh());
+ assertTrue(
+ expectedEx.getMessage().contains("Audience are not configured for GDCH service account"));
+
+ // However, the credentials prepared in ClientContext should be able to refresh since the
+ // audience would be
+ // internally defaulted the endpoint of the StubSettings
+ registerCredential(fromContext);
+ ((GdchCredentials) fromContext).refreshAccessToken();
+ String usedAudience = transportFactory.transport.getLastAudienceSent();
+ assertEquals(testEndpoint, usedAudience);
+ }
+
+ /**
+ * Confirms creating a client with a valid audience is successful. We cannot confirm which
+ * audience is chosen (our passed audience or the endpoint) but this is confirmed in the unit
+ * tests.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void
+ testCreateClient_withGdchCredentialWithValidAudience_usesCredentialWithPassedAudience()
+ throws IOException {
+
+ // Similar to the previous test, create a client as usual but this time we pass a explicit
+ // audience. It should
+ // be created without issues
+ String testAudience = "valid-audience";
+ settings = settings.toBuilder().setGdchApiAudience(testAudience).build();
+ context = ClientContext.create(settings);
+ stubSettings = EchoStubSettings.newBuilder(context).build();
+ client = EchoClient.create(stubSettings.createStub());
+
+ // We retrieve both creds from the creds provider and the ones prepared in the context (which
+ // should have been
+ // re-created using GdchCredentials.createWithAudience("valid-audience"))
+ Credentials fromContext = context.getCredentials();
+ assertNotSame(fromContext, initialCredentials);
+
+ // Again, since the initial credentials don't have an audience, we should not be able to refresh
+ // them
+ NullPointerException thrownByClientCreds =
+ assertThrows(NullPointerException.class, () -> initialCredentials.refresh());
+ assertTrue(
+ thrownByClientCreds
+ .getMessage()
+ .contains("Audience are not configured for GDCH service account"));
+
+ // But the credentials prepared in ClientContext should be able to refresh since the audience
+ // would be internally
+ // set to the one passed in stub settings
+ registerCredential(fromContext);
+ ((GdchCredentials) fromContext).refreshAccessToken();
+ String usedAudience = transportFactory.transport.getLastAudienceSent();
+ assertEquals(testAudience, usedAudience);
+ }
+
+ private void registerCredential(Credentials fromContext) {
+ GdchCredentialsTestUtil.registerGdchCredentialWithMockTransport(
+ (GdchCredentials) fromContext,
+ transportFactory.transport,
+ projectId,
+ SID_NAME,
+ GDCH_TOKEN_STRING,
+ tokenUri);
+ }
+}
diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/InterceptingMockTokenServerTransport.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/InterceptingMockTokenServerTransport.java
new file mode 100644
index 0000000000..40860d97be
--- /dev/null
+++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/InterceptingMockTokenServerTransport.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.showcase.v1beta1.it.util;
+
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.api.client.json.webtoken.JsonWebSignature;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.util.StreamingContent;
+import com.google.auth.TestUtils;
+import com.google.auth.oauth2.MockTokenServerTransport;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class InterceptingMockTokenServerTransport extends MockTokenServerTransport {
+ private MockLowLevelHttpRequest lastRequest;
+ private static final JsonFactory JSON_FACTORY = new GsonFactory();
+
+ @Override
+ public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ MockLowLevelHttpRequest baseRequest = (MockLowLevelHttpRequest) super.buildRequest(method, url);
+ lastRequest = baseRequest;
+ return baseRequest;
+ }
+
+ public String getLastAudienceSent() throws IOException {
+ String contentString = lastRequest.getContentAsString();
+ Map query = TestUtils.parseQuery(contentString);
+ String assertion = query.get("assertion");
+ JsonWebSignature signature = JsonWebSignature.parse(JSON_FACTORY, assertion);
+ String foundTargetAudience = (String) signature.getPayload().get("api_audience");
+ return foundTargetAudience;
+ }
+}
diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/InterceptingMockTokenServerTransportFactory.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/InterceptingMockTokenServerTransportFactory.java
new file mode 100644
index 0000000000..175beb61d6
--- /dev/null
+++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/InterceptingMockTokenServerTransportFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.showcase.v1beta1.it.util;
+
+import com.google.api.client.http.HttpTransport;
+import com.google.auth.oauth2.MockTokenServerTransport;
+import com.google.auth.oauth2.MockTokenServerTransportFactory;
+
+public class InterceptingMockTokenServerTransportFactory extends MockTokenServerTransportFactory {
+
+ public InterceptingMockTokenServerTransport transport = new InterceptingMockTokenServerTransport();
+
+ @Override
+ public HttpTransport create() {
+ return transport;
+ }
+}
diff --git a/showcase/gapic-showcase/src/test/resources/fake_cert.pem b/showcase/gapic-showcase/src/test/resources/fake_cert.pem
new file mode 100644
index 0000000000..03febfd3ae
--- /dev/null
+++ b/showcase/gapic-showcase/src/test/resources/fake_cert.pem
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIICUTCCAfugAwIBAgIBADANBgkqhkiG9w0BAQQFADBXMQswCQYDVQQGEwJDTjEL
+MAkGA1UECBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMC
+VU4xFDASBgNVBAMTC0hlcm9uZyBZYW5nMB4XDTA1MDcxNTIxMTk0N1oXDTA1MDgx
+NDIxMTk0N1owVzELMAkGA1UEBhMCQ04xCzAJBgNVBAgTAlBOMQswCQYDVQQHEwJD
+TjELMAkGA1UEChMCT04xCzAJBgNVBAsTAlVOMRQwEgYDVQQDEwtIZXJvbmcgWWFu
+ZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQCp5hnG7ogBhtlynpOS21cBewKE/B7j
+V14qeyslnr26xZUsSVko36ZnhiaO/zbMOoRcKK9vEcgMtcLFuQTWDl3RAgMBAAGj
+gbEwga4wHQYDVR0OBBYEFFXI70krXeQDxZgbaCQoR4jUDncEMH8GA1UdIwR4MHaA
+FFXI70krXeQDxZgbaCQoR4jUDncEoVukWTBXMQswCQYDVQQGEwJDTjELMAkGA1UE
+CBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMCVU4xFDAS
+BgNVBAMTC0hlcm9uZyBZYW5nggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEE
+BQADQQA/ugzBrjjK9jcWnDVfGHlk3icNRq0oV7Ri32z/+HQX67aRfgZu7KWdI+Ju
+Wm7DCfrPNGVwFWUQOmsPue9rZBgO
+-----END CERTIFICATE-----
diff --git a/showcase/gapic-showcase/src/test/resources/test_gdch_credential.json b/showcase/gapic-showcase/src/test/resources/test_gdch_credential.json
new file mode 100644
index 0000000000..16c2582cdc
--- /dev/null
+++ b/showcase/gapic-showcase/src/test/resources/test_gdch_credential.json
@@ -0,0 +1,10 @@
+{
+ "type": "gdch_service_account",
+ "format_version": "1",
+ "project": "project-id",
+ "private_key_id": "d84a4fefcf50791d4a90f2d7af17469d6282df9d",
+ "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12i\nkv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj79Xc3yBDfKP2IeyYQIFe0t0\nzkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kHvoa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw\n4Az2ZkmeuN6Fk/y9H+Lcb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB/Gr\nCtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ryJR7GLbCzxPnJm/oQJBANwOCO6\nD2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrP\nSXagBxzp8PecbaCHjzNRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAkAut\nLPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE80L8jVLSbrbrlH43H0DjU5AkEA\ngidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEiUltk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJ\nADj3e1YhMVdjJW5jqwlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAzMDjCQ\n==\n-----END PRIVATE KEY-----\n",
+ "name": "service-identity-name",
+ "ca_cert_path": "fake-cert-path",
+ "token_uri": "https://service-identity.fake-domain/authenticate"
+}