This is an automated email from the ASF dual-hosted git repository.

turcsanyi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 7e5f84cc5f NIFI-15166 - Add support for GCP Workload Identity 
Federation to GCPCredentialsControllerService
7e5f84cc5f is described below

commit 7e5f84cc5fe7104ae002703aeebb00f9b6c0a404
Author: Pierre Villard <[email protected]>
AuthorDate: Fri Oct 31 15:24:03 2025 +0100

    NIFI-15166 - Add support for GCP Workload Identity Federation to 
GCPCredentialsControllerService
    
    This closes #10485.
    
    Signed-off-by: Peter Turcsanyi <[email protected]>
---
 .../nifi-gcp-bundle/nifi-gcp-processors/pom.xml    |   6 +-
 .../factory/AuthenticationStrategy.java            |   1 +
 .../factory/CredentialPropertyDescriptors.java     |  60 ++++++++
 .../credentials/factory/CredentialsFactory.java    |  11 ++
 .../credentials/factory/CredentialsStrategy.java   |  14 ++
 ...kloadIdentityFederationCredentialsStrategy.java | 133 +++++++++++++++++
 .../service/GCPCredentialsControllerService.java   |  16 +-
 .../org.apache.nifi.controller.ControllerService   |   2 +-
 .../additionalDetails.md                           | 165 +++++++++++++++++++++
 .../service/GCPCredentialsServiceTest.java         |  56 +++++++
 .../nifi-gcp-bundle/nifi-gcp-services-api/pom.xml  |   1 -
 11 files changed, 460 insertions(+), 5 deletions(-)

diff --git a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/pom.xml 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/pom.xml
index c7ed3e2454..cef709fc39 100644
--- a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/pom.xml
+++ b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/pom.xml
@@ -36,6 +36,11 @@
             <artifactId>nifi-utils</artifactId>
         </dependency>
 
+        <!-- OAuth2 Access Token Provider API for workload identity federation 
support -->
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-oauth2-provider-api</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-proxy-configuration-api</artifactId>
@@ -153,7 +158,6 @@
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-core</artifactId>
-            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
diff --git 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/AuthenticationStrategy.java
 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/AuthenticationStrategy.java
