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