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

exceptionfactory 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 157ebb2327 NIFI-15021 Support custom form parameters in 
StandardOauth2AccessTokenProvider (#10352)
157ebb2327 is described below

commit 157ebb2327d53935812133918779f0b02f557d8f
Author: Pierre Villard <[email protected]>
AuthorDate: Tue Oct 7 00:34:26 2025 +0200

    NIFI-15021 Support custom form parameters in 
StandardOauth2AccessTokenProvider (#10352)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../oauth2/StandardOauth2AccessTokenProvider.java  | 52 ++++++++++++++++++++++
 .../StandardOauth2AccessTokenProviderTest.java     | 46 +++++++++++++++++++
 2 files changed, 98 insertions(+)

diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/main/java/org/apache/nifi/oauth2/StandardOauth2AccessTokenProvider.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/main/java/org/apache/nifi/oauth2/StandardOauth2AccessTokenProvider.java
index d6388b05f7..2e9768ea55 100644
--- 
a/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/main/java/org/apache/nifi/oauth2/StandardOauth2AccessTokenProvider.java
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/main/java/org/apache/nifi/oauth2/StandardOauth2AccessTokenProvider.java
@@ -26,6 +26,9 @@ import okhttp3.OkHttpClient;
 import okhttp3.Request;
 import okhttp3.RequestBody;
 import okhttp3.Response;
+import org.apache.nifi.annotation.behavior.DynamicProperties;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+import org.apache.nifi.annotation.behavior.SupportsSensitiveDynamicProperties;
 import org.apache.nifi.annotation.documentation.CapabilityDescription;
 import org.apache.nifi.annotation.documentation.Tags;
 import org.apache.nifi.annotation.lifecycle.OnDisabled;
@@ -39,6 +42,7 @@ import org.apache.nifi.components.Validator;
 import org.apache.nifi.controller.AbstractControllerService;
 import org.apache.nifi.controller.ConfigurationContext;
 import org.apache.nifi.controller.VerifiableControllerService;
+import org.apache.nifi.expression.AttributeExpression;
 import org.apache.nifi.expression.ExpressionLanguageScope;
 import org.apache.nifi.logging.ComponentLog;
 import org.apache.nifi.migration.PropertyConfiguration;
@@ -58,14 +62,24 @@ import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
+@SupportsSensitiveDynamicProperties
 @Tags({"oauth2", "provider", "authorization", "access token", "http"})
 @CapabilityDescription("Provides OAuth 2.0 access tokens that can be used as 
Bearer authorization header in HTTP requests." +
     " Can use either Resource Owner Password Credentials Grant or Client 
Credentials Grant." +
     " Client authentication can be done with either HTTP Basic authentication 
or in the request body.")
+@DynamicProperties({
+    @DynamicProperty(
+        name = "FORM.Request parameter name",
+        value = "Request parameter value",
+        expressionLanguageScope = ExpressionLanguageScope.ENVIRONMENT,
+        description = "Custom parameters that should be added to the body of 
the request against the token endpoint."
+    )
+})
 public class StandardOauth2AccessTokenProvider extends 
AbstractControllerService implements OAuth2AccessTokenProvider, 
VerifiableControllerService {
     public static final PropertyDescriptor AUTHORIZATION_SERVER_URL = new 
PropertyDescriptor.Builder()
         .name("Authorization Server URL")
@@ -218,6 +232,7 @@ public class StandardOauth2AccessTokenProvider extends 
AbstractControllerService
     );
 
     private static final String AUTHORIZATION_HEADER = "Authorization";
+    private static final String FORM_PREFIX = "FORM.";
 
     public static final ObjectMapper ACCESS_DETAILS_MAPPER = new ObjectMapper()
         .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
@@ -237,6 +252,7 @@ public class StandardOauth2AccessTokenProvider extends 
AbstractControllerService
     private volatile String resource;
     private volatile String audience;
     private volatile long refreshWindowSeconds;
+    private volatile Map<String, String> customFormParameters = new 
HashMap<>();
 
     private volatile AccessToken accessDetails;
 
@@ -262,6 +278,22 @@ public class StandardOauth2AccessTokenProvider extends 
AbstractControllerService
         return PROPERTY_DESCRIPTORS;
     }
 
+    @Override
+    protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final 
String propertyDescriptorName) {
+        if (propertyDescriptorName.startsWith(FORM_PREFIX)) {
+            return new PropertyDescriptor.Builder()
+                .required(false)
+                .name(propertyDescriptorName)
+                .description("The value of the form parameter to add to the 
request body.")
+                
.addValidator(StandardValidators.createAttributeExpressionLanguageValidator(AttributeExpression.ResultType.STRING,
 true))
+                .dynamic(true)
+                
.expressionLanguageSupported(ExpressionLanguageScope.ENVIRONMENT)
+                .build();
+        }
+
+        return null;
+    }
+
     @OnEnabled
     public void onEnabled(ConfigurationContext context) {
         getProperties(context);
@@ -396,6 +428,25 @@ public class StandardOauth2AccessTokenProvider extends 
AbstractControllerService
         }
 
         refreshWindowSeconds = 
context.getProperty(REFRESH_WINDOW).asTimePeriod(TimeUnit.SECONDS);
+
+        Map<String, String> formParameters = new HashMap<>();
+        for (PropertyDescriptor descriptor : context.getProperties().keySet()) 
{
+            if (!descriptor.isDynamic() || 
!descriptor.getName().startsWith(FORM_PREFIX)) {
+                continue;
+            }
+
+            String parameterName = 
descriptor.getName().substring(FORM_PREFIX.length());
+            if (parameterName.isEmpty()) {
+                continue;
+            }
+
+            String evaluatedValue = 
context.getProperty(descriptor).evaluateAttributeExpressions().getValue();
+            if (evaluatedValue != null) {
+                formParameters.put(parameterName, evaluatedValue);
+            }
+        }
+
+        customFormParameters = formParameters;
     }
 
     private boolean isRefreshRequired() {
@@ -438,6 +489,7 @@ public class StandardOauth2AccessTokenProvider extends 
AbstractControllerService
         if (audience != null) {
             formBuilder.add("audience", audience);
         }
+        customFormParameters.forEach(formBuilder::add);
     }
 
     private AccessToken requestToken(FormBody.Builder formBuilder) {
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/test/java/org/apache/nifi/oauth2/StandardOauth2AccessTokenProviderTest.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/test/java/org/apache/nifi/oauth2/StandardOauth2AccessTokenProviderTest.java
index c55d7c86ca..da1e43dad7 100644
--- 
a/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/test/java/org/apache/nifi/oauth2/StandardOauth2AccessTokenProviderTest.java
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/test/java/org/apache/nifi/oauth2/StandardOauth2AccessTokenProviderTest.java
@@ -25,12 +25,15 @@ import okhttp3.Response;
 import okhttp3.ResponseBody;
 import okio.Buffer;
 import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.controller.ConfigurationContext;
 import org.apache.nifi.controller.VerifiableControllerService;
+import org.apache.nifi.expression.ExpressionLanguageScope;
 import org.apache.nifi.logging.ComponentLog;
 import org.apache.nifi.processor.Processor;
 import org.apache.nifi.processor.exception.ProcessException;
 import org.apache.nifi.util.NoOpProcessor;
+import org.apache.nifi.util.MockPropertyValue;
 import org.apache.nifi.util.TestRunner;
 import org.apache.nifi.util.TestRunners;
 import org.junit.jupiter.api.BeforeEach;
@@ -49,7 +52,9 @@ import java.nio.charset.Charset;
 import java.util.Arrays;
 import java.util.Base64;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -123,6 +128,7 @@ public class StandardOauth2AccessTokenProviderTest {
         
when(mockContext.getProperty(StandardOauth2AccessTokenProvider.AUDIENCE).getValue()).thenReturn(AUDIENCE);
         
when(mockContext.getProperty(StandardOauth2AccessTokenProvider.REFRESH_WINDOW).asTimePeriod(eq(TimeUnit.SECONDS))).thenReturn(FIVE_MINUTES);
         
when(mockContext.getProperty(StandardOauth2AccessTokenProvider.CLIENT_AUTHENTICATION_STRATEGY).getValue()).thenReturn(ClientAuthenticationStrategy.BASIC_AUTHENTICATION.getValue());
+        when(mockContext.getProperties()).thenReturn(Collections.emptyMap());
     }
 
     @Nested
@@ -411,6 +417,46 @@ public class StandardOauth2AccessTokenProviderTest {
             assertEquals(expected, 
buffer.readString(Charset.defaultCharset()));
         }
 
+        @Test
+        public void testRequestBodyFormDataIncludesCustomParameters() throws 
Exception {
+            PropertyDescriptor accountIdDescriptor = new 
PropertyDescriptor.Builder()
+                .name("FORM.account_id")
+                .dynamic(true)
+                
.expressionLanguageSupported(ExpressionLanguageScope.ENVIRONMENT)
+                .build();
+
+            
when(mockContext.getProperty(StandardOauth2AccessTokenProvider.GRANT_TYPE).getValue()).thenReturn(StandardOauth2AccessTokenProvider.CLIENT_CREDENTIALS_GRANT_TYPE.getValue());
+            
when(mockContext.getProperty(StandardOauth2AccessTokenProvider.CLIENT_AUTHENTICATION_STRATEGY).getValue()).thenReturn(ClientAuthenticationStrategy.REQUEST_BODY.getValue());
+            Map<PropertyDescriptor, String> properties = new HashMap<>();
+            properties.put(accountIdDescriptor, "12345");
+            when(mockContext.getProperties()).thenReturn(properties);
+            when(mockContext.getProperty(accountIdDescriptor)).thenReturn(new 
MockPropertyValue("12345"));
+
+            testSubject.onEnabled(mockContext);
+
+            Response response = buildResponse(HTTP_OK, 
"{\"access_token\":\"foobar\"}");
+            
when(mockHttpClient.newCall(any(Request.class)).execute()).thenReturn(response);
+
+            testSubject.getAccessDetails();
+
+            verify(mockHttpClient, 
atLeast(1)).newCall(requestCaptor.capture());
+            FormBody formBody = (FormBody) requestCaptor.getValue().body();
+            assertNotNull(formBody);
+
+            Map<String, String> parameters = new HashMap<>();
+            for (int i = 0; i < formBody.size(); i++) {
+                parameters.put(formBody.encodedName(i), 
formBody.encodedValue(i));
+            }
+
+            assertEquals("client_credentials", parameters.get("grant_type"));
+            assertEquals(CLIENT_ID, parameters.get("client_id"));
+            assertEquals(CLIENT_SECRET, parameters.get("client_secret"));
+            assertEquals(SCOPE, parameters.get("scope"));
+            assertEquals(RESOURCE, parameters.get("resource"));
+            assertEquals(AUDIENCE, parameters.get("audience"));
+            assertEquals("12345", parameters.get("account_id"));
+        }
+
         @Test
         public void testIOExceptionDuringRefreshAndSubsequentAcquire() throws 
Exception {
             // GIVEN

Reply via email to