index 7dcaa2941e..c7edb5024d 100644
--- 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/AuthenticationStrategy.java
+++ 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/AuthenticationStrategy.java
@@ -28,6 +28,7 @@ public enum AuthenticationStrategy implements DescribedValue {
     APPLICATION_DEFAULT("Application Default Credentials", "Use Google 
Application Default Credentials such as the GOOGLE_APPLICATION_CREDENTIALS 
environment variable or gcloud configuration."),
     SERVICE_ACCOUNT_JSON_FILE("Service Account Credentials (Json File)", "Use 
a Service Account key stored in a JSON file."),
     SERVICE_ACCOUNT_JSON("Service Account Credentials (Json Value)", "Use a 
Service Account key provided directly as JSON."),
+    WORKLOAD_IDENTITY_FEDERATION("Workload Identity Federation", "Exchange 
workload identity tokens using Google Identity Pool credentials."),
     COMPUTE_ENGINE("Compute Engine Credentials", "Use the Compute Engine 
service account available to the NiFi instance.");
 
     private final String displayName;
diff --git 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialPropertyDescriptors.java
 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialPropertyDescriptors.java
index 6a87648f88..a0ef1988fa 100644
--- 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialPropertyDescriptors.java
+++ 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialPropertyDescriptors.java
@@ -20,6 +20,7 @@ import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.components.resource.ResourceCardinality;
 import org.apache.nifi.components.resource.ResourceType;
 import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.oauth2.OAuth2AccessTokenProvider;
 import org.apache.nifi.processor.util.JsonValidator;
 import org.apache.nifi.processor.util.StandardValidators;
 
@@ -79,12 +80,68 @@ public final class CredentialPropertyDescriptors {
             .sensitive(true)
             .build();
 
+    private static final String DEFAULT_WORKLOAD_IDENTITY_SCOPE = 
"https://www.googleapis.com/auth/cloud-platform";;
+    private static final String DEFAULT_WORKLOAD_IDENTITY_TOKEN_ENDPOINT = 
"https://sts.googleapis.com/v1/token";;
+    private static final String DEFAULT_WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE = 
"urn:ietf:params:oauth:token-type:jwt";
+    private static final String SUBJECT_TOKEN_TYPE_ID_TOKEN = 
"urn:ietf:params:oauth:token-type:id_token";
+    private static final String SUBJECT_TOKEN_TYPE_ACCESS_TOKEN = 
"urn:ietf:params:oauth:token-type:access_token";
+
+    public static final PropertyDescriptor WORKLOAD_IDENTITY_AUDIENCE = new 
PropertyDescriptor.Builder()
+            .name("Audience")
+            .description("The audience corresponding to the target Workload 
Identity Provider, typically the full resource name.")
+            .required(true)
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .dependsOn(AUTHENTICATION_STRATEGY, 
AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION.getValue())
+            .build();
+
+    public static final PropertyDescriptor WORKLOAD_IDENTITY_SCOPE = new 
PropertyDescriptor.Builder()
+            .name("Scope")
+            .description("OAuth2 scopes requested for the exchanged access 
token. Multiple scopes can be separated by space or comma.")
+            .required(true)
+            .defaultValue(DEFAULT_WORKLOAD_IDENTITY_SCOPE)
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .dependsOn(AUTHENTICATION_STRATEGY, 
AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION.getValue())
+            .build();
+
+    public static final PropertyDescriptor WORKLOAD_IDENTITY_TOKEN_ENDPOINT = 
new PropertyDescriptor.Builder()
+            .name("STS Token Endpoint")
+            .description("Google Security Token Service endpoint used for 
token exchange.")
+            .required(true)
+            .defaultValue(DEFAULT_WORKLOAD_IDENTITY_TOKEN_ENDPOINT)
+            .addValidator(StandardValidators.URL_VALIDATOR)
+            .dependsOn(AUTHENTICATION_STRATEGY, 
AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION.getValue())
+            .build();
+
+    public static final PropertyDescriptor 
WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER = new PropertyDescriptor.Builder()
+            .name("Subject Token Provider")
+            .description("Controller Service that retrieves the external 
workload identity token to exchange.")
+            .identifiesControllerService(OAuth2AccessTokenProvider.class)
+            .required(true)
+            .dependsOn(AUTHENTICATION_STRATEGY, 
AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION.getValue())
+            .build();
+
+    public static final PropertyDescriptor 
WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE = new PropertyDescriptor.Builder()
+            .name("Subject Token Type")
+            .description("The type of token returned by the Subject Token 
Provider.")
+            .required(true)
+            .defaultValue(DEFAULT_WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE)
+            .allowableValues(
+                    DEFAULT_WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE,
+                    SUBJECT_TOKEN_TYPE_ID_TOKEN,
+                    SUBJECT_TOKEN_TYPE_ACCESS_TOKEN
+            )
+            .dependsOn(AUTHENTICATION_STRATEGY, 
AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION.getValue())
+            .build();
+
     public static final PropertyDescriptor DELEGATION_STRATEGY = new 
PropertyDescriptor.Builder()
             .name("Delegation Strategy")
             .required(true)
             .defaultValue(DelegationStrategy.SERVICE_ACCOUNT)
             .allowableValues(DelegationStrategy.class)
             .description("The Delegation Strategy determines which account is 
used when calls are made with the GCP Credential.")
+            .dependsOn(AUTHENTICATION_STRATEGY,
+                    
AuthenticationStrategy.SERVICE_ACCOUNT_JSON_FILE.getValue(),
+                    AuthenticationStrategy.SERVICE_ACCOUNT_JSON.getValue())
             .build();
 
     public static final PropertyDescriptor DELEGATION_USER = new 
PropertyDescriptor.Builder()
@@ -93,6 +150,9 @@ public final class CredentialPropertyDescriptors {
             .required(true)
             .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
             .dependsOn(DELEGATION_STRATEGY, 
DelegationStrategy.DELEGATED_ACCOUNT)
+            .dependsOn(AUTHENTICATION_STRATEGY,
+                    
AuthenticationStrategy.SERVICE_ACCOUNT_JSON_FILE.getValue(),
+                    AuthenticationStrategy.SERVICE_ACCOUNT_JSON.getValue())
             .description("This user will be impersonated by the service 
account for api calls. " +
                     "API calls made using this credential will appear as if 
they are coming from delegate user with the delegate user's access. " +
                     "Any scopes supplied from processors to this credential 
must have domain-wide delegation setup with the service account.")
diff --git 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsFactory.java
 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsFactory.java
index fb186fea3d..7954400817 100644
--- 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsFactory.java
+++ 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsFactory.java
@@ -19,10 +19,12 @@ package org.apache.nifi.processors.gcp.credentials.factory;
 import com.google.auth.http.HttpTransportFactory;
 import com.google.auth.oauth2.GoogleCredentials;
 import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ConfigurationContext;
 import 
org.apache.nifi.processors.gcp.credentials.factory.strategies.ApplicationDefaultCredentialsStrategy;
 import 
org.apache.nifi.processors.gcp.credentials.factory.strategies.ComputeEngineCredentialsStrategy;
 import 
org.apache.nifi.processors.gcp.credentials.factory.strategies.JsonFileServiceAccountCredentialsStrategy;
 import 
org.apache.nifi.processors.gcp.credentials.factory.strategies.JsonStringServiceAccountCredentialsStrategy;
+import 
org.apache.nifi.processors.gcp.credentials.factory.strategies.WorkloadIdentityFederationCredentialsStrategy;
 
 import java.io.IOException;
 import java.util.EnumMap;
@@ -47,6 +49,7 @@ public class CredentialsFactory {
         
strategiesByAuthentication.put(AuthenticationStrategy.APPLICATION_DEFAULT, new 
ApplicationDefaultCredentialsStrategy());
         
strategiesByAuthentication.put(AuthenticationStrategy.SERVICE_ACCOUNT_JSON_FILE,
 new JsonFileServiceAccountCredentialsStrategy());
         
strategiesByAuthentication.put(AuthenticationStrategy.SERVICE_ACCOUNT_JSON, new 
JsonStringServiceAccountCredentialsStrategy());
+        
strategiesByAuthentication.put(AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION,
 new WorkloadIdentityFederationCredentialsStrategy());
         strategiesByAuthentication.put(AuthenticationStrategy.COMPUTE_ENGINE, 
new ComputeEngineCredentialsStrategy());
     }
 
@@ -71,4 +74,12 @@ public class CredentialsFactory {
         }
         return primaryStrategy.getGoogleCredentials(properties, 
transportFactory);
     }
+
+    public GoogleCredentials getGoogleCredentials(final ConfigurationContext 
context, final HttpTransportFactory transportFactory) throws IOException {
+        final CredentialsStrategy primaryStrategy = 
selectPrimaryStrategy(context.getProperties());
+        if (primaryStrategy == null) {
+            throw new IllegalStateException("No matching authentication 
strategy is configured");
+        }
+        return primaryStrategy.getGoogleCredentials(context, transportFactory);
+    }
 }
diff --git 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsStrategy.java
 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsStrategy.java
index 63b780b50a..9efe0e7161 100644
--- 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsStrategy.java
+++ 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/CredentialsStrategy.java
@@ -19,6 +19,7 @@ package org.apache.nifi.processors.gcp.credentials.factory;
 import com.google.auth.http.HttpTransportFactory;
 import com.google.auth.oauth2.GoogleCredentials;
 import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ConfigurationContext;
 
 import java.io.IOException;
 import java.util.Map;
@@ -43,4 +44,17 @@ public interface CredentialsStrategy {
      * @throws IOException if the provided credentials cannot be accessed or 
are invalid
      */
     GoogleCredentials getGoogleCredentials(Map<PropertyDescriptor, String> 
properties, HttpTransportFactory transportFactory) throws IOException;
+
+    /**
+     * Creates Google Credentials using the supplied ConfigurationContext for 
controller services that need
+     * access to additional controller service references beyond raw property 
values.
+     *
+     * @param context Controller Service configuration context
+     * @param transportFactory Transport factory to be used when accessing 
Google services
+     * @return GoogleCredentials for the configured strategy
+     * @throws IOException on credential creation failures
+     */
+    default GoogleCredentials getGoogleCredentials(ConfigurationContext 
context, HttpTransportFactory transportFactory) throws IOException {
+        return getGoogleCredentials(context.getProperties(), transportFactory);
+    }
 }
diff --git 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/strategies/WorkloadIdentityFederationCredentialsStrategy.java
 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/strategies/WorkloadIdentityFederationCredentialsStrategy.java
new file mode 100644
index 0000000000..15877f156a
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/factory/strategies/WorkloadIdentityFederationCredentialsStrategy.java
@@ -0,0 +1,133 @@
+/*
+ * 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.nifi.processors.gcp.credentials.factory.strategies;
+
+import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.auth.oauth2.IdentityPoolCredentials;
+import com.google.auth.oauth2.IdentityPoolSubjectTokenSupplier;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.oauth2.AccessToken;
+import org.apache.nifi.oauth2.OAuth2AccessTokenProvider;
+import 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Credentials strategy that configures Workload Identity Federation using 
Google Identity Pool credentials.
+ */
+public class WorkloadIdentityFederationCredentialsStrategy extends 
AbstractCredentialsStrategy {
+
+    private static final String ERROR_NO_SUBJECT_TOKEN = "Subject token 
provider returned no usable token";
+    private static final String SUBJECT_TOKEN_PARAMETER_ID_TOKEN = "id_token";
+    private static final String SUBJECT_TOKEN_PARAMETER_ACCESS_TOKEN = 
"access_token";
+
+    public WorkloadIdentityFederationCredentialsStrategy() {
+        super("Workload Identity Federation");
+    }
+
+    @Override
+    public GoogleCredentials getGoogleCredentials(final 
Map<PropertyDescriptor, String> properties, final HttpTransportFactory 
transportFactory) {
+        throw new UnsupportedOperationException("Workload Identity Federation 
requires the controller service configuration context");
+    }
+
+    @Override
+    public GoogleCredentials getGoogleCredentials(final ConfigurationContext 
context, final HttpTransportFactory transportFactory) throws IOException {
+        final OAuth2AccessTokenProvider subjectTokenProvider = 
context.getProperty(CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER)
+                .asControllerService(OAuth2AccessTokenProvider.class);
+        final String audience = 
context.getProperty(CredentialPropertyDescriptors.WORKLOAD_IDENTITY_AUDIENCE).getValue();
+        final String scopeValue = 
context.getProperty(CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SCOPE).getValue();
+        final String tokenEndpoint = 
context.getProperty(CredentialPropertyDescriptors.WORKLOAD_IDENTITY_TOKEN_ENDPOINT).getValue();
+        final String subjectTokenType = 
context.getProperty(CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE).getValue();
+
+        final List<String> scopes = parseScopes(scopeValue);
+        final IdentityPoolSubjectTokenSupplier tokenSupplier = 
createSubjectTokenSupplier(subjectTokenProvider);
+        final IdentityPoolCredentials.Builder builder = 
IdentityPoolCredentials.newBuilder()
+                .setAudience(audience)
+                .setTokenUrl(tokenEndpoint)
+                .setSubjectTokenType(subjectTokenType)
+                .setSubjectTokenSupplier(tokenSupplier);
+
+        if (!scopes.isEmpty()) {
+            builder.setScopes(scopes);
+        }
+
+        if (transportFactory != null) {
+            builder.setHttpTransportFactory(transportFactory);
+        }
+
+        return builder.build();
+    }
+
+    private IdentityPoolSubjectTokenSupplier createSubjectTokenSupplier(final 
OAuth2AccessTokenProvider tokenProvider) {
+        return context -> getSubjectToken(tokenProvider);
+    }
+
+    private String getSubjectToken(final OAuth2AccessTokenProvider 
tokenProvider) throws IOException {
+        final AccessToken subjectToken = tokenProvider.getAccessDetails();
+
+        final String subjectTokenValue = extractTokenValue(subjectToken);
+        if (StringUtils.isBlank(subjectTokenValue)) {
+            throw new IOException(ERROR_NO_SUBJECT_TOKEN);
+        }
+
+        return subjectTokenValue;
+    }
+
+    private String extractTokenValue(final AccessToken subjectToken) {
+        if (subjectToken == null) {
+            return null;
+        }
+
+        final Map<String, Object> additionalParameters = 
subjectToken.getAdditionalParameters();
+        if (additionalParameters != null) {
+            final Object idToken = 
additionalParameters.get(SUBJECT_TOKEN_PARAMETER_ID_TOKEN);
+            if (idToken instanceof String && StringUtils.isNotBlank((String) 
idToken)) {
+                return (String) idToken;
+            }
+
+            final Object accessToken = 
additionalParameters.get(SUBJECT_TOKEN_PARAMETER_ACCESS_TOKEN);
+            if (accessToken instanceof String && 
StringUtils.isNotBlank((String) accessToken)) {
+                return (String) accessToken;
+            }
+        }
+
+        final String accessTokenValue = subjectToken.getAccessToken();
+        if (StringUtils.isNotBlank(accessTokenValue)) {
+            return accessTokenValue;
+        }
+
+        return null;
+    }
+
+    private static List<String> parseScopes(final String scopeValue) {
+        if (StringUtils.isBlank(scopeValue)) {
+            return Collections.emptyList();
+        }
+
+        return Arrays.stream(scopeValue.split("[\\s,]+"))
+                .filter(StringUtils::isNotBlank)
+                .collect(Collectors.toList());
+    }
+}
diff --git 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsControllerService.java
 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsControllerService.java
index 5a2227112a..1c15e26e5f 100644
--- 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsControllerService.java
+++ 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsControllerService.java
@@ -57,6 +57,11 @@ import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPrope
 import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.LEGACY_USE_COMPUTE_ENGINE_CREDENTIALS;
 import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.SERVICE_ACCOUNT_JSON;
 import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.SERVICE_ACCOUNT_JSON_FILE;
+import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_AUDIENCE;
+import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SCOPE;
+import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER;
+import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE;
+import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_TOKEN_ENDPOINT;
 
 /**
  * Implementation of GCPCredentialsService interface
@@ -83,6 +88,11 @@ public class GCPCredentialsControllerService extends 
AbstractControllerService i
             AUTHENTICATION_STRATEGY,
             SERVICE_ACCOUNT_JSON_FILE,
             SERVICE_ACCOUNT_JSON,
+            WORKLOAD_IDENTITY_AUDIENCE,
+            WORKLOAD_IDENTITY_SCOPE,
+            WORKLOAD_IDENTITY_TOKEN_ENDPOINT,
+            WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER,
+            WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE,
             
ProxyConfiguration.createProxyConfigPropertyDescriptor(ProxyAwareTransportFactory.PROXY_SPECS),
             DELEGATION_STRATEGY,
             DELEGATION_USER
@@ -103,7 +113,7 @@ public class GCPCredentialsControllerService extends 
AbstractControllerService i
 
     @Override
     protected Collection<ValidationResult> customValidate(final 
ValidationContext validationContext) {
-        final Collection<ValidationResult> results = new ArrayList<>();
+        final List<ValidationResult> results = new ArrayList<>();
         ProxyConfiguration.validateProxySpec(validationContext, results, 
ProxyAwareTransportFactory.PROXY_SPECS);
         return results;
     }
@@ -167,6 +177,8 @@ public class GCPCredentialsControllerService extends 
AbstractControllerService i
             return AuthenticationStrategy.APPLICATION_DEFAULT;
         } else if (config.isPropertySet(SERVICE_ACCOUNT_JSON_FILE)) {
             return AuthenticationStrategy.SERVICE_ACCOUNT_JSON_FILE;
+        } else if 
(config.isPropertySet(WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER)) {
+            return AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION;
         } else if (config.isPropertySet(SERVICE_ACCOUNT_JSON)) {
             return AuthenticationStrategy.SERVICE_ACCOUNT_JSON;
         } else if (isTrue(config, LEGACY_USE_COMPUTE_ENGINE_CREDENTIALS)) {
@@ -184,7 +196,7 @@ public class GCPCredentialsControllerService extends 
AbstractControllerService i
     private GoogleCredentials getGoogleCredentials(final ConfigurationContext 
context) throws IOException {
         final ProxyConfiguration proxyConfiguration = 
ProxyConfiguration.getConfiguration(context);
         final HttpTransportFactory transportFactory = new 
ProxyAwareTransportFactory(proxyConfiguration);
-        return 
credentialsProviderFactory.getGoogleCredentials(context.getProperties(), 
transportFactory);
+        return credentialsProviderFactory.getGoogleCredentials(context, 
transportFactory);
     }
 
     @Override
diff --git 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
index e5c9970ebc..f4fa86f59e 100644
--- 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
+++ 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
@@ -13,4 +13,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
org.apache.nifi.processors.gcp.credentials.service.GCPCredentialsControllerService
-org.apache.nifi.processors.gcp.storage.GCSFileResourceService
\ No newline at end of file
+org.apache.nifi.processors.gcp.storage.GCSFileResourceService
diff --git 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/resources/docs/org.apache.nifi.processors.gcp.credentials.service.GCPCredentialsControllerService/additionalDetails.md
 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/resources/docs/org.apache.nifi.processors.gcp.credentials.service.GCPCredentialsControllerService/additionalDetails.md
new file mode 100644
index 0000000000..fc8d0b6acf
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/resources/docs/org.apache.nifi.processors.gcp.credentials.service.GCPCredentialsControllerService/additionalDetails.md
@@ -0,0 +1,165 @@
+<!--
+  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.
+-->
+
+# GCPCredentialsControllerService
+
+The **GCPCredentialsControllerService** centralizes all authentication 
strategies used by NiFi components that
+interact with Google Cloud. Each strategy exposes only the properties it 
requires, which lets administrators swap
+approaches without touching downstream processors. This guide summarizes every 
supported strategy.
+
+---
+
+## Application Default Credentials
+
+Application Default Credentials (ADC) allow NiFi to inherit credentials 
exposed through the runtime environment,
+including:
+- The `GOOGLE_APPLICATION_CREDENTIALS` environment variable referencing a 
service-account key file
+- `gcloud auth application-default login` on development machines
+- Cloud Shell or other Google-managed environments that inject ADC 
automatically
+
+No extra properties are required. Confirm that the account supplying the ADC 
token has the IAM roles needed by the
+processors referencing this controller service.
+
+---
+
+## Service Account Credentials (JSON File)
+
+Use this strategy when the service-account key material is stored on disk. 
Configure the **Service Account JSON
+File** property to point at the JSON key file. NiFi reads the file when the 
controller service is enabled and
+caches the Google credentials.
+
+**Best practices**
+- Restrict filesystem permissions so only the NiFi service user can read the 
key.
+- Rotate keys regularly and delete unused keys from the Google Cloud Console.
+- When impersonating a domain user, set **Delegation Strategy** to *Delegated 
Account* and provide **Delegation
+  User** so that NiFi calls Google APIs on behalf of that user.
+
+---
+
+## Service Account Credentials (JSON Value)
+
+This strategy embeds the entire service-account JSON document directly inside 
the controller-service property. The
+value is marked sensitive and can be injected through Parameter Contexts to 
separate credentials from flow
+definitions.
+
+**Best practices**
+- Store the JSON value in a Parameter Context referenced by this property so 
you can swap credentials per
+  environment.
+- Use NiFi’s Sensitive Property encryption in `nifi.properties` to encrypt the 
stored JSON on disk.
+
+---
+
+## Compute Engine Credentials
+
+Select **Compute Engine Credentials** when NiFi runs on a Google-managed 
runtime (Compute Engine, GKE, etc.) and
+should use the instance’s attached service account. Google automatically 
refreshes the metadata server tokens, so
+no additional properties are required.
+
+**Best practices**
+- Grant the instance service account only the roles required by your flows.
+- If multiple NiFi nodes share the same instance template, verify that all 
nodes have access to the same IAM
+  permissions or configure Workload Identity Federation for finer control.
+
+---
+
+## Workload Identity Federation
+
+Workload Identity Federation (WIF) exchanges an external identity-provider 
token for a short-lived Google Cloud
+access token via Google’s Security Token Service (STS). The controller service 
configures Google’s
+`IdentityPoolCredentials`, allowing Google client libraries to refresh Google 
Cloud tokens automatically.
+
+### 1. Configure Workload Identity Federation in Google Cloud
+
+```bash
+# Create a pool (only once per environment)
+gcloud iam workload-identity-pools create nifi-pool \
+  --project=MY_PROJECT_ID \
+  --location=global \
+  --display-name="NiFi Pool"
+
+# Create a provider bound to your IdP (example for OIDC named myidp)
+gcloud iam workload-identity-pools providers create-oidc myidp \
+  --project=MY_PROJECT_ID \
+  --location=global \
+  --workload-identity-pool=nifi-pool \
+  --display-name="My Identity Provider" \
+  --issuer-uri="https://identity.myidp.com/oauth2/..."; \
+  
--allowed-audiences="//iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/nifi-pool/providers/myidp"
 \
+  --attribute-mapping="google.subject=assertion.sub"
+```
+
+Record the audience string printed by the command; it must be copied into 
NiFi’s **Audience** property exactly:
+
+```
+//iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/nifi-pool/providers/myidp
+```
+
+### 2. Authorize the workload identity principal for Google Cloud resources
+
+The STS-issued access token represents the workload identity principal itself. 
Grant IAM roles to that identity on
+projects or specific resources:
+
+```bash
+# Project scoped
+gcloud projects add-iam-policy-binding MY_PROJECT_ID \
+  
--member="principal://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/nifi-pool/subject/IDENTITY_SUBJECT"
 \
+  --role="roles/storage.objectViewer"
+
+# Bucket scoped (example)
+gcloud storage buckets add-iam-policy-binding gs://MY_BUCKET \
+  
--member="principal://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/nifi-pool/subject/IDENTITY_SUBJECT"
 \
+  --role="roles/storage.objectViewer"
+```
+
+`IDENTITY_SUBJECT` must match the claim you mapped in the provider (for 
example `assertion.sub`). Service-account
+impersonation is not yet supported, so grant roles directly to the workload 
identity principal.
+
+### 3. Configure NiFi properties (Workload Identity strategy selected)
+
+| Property | Guidance |
+| --- | --- |
+| **Audience** | Paste the provider resource name recorded above. |
+| **Scope** | Defaults to `https://www.googleapis.com/auth/cloud-platform`; 
supply space- or comma-separated scopes if you need fewer permissions. |
+| **STS Token Endpoint** | Optional override for the Google STS endpoint; 
leave blank to use `https://sts.googleapis.com/v1/token`. |
+| **Subject Token Provider** | Controller Service that retrieves the upstream 
workload identity token (JWT or access token). The token must contain the 
claims referenced by your attribute mapping. |
+| **Subject Token Type** | Defaults to `urn:ietf:params:oauth:token-type:jwt`. 
Choose the alternate access-token type only when the upstream provider issues 
OAuth access tokens instead of JWTs. |
+| **Proxy Configuration Service** | Optional controller service allowing NiFi 
to reach STS through HTTP/SOCKS proxies. |
+
+Once these properties are set, enable GCPCredentialsControllerService. 
Processors referencing it immediately obtain
+`IdentityPoolCredentials`, and Google’s libraries refresh access tokens 
automatically using the configured subject
+-token provider.
+
+### Verification workflow
+
+1. Enable or refresh the Subject Token Provider controller service.
+2. Use the **Verify** action on GCPCredentialsControllerService. Successful 
verification confirms that NiFi can
+   exchange the subject token with Google STS using the configured proxy, 
audience, and scopes.
+3. Enable dependent processors. No additional controller services are required.
+
+### Troubleshooting
+
+| Symptom | Guidance |
+| --- | --- |
+| `403 Caller does not have storage.objects.list` | Confirm the workload 
identity principal has the required IAM role: `gcloud projects get-iam-policy` 
/ `gcloud storage buckets get-iam-policy`. Ensure the attribute mapping emits 
the same subject referenced in IAM. |
+| STS errors during verification | Double-check the **Audience** string and 
**STS Token Endpoint**. Use DEBUG logs or the Verify dialog output to inspect 
the STS response. Ensure the subject token includes the mapped claims. |
+| Access token rejected by Google APIs | Call the API directly with the 
federated token (for example, `curl -H "Authorization: Bearer TOKEN" 
https://storage.googleapis.com/...`). If it still fails, revisit IAM bindings 
or scope selection. |
+| Need to rotate upstream tokens | The controller service requests a fresh 
subject token 60 seconds before expiry. Trigger **Refresh** on the Subject 
Token Provider to invalidate cached tokens immediately. |
+
+---
+
+With every strategy available from a single controller-service configuration, 
NiFi users can migrate between
+service-account keys, Compute Engine, Application Default Credentials, and 
Workload Identity Federation without
+introducing new controller services or updating processor properties. Adjust 
the authentication strategy once and
+all dependent processors automatically pick up the new credentials.
diff --git 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/test/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsServiceTest.java
 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/test/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsServiceTest.java
index ab04129260..6c68288251 100644
--- 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/test/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsServiceTest.java
+++ 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/test/java/org/apache/nifi/processors/gcp/credentials/service/GCPCredentialsServiceTest.java
@@ -17,8 +17,12 @@
 package org.apache.nifi.processors.gcp.credentials.service;
 
 import com.google.auth.oauth2.GoogleCredentials;
+import com.google.auth.oauth2.IdentityPoolCredentials;
 import com.google.auth.oauth2.ServiceAccountCredentials;
+import org.apache.nifi.controller.AbstractControllerService;
 import org.apache.nifi.gcp.credentials.service.GCPCredentialsService;
+import org.apache.nifi.oauth2.AccessToken;
+import org.apache.nifi.oauth2.OAuth2AccessTokenProvider;
 import 
org.apache.nifi.processors.gcp.credentials.factory.AuthenticationStrategy;
 import org.apache.nifi.util.TestRunner;
 import org.apache.nifi.util.TestRunners;
@@ -30,6 +34,11 @@ import java.nio.file.Paths;
 import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.AUTHENTICATION_STRATEGY;
 import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.SERVICE_ACCOUNT_JSON;
 import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.SERVICE_ACCOUNT_JSON_FILE;
+import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_AUDIENCE;
+import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SCOPE;
+import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER;
+import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE;
+import static 
org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_TOKEN_ENDPOINT;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
@@ -103,6 +112,35 @@ public class GCPCredentialsServiceTest {
                 "Credentials class should be equal");
     }
 
+    @Test
+    public void testWorkloadIdentityFederationCredentials() throws Exception {
+        final TestRunner runner = 
TestRunners.newTestRunner(MockCredentialsServiceProcessor.class);
+        final GCPCredentialsControllerService serviceImpl = new 
GCPCredentialsControllerService();
+        runner.addControllerService("gcpCredentialsProvider", serviceImpl);
+
+        final MockOAuth2AccessTokenProvider subjectTokenProvider = new 
MockOAuth2AccessTokenProvider();
+        runner.addControllerService("subjectTokenProvider", 
subjectTokenProvider);
+        runner.enableControllerService(subjectTokenProvider);
+
+        runner.setProperty(serviceImpl, AUTHENTICATION_STRATEGY, 
AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION.getValue());
+        runner.setProperty(serviceImpl, WORKLOAD_IDENTITY_AUDIENCE, 
"projects/123456789/locations/global/workloadIdentityPools/pool/providers/provider");
+        runner.setProperty(serviceImpl, WORKLOAD_IDENTITY_SCOPE, 
"https://www.googleapis.com/auth/cloud-platform";);
+        runner.setProperty(serviceImpl, WORKLOAD_IDENTITY_TOKEN_ENDPOINT, 
"https://sts.googleapis.com/v1/token";);
+        runner.setProperty(serviceImpl, WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE, 
"urn:ietf:params:oauth:token-type:jwt");
+        runner.setProperty(serviceImpl, 
WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER, "subjectTokenProvider");
+
+        runner.enableControllerService(serviceImpl);
+        runner.assertValid(serviceImpl);
+
+        final GCPCredentialsService service = (GCPCredentialsService) 
runner.getProcessContext()
+                
.getControllerServiceLookup().getControllerService("gcpCredentialsProvider");
+
+        assertNotNull(service);
+        final GoogleCredentials credentials = service.getGoogleCredentials();
+        assertNotNull(credentials);
+        assertEquals(IdentityPoolCredentials.class, credentials.getClass());
+    }
+
     @Test
     public void testBadFileCredentials() throws Exception {
         final TestRunner runner = 
TestRunners.newTestRunner(MockCredentialsServiceProcessor.class);
@@ -156,4 +194,22 @@ public class GCPCredentialsServiceTest {
         assertEquals(ServiceAccountCredentials.class, credentials.getClass(),
                 "Credentials class should be equal");
     }
+
+    private static final class MockOAuth2AccessTokenProvider extends 
AbstractControllerService implements OAuth2AccessTokenProvider {
+        private static final String ACCESS_TOKEN_VALUE = 
"federated-access-token";
+        private static final long EXPIRES_IN_SECONDS = 3600;
+
+        @Override
+        public AccessToken getAccessDetails() {
+            final AccessToken accessToken = new AccessToken();
+            accessToken.setAccessToken(ACCESS_TOKEN_VALUE);
+            accessToken.setExpiresIn(EXPIRES_IN_SECONDS);
+            return accessToken;
+        }
+
+        @Override
+        public void refreshAccessDetails() {
+            // Intentionally left blank for test implementation
+        }
+    }
 }
diff --git 
a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-services-api/pom.xml 
b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-services-api/pom.xml
index da59a49a1a..c94ee7f274 100644
--- a/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-services-api/pom.xml
+++ b/nifi-extension-bundles/nifi-gcp-bundle/nifi-gcp-services-api/pom.xml
@@ -26,7 +26,6 @@
     <packaging>jar</packaging>
 
     <dependencies>
-
         <dependency>
             <groupId>com.google.auth</groupId>
             <artifactId>google-auth-library-oauth2-http</artifactId>


Reply via email to