This is an automated email from the ASF dual-hosted git repository.
acosentino pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 659ab8ac9e5f CAMEL-23231 - Camel-Google-common: Add Workload Identity
Federation support for all Google components (#22191)
659ab8ac9e5f is described below
commit 659ab8ac9e5f8c01e6a2323d9099d56dc2343580
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Mar 23 14:28:23 2026 +0100
CAMEL-23231 - Camel-Google-common: Add Workload Identity Federation support
for all Google components (#22191)
Add WIF support to GoogleCredentialsHelper enabling all 16 Google
components to authenticate using external identity providers (AWS,
Azure, GitHub Actions, GKE) without service account key files.
- Add 3 new default methods to GoogleCommonConfiguration interface:
isUseWorkloadIdentityFederation(), getWorkloadIdentityConfig(),
getImpersonatedServiceAccount()
- Update GoogleCredentialsHelper credential resolution to support
WIF config files via GoogleCredentials.fromStream() and service
account impersonation via ImpersonatedCredentials
- Add 5 unit tests for WIF credential creation flows
- Add WIF documentation with GKE and GitHub Actions examples to
google-pubsub-component.adoc
Signed-off-by: Andrea Cosentino <[email protected]>
---
.../camel-google/camel-google-common/pom.xml | 11 ++
.../google/common/GoogleCommonConfiguration.java | 31 ++++
.../google/common/GoogleCredentialsHelper.java | 67 +++++++-
.../google/common/GoogleCredentialsHelperTest.java | 170 +++++++++++++++++++++
.../src/test/resources/wif-config.json | 12 ++
.../src/main/docs/google-pubsub-component.adoc | 33 ++++
6 files changed, 323 insertions(+), 1 deletion(-)
diff --git a/components/camel-google/camel-google-common/pom.xml
b/components/camel-google/camel-google-common/pom.xml
index f7eab363c5c8..7e61dc085dc1 100644
--- a/components/camel-google/camel-google-common/pom.xml
+++ b/components/camel-google/camel-google-common/pom.xml
@@ -90,6 +90,17 @@
</exclusion>
</exclusions>
</dependency>
+ <!-- test dependencies -->
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git
a/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCommonConfiguration.java
b/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCommonConfiguration.java
index d36e10ac558b..7609245c0624 100644
---
a/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCommonConfiguration.java
+++
b/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCommonConfiguration.java
@@ -83,6 +83,37 @@ public interface GoogleCommonConfiguration {
return null;
}
+ // ==================== Workload Identity Federation ====================
+
+ /**
+ * Whether to use Workload Identity Federation (WIF) for authentication.
When enabled, the component can
+ * authenticate using external identity providers (e.g., AWS, Azure, OIDC)
without service account keys.
+ */
+ default boolean isUseWorkloadIdentityFederation() {
+ return false;
+ }
+
+ /**
+ * Path to a Workload Identity Federation JSON configuration file. This
file contains the external credential source
+ * configuration for authenticating with GCP via OIDC token exchange. Can
be loaded from classpath, file system, or
+ * http(s) URL using prefixes: classpath:, file:, http:, https:
+ * <p>
+ * If not set and useWorkloadIdentityFederation is true, Application
Default Credentials will be used, which
+ * automatically works on GKE with Workload Identity.
+ */
+ default String getWorkloadIdentityConfig() {
+ return null;
+ }
+
+ /**
+ * Target service account email for service account impersonation via
Workload Identity Federation. When set, the
+ * external credentials obtained via WIF will be used to impersonate this
service account, which grants the
+ * permissions of that service account to the workload.
+ */
+ default String getImpersonatedServiceAccount() {
+ return null;
+ }
+
// ==================== Authentication Toggle ====================
/**
diff --git
a/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCredentialsHelper.java
b/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCredentialsHelper.java
index 1449534d33b4..4c32d5ad8d65 100644
---
a/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCredentialsHelper.java
+++
b/components/camel-google/camel-google-common/src/main/java/org/apache/camel/component/google/common/GoogleCredentialsHelper.java
@@ -18,7 +18,9 @@ package org.apache.camel.component.google.common;
import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.List;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
@@ -28,6 +30,7 @@ import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.auth.Credentials;
import com.google.auth.oauth2.GoogleCredentials;
+import com.google.auth.oauth2.ImpersonatedCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import org.apache.camel.CamelContext;
import org.apache.camel.support.ResourceHelper;
@@ -38,8 +41,10 @@ import org.apache.camel.util.ObjectHelper;
* <p>
* This class handles the common patterns for obtaining Google credentials:
* <ul>
+ * <li>Workload Identity Federation (WIF) via external credential config
file</li>
* <li>Service Account JSON key file (for GCP native clients)</li>
* <li>OAuth 2.0 credentials with client ID/secret (for legacy Google API
clients)</li>
+ * <li>Service account impersonation via WIF</li>
* <li>Application Default Credentials (ADC) as fallback</li>
* </ul>
*/
@@ -59,9 +64,11 @@ public final class GoogleCredentialsHelper {
* <p>
* Resolution order:
* <ol>
+ * <li>Workload Identity Federation config file if WIF is enabled and
config provided</li>
* <li>Service Account key file if provided</li>
- * <li>Application Default Credentials (ADC) as fallback</li>
+ * <li>Application Default Credentials (ADC) as fallback (also supports
WIF on GKE automatically)</li>
* </ol>
+ * If impersonatedServiceAccount is set, the resolved credentials are
wrapped with {@link ImpersonatedCredentials}.
*
* @param context the Camel context for resource resolution
* @param config the component configuration
@@ -79,6 +86,22 @@ public final class GoogleCredentialsHelper {
return null;
}
+ // Check for Workload Identity Federation with explicit config file
+ if (config.isUseWorkloadIdentityFederation()) {
+ String wifConfig = config.getWorkloadIdentityConfig();
+ GoogleCredentials credentials;
+ if (ObjectHelper.isNotEmpty(wifConfig)) {
+ credentials = loadExternalAccountCredentials(context,
wifConfig, scopes);
+ } else {
+ // No explicit config — use ADC which auto-detects WIF on GKE
+ credentials = GoogleCredentials.getApplicationDefault();
+ if (scopes != null && !scopes.isEmpty()) {
+ credentials = credentials.createScoped(scopes);
+ }
+ }
+ return wrapWithImpersonationIfNeeded(credentials, config, scopes);
+ }
+
String serviceAccountKey = config.getServiceAccountKey();
if (ObjectHelper.isNotEmpty(serviceAccountKey)) {
return loadServiceAccountCredentials(context, serviceAccountKey,
scopes, config.getDelegate());
@@ -237,6 +260,48 @@ public final class GoogleCredentialsHelper {
}
}
+ /**
+ * Loads credentials from a Workload Identity Federation JSON
configuration file. Uses
+ * {@link GoogleCredentials#fromStream(InputStream)} which auto-detects
the credential type (external_account,
+ * authorized_user, etc.).
+ */
+ private static GoogleCredentials loadExternalAccountCredentials(
+ CamelContext context,
+ String wifConfigPath,
+ Collection<String> scopes)
+ throws IOException {
+
+ try (InputStream is =
ResourceHelper.resolveMandatoryResourceAsInputStream(context, wifConfigPath)) {
+ GoogleCredentials credentials = GoogleCredentials.fromStream(is);
+ if (scopes != null && !scopes.isEmpty()) {
+ credentials = credentials.createScoped(scopes);
+ }
+ return credentials;
+ }
+ }
+
+ /**
+ * Wraps the given credentials with {@link ImpersonatedCredentials} if
impersonatedServiceAccount is configured.
+ */
+ private static GoogleCredentials wrapWithImpersonationIfNeeded(
+ GoogleCredentials sourceCredentials,
+ GoogleCommonConfiguration config,
+ Collection<String> scopes) {
+
+ String targetServiceAccount = config.getImpersonatedServiceAccount();
+ if (ObjectHelper.isEmpty(targetServiceAccount)) {
+ return sourceCredentials;
+ }
+
+ List<String> scopeList = scopes != null ? new ArrayList<>(scopes) :
new ArrayList<>();
+ return ImpersonatedCredentials.create(
+ sourceCredentials,
+ targetServiceAccount,
+ null, // delegates
+ scopeList,
+ 3600); // lifetime in seconds (1 hour)
+ }
+
/**
* Loads service account credentials as legacy GoogleCredential for older
API clients.
*/
diff --git
a/components/camel-google/camel-google-common/src/test/java/org/apache/camel/component/google/common/GoogleCredentialsHelperTest.java
b/components/camel-google/camel-google-common/src/test/java/org/apache/camel/component/google/common/GoogleCredentialsHelperTest.java
new file mode 100644
index 000000000000..a29e7004fb7a
--- /dev/null
+++
b/components/camel-google/camel-google-common/src/test/java/org/apache/camel/component/google/common/GoogleCredentialsHelperTest.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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 org.apache.camel.component.google.common;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.ExternalAccountCredentials;
+import com.google.auth.oauth2.ImpersonatedCredentials;
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class GoogleCredentialsHelperTest {
+
+ private CamelContext context;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ context = new DefaultCamelContext();
+ context.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ if (context != null) {
+ context.stop();
+ }
+ }
+
+ @Test
+ void testWifWithExplicitConfigReturnsExternalAccountCredentials() throws
Exception {
+ TestConfig config = new TestConfig();
+ config.setUseWorkloadIdentityFederation(true);
+ config.setWorkloadIdentityConfig("classpath:wif-config.json");
+
+ List<String> scopes =
Arrays.asList("https://www.googleapis.com/auth/cloud-platform");
+ Credentials credentials =
GoogleCredentialsHelper.getCredentials(context, config, scopes);
+
+ assertNotNull(credentials);
+ assertInstanceOf(ExternalAccountCredentials.class, credentials);
+ }
+
+ @Test
+ void testWifWithImpersonationReturnsImpersonatedCredentials() throws
Exception {
+ TestConfig config = new TestConfig();
+ config.setUseWorkloadIdentityFederation(true);
+ config.setWorkloadIdentityConfig("classpath:wif-config.json");
+
config.setImpersonatedServiceAccount("[email protected]");
+
+ List<String> scopes =
Arrays.asList("https://www.googleapis.com/auth/cloud-platform");
+ Credentials credentials =
GoogleCredentialsHelper.getCredentials(context, config, scopes);
+
+ assertNotNull(credentials);
+ assertInstanceOf(ImpersonatedCredentials.class, credentials);
+ }
+
+ @Test
+ void testWifWithExplicitConfigNoScopes() throws Exception {
+ TestConfig config = new TestConfig();
+ config.setUseWorkloadIdentityFederation(true);
+ config.setWorkloadIdentityConfig("classpath:wif-config.json");
+
+ Credentials credentials =
GoogleCredentialsHelper.getCredentials(context, config, null);
+
+ assertNotNull(credentials);
+ assertInstanceOf(ExternalAccountCredentials.class, credentials);
+ }
+
+ @Test
+ void testAuthenticateDisabledReturnsNull() throws Exception {
+ TestConfig config = new TestConfig();
+ config.setAuthenticate(false);
+
+ Credentials credentials =
GoogleCredentialsHelper.getCredentials(context, config, null);
+
+ assertNull(credentials);
+ }
+
+ @Test
+ void testDefaultConfigInterfaceValues() {
+ // Verify that the interface defaults return the expected values
+ GoogleCommonConfiguration config = new TestConfig();
+ assertNull(config.getWorkloadIdentityConfig());
+ assertNull(config.getImpersonatedServiceAccount());
+ }
+
+ /**
+ * Test configuration implementing GoogleCommonConfiguration with WIF
support.
+ */
+ static class TestConfig implements GoogleCommonConfiguration {
+ private String serviceAccountKey;
+ private boolean useWorkloadIdentityFederation;
+ private String workloadIdentityConfig;
+ private String impersonatedServiceAccount;
+ private boolean authenticate = true;
+
+ @Override
+ public String getServiceAccountKey() {
+ return serviceAccountKey;
+ }
+
+ public void setServiceAccountKey(String serviceAccountKey) {
+ this.serviceAccountKey = serviceAccountKey;
+ }
+
+ @Override
+ public boolean isUseWorkloadIdentityFederation() {
+ return useWorkloadIdentityFederation;
+ }
+
+ public void setUseWorkloadIdentityFederation(boolean
useWorkloadIdentityFederation) {
+ this.useWorkloadIdentityFederation = useWorkloadIdentityFederation;
+ }
+
+ @Override
+ public String getWorkloadIdentityConfig() {
+ return workloadIdentityConfig;
+ }
+
+ public void setWorkloadIdentityConfig(String workloadIdentityConfig) {
+ this.workloadIdentityConfig = workloadIdentityConfig;
+ }
+
+ @Override
+ public String getImpersonatedServiceAccount() {
+ return impersonatedServiceAccount;
+ }
+
+ public void setImpersonatedServiceAccount(String
impersonatedServiceAccount) {
+ this.impersonatedServiceAccount = impersonatedServiceAccount;
+ }
+
+ @Override
+ public boolean isAuthenticate() {
+ return authenticate;
+ }
+
+ public void setAuthenticate(boolean authenticate) {
+ this.authenticate = authenticate;
+ }
+
+ @Override
+ public Collection<String> getScopesAsList() {
+ return null;
+ }
+ }
+}
diff --git
a/components/camel-google/camel-google-common/src/test/resources/wif-config.json
b/components/camel-google/camel-google-common/src/test/resources/wif-config.json
new file mode 100644
index 000000000000..294f871380da
--- /dev/null
+++
b/components/camel-google/camel-google-common/src/test/resources/wif-config.json
@@ -0,0 +1,12 @@
+{
+ "type": "external_account",
+ "audience":
"//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/my-pool/providers/my-provider",
+ "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
+ "token_url": "https://sts.googleapis.com/v1/token",
+ "credential_source": {
+ "file": "/var/run/secrets/tokens/gcp-token",
+ "format": {
+ "type": "text"
+ }
+ }
+}
diff --git
a/components/camel-google/camel-google-pubsub/src/main/docs/google-pubsub-component.adoc
b/components/camel-google/camel-google-pubsub/src/main/docs/google-pubsub-component.adoc
index d795cf6c2101..523021389b96 100644
---
a/components/camel-google/camel-google-pubsub/src/main/docs/google-pubsub-component.adoc
+++
b/components/camel-google/camel-google-pubsub/src/main/docs/google-pubsub-component.adoc
@@ -154,6 +154,39 @@ By default, this component acquires credentials using
`GoogleCredentials.getAppl
This behavior can be disabled by setting _authenticate_ option to `false`, in
which case requests to Google API will be made without authentication details.
This is only desirable when developing against an emulator.
This behavior can be altered by supplying a path to a service account key file.
+==== Workload Identity Federation (WIF)
+
+All Google components support
https://cloud.google.com/iam/docs/workload-identity-federation[Workload
Identity Federation], which enables workloads running outside of Google Cloud
(e.g., on AWS, Azure, GitHub Actions) or on GKE to authenticate without service
account key files.
+
+**On GKE with Workload Identity:** No configuration is needed. Application
Default Credentials (ADC) automatically detects the GKE environment and uses
the Kubernetes service account's associated GCP identity.
+
+**With an explicit WIF configuration file:** Set
`useWorkloadIdentityFederation=true` and provide the path to the WIF JSON
config file via `workloadIdentityConfig`. This is the typical setup for GitHub
Actions, AWS, and Azure workloads.
+
+[source,java]
+----
+// GKE with Workload Identity - ADC handles it automatically
+from("google-pubsub:my-project:my-subscription")
+ .to("direct:process");
+
+// GitHub Actions / AWS / Azure with WIF config file
+GooglePubsubEndpoint endpoint =
context.getEndpoint("google-pubsub:my-project:my-subscription",
GooglePubsubEndpoint.class);
+endpoint.setUseWorkloadIdentityFederation(true);
+endpoint.setWorkloadIdentityConfig("/path/to/wif-config.json");
+----
+
+**With Service Account Impersonation:** Set `impersonatedServiceAccount` to a
target service account email. The external credentials obtained via WIF will
impersonate that service account, inheriting its permissions.
+
+[source,java]
+----
+// WIF with service account impersonation
+GooglePubsubEndpoint endpoint =
context.getEndpoint("google-pubsub:my-project:my-subscription",
GooglePubsubEndpoint.class);
+endpoint.setUseWorkloadIdentityFederation(true);
+endpoint.setWorkloadIdentityConfig("/path/to/wif-config.json");
+endpoint.setImpersonatedServiceAccount("[email protected]");
+----
+
+NOTE: Workload Identity Federation support is available in all Google
components (PubSub, Storage, BigQuery, Firestore, Sheets, Calendar, Drive,
Mail, Functions, Secret Manager, Vision, Vertex AI, Speech-to-Text,
Text-to-Speech) through the common `GoogleCommonConfiguration` interface.
+
include::spring-boot:partial$starter.adoc[]
=== Manual Acknowledgement