From 767fa890eecb31baf2d65f69a884c99eb88247cd Mon Sep 17 00:00:00 2001 From: Will Howes Date: Tue, 2 Jun 2026 19:45:06 +0000 Subject: [PATCH 1/5] feat: enable self-signed JWTs by default in ServiceOptions --- .../java/com/google/cloud/bigquery/BigQueryOptions.java | 5 +++++ .../src/main/java/com/google/cloud/ServiceOptions.java | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryOptions.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryOptions.java index 8ac4c622a91c..88821417967f 100644 --- a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryOptions.java +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryOptions.java @@ -213,6 +213,11 @@ public static HttpTransportOptions getDefaultHttpTransportOptions() { return HttpTransportOptions.newBuilder().setReadTimeout(DEFAULT_READ_API_TIME_OUT).build(); } + @Override + protected boolean useSelfSignedJwt() { + return false; + } + @Override protected Set getScopes() { return SCOPES; diff --git a/sdk-platform-java/java-core/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java b/sdk-platform-java/java-core/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java index 92aaa9d6a9e1..4cd8e1ca7535 100644 --- a/sdk-platform-java/java-core/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java +++ b/sdk-platform-java/java-core/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java @@ -650,9 +650,17 @@ public Credentials getScopedCredentials() { && ((GoogleCredentials) credentials).createScopedRequired()) { credentialsToReturn = ((GoogleCredentials) credentials).createScoped(getScopes()); } + if (useSelfSignedJwt() && credentialsToReturn instanceof ServiceAccountCredentials) { + credentialsToReturn = + ((ServiceAccountCredentials) credentialsToReturn).createWithUseJwtAccessWithScope(true); + } return credentialsToReturn; } + protected boolean useSelfSignedJwt() { + return true; + } + /** Returns configuration parameters for request retries. */ public RetrySettings getRetrySettings() { return retrySettings; From 7e19520be48f778dda38ea93c0fa6215e08c88dd Mon Sep 17 00:00:00 2001 From: Will Howes Date: Fri, 5 Jun 2026 23:51:18 +0000 Subject: [PATCH 2/5] add new test cases to ServiceOptionsTest --- .../com/google/cloud/ServiceOptionsTest.java | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/sdk-platform-java/java-core/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java b/sdk-platform-java/java-core/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java index eb8eab37b2a6..9ee8c29252cc 100644 --- a/sdk-platform-java/java-core/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java +++ b/sdk-platform-java/java-core/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java @@ -285,7 +285,7 @@ protected TestServiceOptions build() { } } - private TestServiceOptions(Builder builder) { + protected TestServiceOptions(Builder builder) { super( TestServiceFactory.class, TestServiceRpcFactory.class, @@ -337,6 +337,25 @@ public int hashCode() { } } + private static class NonSsjwtServiceOptions extends TestServiceOptions { + private static class Builder extends TestServiceOptions.Builder { + @Override + protected NonSsjwtServiceOptions build() { + return new NonSsjwtServiceOptions(this); + } + } + + private NonSsjwtServiceOptions(Builder builder) { + super(builder); + } + + @Override + protected boolean useSelfSignedJwt() { + return false; + } + } + + @Test public void testBuilder() { assertSame(credentials, OPTIONS.getCredentials()); @@ -627,7 +646,29 @@ void testIsValidUniverseDomain_userUniverseDomainConfig_nonGDUCredentials() thro .setUniverseDomain("random.com") .setCredentials(credentialsNotInGDU) .build(); - assertThat(options.hasValidUniverseDomain()).isTrue(); + } + + @Test + void testGetScopedCredentials_enablesSelfSignedJwtForServiceAccount() { + TestServiceOptions options = + TestServiceOptions.newBuilder() + .setProjectId("project-id") + .setCredentials(credentials) + .build(); + com.google.auth.Credentials scoped = options.getScopedCredentials(); + assertThat(scoped).isInstanceOf(ServiceAccountCredentials.class); + } + + @Test + void testGetScopedCredentials_optsOutSelfSignedJwt() { + TestServiceOptions options = + new NonSsjwtServiceOptions.Builder() + .setProjectId("project-id") + .setCredentials(credentials) + .build(); + com.google.auth.Credentials scoped = options.getScopedCredentials(); + assertThat(scoped).isInstanceOf(ServiceAccountCredentials.class); + assertThat(((ServiceAccountCredentials) scoped).getUseJwtAccessWithScope()).isFalse(); } private HttpResponse createHttpResponseWithHeader(final Multimap headers) From 7e047740d0fad0977729bade6206ee0c05dc90a6 Mon Sep 17 00:00:00 2001 From: Will Howes Date: Wed, 10 Jun 2026 01:54:09 +0000 Subject: [PATCH 3/5] address review comments --- .../cloud/bigquery/BigQueryOptions.java | 9 ++---- .../java/com/google/cloud/ServiceOptions.java | 31 ++++++++++++++++--- .../com/google/cloud/ServiceOptionsTest.java | 26 +++------------- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryOptions.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryOptions.java index 88821417967f..b8b7994f8775 100644 --- a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryOptions.java +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryOptions.java @@ -77,7 +77,9 @@ public static class Builder extends ServiceOptions.Builder resultRetryAlgorithm; - private Builder() {} + private Builder() { + setUseJwtAccessWithScope(false); + } private Builder(BigQueryOptions options) { super(options); @@ -213,11 +215,6 @@ public static HttpTransportOptions getDefaultHttpTransportOptions() { return HttpTransportOptions.newBuilder().setReadTimeout(DEFAULT_READ_API_TIME_OUT).build(); } - @Override - protected boolean useSelfSignedJwt() { - return false; - } - @Override protected Set getScopes() { return SCOPES; diff --git a/sdk-platform-java/java-core/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java b/sdk-platform-java/java-core/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java index 4cd8e1ca7535..3e9f2b24cb10 100644 --- a/sdk-platform-java/java-core/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java +++ b/sdk-platform-java/java-core/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java @@ -106,6 +106,7 @@ public abstract class ServiceOptions< private final TransportOptions transportOptions; private final HeaderProvider headerProvider; private final String quotaProjectId; + private final boolean useJwtAccessWithScope; private transient ServiceRpcFactory serviceRpcFactory; private transient ServiceFactory serviceFactory; @@ -140,6 +141,7 @@ public abstract static class Builder< private HeaderProvider headerProvider; private String clientLibToken = ServiceOptions.getGoogApiClientLibName(); private String quotaProjectId; + private boolean useJwtAccessWithScope = true; private ApiTracerFactory apiTracerFactory; @@ -159,6 +161,7 @@ protected Builder(ServiceOptions options) { transportOptions = options.transportOptions; clientLibToken = options.clientLibToken; quotaProjectId = options.quotaProjectId; + useJwtAccessWithScope = options.useJwtAccessWithScope; apiTracerFactory = options.apiTracerFactory; } @@ -313,6 +316,18 @@ public B setQuotaProjectId(String quotaProjectId) { return self(); } + /** + * Sets the configuration determining whether self-signed JWT with scopes are used for service + * account credentials. + * + * @param useJwtAccessWithScope whether to use self-signed JWT with scopes + * @return the builder + */ + public B setUseJwtAccessWithScope(final boolean useJwtAccessWithScope) { + this.useJwtAccessWithScope = useJwtAccessWithScope; + return self(); + } + /** * Sets the {@link ApiTracerFactory}. It will be used to create an {@link ApiTracer} that is * annotated throughout the lifecycle of an RPC operation. @@ -365,6 +380,7 @@ protected ServiceOptions( builder.quotaProjectId != null ? builder.quotaProjectId : getValueFromCredentialsFile(getCredentialsPath(), "quota_project_id"); + useJwtAccessWithScope = builder.useJwtAccessWithScope; apiTracerFactory = builder.apiTracerFactory; } @@ -650,17 +666,13 @@ public Credentials getScopedCredentials() { && ((GoogleCredentials) credentials).createScopedRequired()) { credentialsToReturn = ((GoogleCredentials) credentials).createScoped(getScopes()); } - if (useSelfSignedJwt() && credentialsToReturn instanceof ServiceAccountCredentials) { + if (getUseJwtAccessWithScope() && credentialsToReturn instanceof ServiceAccountCredentials) { credentialsToReturn = ((ServiceAccountCredentials) credentialsToReturn).createWithUseJwtAccessWithScope(true); } return credentialsToReturn; } - protected boolean useSelfSignedJwt() { - return true; - } - /** Returns configuration parameters for request retries. */ public RetrySettings getRetrySettings() { return retrySettings; @@ -831,6 +843,15 @@ public String getQuotaProjectId() { return quotaProjectId; } + /** + * Returns true when self-signed JWT with scopes are used for service account credentials. + * + * @return true when self-signed JWT with scopes are used + */ + public boolean getUseJwtAccessWithScope() { + return useJwtAccessWithScope; + } + /** * Returns the resolved host for the Service to connect to Google Cloud * diff --git a/sdk-platform-java/java-core/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java b/sdk-platform-java/java-core/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java index 9ee8c29252cc..0fe91ae5c53f 100644 --- a/sdk-platform-java/java-core/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java +++ b/sdk-platform-java/java-core/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java @@ -285,7 +285,7 @@ protected TestServiceOptions build() { } } - protected TestServiceOptions(Builder builder) { + private TestServiceOptions(Builder builder) { super( TestServiceFactory.class, TestServiceRpcFactory.class, @@ -337,25 +337,6 @@ public int hashCode() { } } - private static class NonSsjwtServiceOptions extends TestServiceOptions { - private static class Builder extends TestServiceOptions.Builder { - @Override - protected NonSsjwtServiceOptions build() { - return new NonSsjwtServiceOptions(this); - } - } - - private NonSsjwtServiceOptions(Builder builder) { - super(builder); - } - - @Override - protected boolean useSelfSignedJwt() { - return false; - } - } - - @Test public void testBuilder() { assertSame(credentials, OPTIONS.getCredentials()); @@ -646,6 +627,7 @@ void testIsValidUniverseDomain_userUniverseDomainConfig_nonGDUCredentials() thro .setUniverseDomain("random.com") .setCredentials(credentialsNotInGDU) .build(); + assertThat(options.hasValidUniverseDomain()).isTrue(); } @Test @@ -657,14 +639,16 @@ void testGetScopedCredentials_enablesSelfSignedJwtForServiceAccount() { .build(); com.google.auth.Credentials scoped = options.getScopedCredentials(); assertThat(scoped).isInstanceOf(ServiceAccountCredentials.class); + assertThat(((ServiceAccountCredentials) scoped).getUseJwtAccessWithScope()).isTrue(); } @Test void testGetScopedCredentials_optsOutSelfSignedJwt() { TestServiceOptions options = - new NonSsjwtServiceOptions.Builder() + new TestServiceOptions.Builder() .setProjectId("project-id") .setCredentials(credentials) + .setUseJwtAccessWithScope(false) .build(); com.google.auth.Credentials scoped = options.getScopedCredentials(); assertThat(scoped).isInstanceOf(ServiceAccountCredentials.class); From ac8cf3edda114ebd5a663ea2534a94563dcfff8f Mon Sep 17 00:00:00 2001 From: Will Howes Date: Wed, 10 Jun 2026 18:01:41 +0000 Subject: [PATCH 4/5] opt out StorageOptions and add unit tests --- .../com/google/cloud/bigquery/BigQueryOptionsTest.java | 7 +++++++ .../java/com/google/cloud/storage/StorageOptions.java | 4 +++- .../google/cloud/storage/StorageOptionsBuilderTest.java | 9 +++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryOptionsTest.java b/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryOptionsTest.java index 050deba4af16..49d6f6fe031f 100644 --- a/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryOptionsTest.java +++ b/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryOptionsTest.java @@ -92,4 +92,11 @@ void dataFormatOptionsSetterHasPrecedence() { assertTrue(options.getDataFormatOptions().useInt64Timestamp()); } + + @Test + void testUseJwtAccessWithScope_defaultsToFalse() { + BigQueryOptions options = BigQueryOptions.newBuilder().setProjectId("project-id").build(); + + assertFalse(options.getUseJwtAccessWithScope()); + } } diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java index 723a11dc34ce..97eecedaeef1 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java @@ -112,7 +112,9 @@ public DefaultStorageRpcFactory() { public abstract static class Builder extends ServiceOptions.Builder { - Builder() {} + Builder() { + setUseJwtAccessWithScope(false); + } Builder(StorageOptions options) { super(options); diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsBuilderTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsBuilderTest.java index 4601a3b2e8df..240040519635 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsBuilderTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsBuilderTest.java @@ -69,6 +69,15 @@ public void grpc() throws Exception { () -> assertThat(rebuilt.hashCode()).isEqualTo(base.hashCode())); } + @Test + public void useJwtAccessWithScope_defaultsToFalse() { + HttpStorageOptions httpOptions = HttpStorageOptions.http().build(); + GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().build(); + + assertThat(httpOptions.getUseJwtAccessWithScope()).isFalse(); + assertThat(grpcOptions.getUseJwtAccessWithScope()).isFalse(); + } + private static class MyStorageRetryStrategy implements StorageRetryStrategy { @Override From 9c1e8271ea855cab823d369c1b7cf032ff8659f9 Mon Sep 17 00:00:00 2001 From: Will Howes Date: Wed, 10 Jun 2026 18:20:39 +0000 Subject: [PATCH 5/5] address review comments --- .../java/com/google/cloud/ServiceOptions.java | 5 +++-- .../com/google/cloud/ServiceOptionsTest.java | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/sdk-platform-java/java-core/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java b/sdk-platform-java/java-core/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java index 3e9f2b24cb10..89f96f4e38d8 100644 --- a/sdk-platform-java/java-core/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java +++ b/sdk-platform-java/java-core/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java @@ -666,9 +666,10 @@ public Credentials getScopedCredentials() { && ((GoogleCredentials) credentials).createScopedRequired()) { credentialsToReturn = ((GoogleCredentials) credentials).createScoped(getScopes()); } - if (getUseJwtAccessWithScope() && credentialsToReturn instanceof ServiceAccountCredentials) { + if (credentialsToReturn instanceof ServiceAccountCredentials) { credentialsToReturn = - ((ServiceAccountCredentials) credentialsToReturn).createWithUseJwtAccessWithScope(true); + ((ServiceAccountCredentials) credentialsToReturn) + .createWithUseJwtAccessWithScope(getUseJwtAccessWithScope()); } return credentialsToReturn; } diff --git a/sdk-platform-java/java-core/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java b/sdk-platform-java/java-core/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java index 0fe91ae5c53f..3c11ba0eeefa 100644 --- a/sdk-platform-java/java-core/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java +++ b/sdk-platform-java/java-core/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java @@ -631,7 +631,7 @@ void testIsValidUniverseDomain_userUniverseDomainConfig_nonGDUCredentials() thro } @Test - void testGetScopedCredentials_enablesSelfSignedJwtForServiceAccount() { + void testGetScopedCredentials_useJwtAccessWithScope_enablesByDefault() { TestServiceOptions options = TestServiceOptions.newBuilder() .setProjectId("project-id") @@ -643,7 +643,7 @@ void testGetScopedCredentials_enablesSelfSignedJwtForServiceAccount() { } @Test - void testGetScopedCredentials_optsOutSelfSignedJwt() { + void testGetScopedCredentials_useJwtAccessWithScope_canBeDisabled() { TestServiceOptions options = new TestServiceOptions.Builder() .setProjectId("project-id") @@ -655,6 +655,21 @@ void testGetScopedCredentials_optsOutSelfSignedJwt() { assertThat(((ServiceAccountCredentials) scoped).getUseJwtAccessWithScope()).isFalse(); } + @Test + void testGetScopedCredentials_useJwtAccessWithScope_overridesCredentials() { + ServiceAccountCredentials trueCredentials = + ((ServiceAccountCredentials) credentials).createWithUseJwtAccessWithScope(true); + TestServiceOptions options = + new TestServiceOptions.Builder() + .setProjectId("project-id") + .setCredentials(trueCredentials) + .setUseJwtAccessWithScope(false) + .build(); + com.google.auth.Credentials scoped = options.getScopedCredentials(); + assertThat(scoped).isInstanceOf(ServiceAccountCredentials.class); + assertThat(((ServiceAccountCredentials) scoped).getUseJwtAccessWithScope()).isFalse(); + } + private HttpResponse createHttpResponseWithHeader(final Multimap headers) throws Exception { HttpTransport mockHttpTransport =