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 d3c3d99482 NIFI-14389 Added OAuth 2 Token Refresh Strategy to 
InvokeHTTP (#9822)
d3c3d99482 is described below

commit d3c3d99482d6ecdc0579ed839e94350600b70918
Author: Pierre Villard <[email protected]>
AuthorDate: Thu Mar 27 17:34:58 2025 +0100

    NIFI-14389 Added OAuth 2 Token Refresh Strategy to InvokeHTTP (#9822)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../nifi/processors/standard/InvokeHTTP.java       | 22 ++++++++++++
 .../nifi/processors/standard/InvokeHTTPTest.java   | 38 ++++++++++++++++++++
 .../nifi/oauth2/OAuth2AccessTokenProvider.java     |  8 +++++
 ...okenProvider.java => TokenRefreshStrategy.java} | 36 ++++++++++++++-----
 .../oauth2/StandardOauth2AccessTokenProvider.java  | 42 ++++++++++++----------
 5 files changed, 119 insertions(+), 27 deletions(-)

diff --git 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/InvokeHTTP.java
 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/InvokeHTTP.java
index 59d0ad3bfc..0d8320c270 100644
--- 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/InvokeHTTP.java
+++ 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/InvokeHTTP.java
@@ -64,6 +64,7 @@ import org.apache.nifi.flowfile.attributes.CoreAttributes;
 import org.apache.nifi.logging.ComponentLog;
 import org.apache.nifi.migration.PropertyConfiguration;
 import org.apache.nifi.oauth2.OAuth2AccessTokenProvider;
+import org.apache.nifi.oauth2.TokenRefreshStrategy;
 import org.apache.nifi.processor.AbstractProcessor;
 import org.apache.nifi.processor.DataUnit;
 import org.apache.nifi.processor.ProcessContext;
@@ -88,6 +89,7 @@ import javax.annotation.Nullable;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.X509TrustManager;
+
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
@@ -267,6 +269,15 @@ public class InvokeHTTP extends AbstractProcessor {
             .required(false)
             .build();
 
+    public static final PropertyDescriptor REQUEST_OAUTH2_REFRESH_TOKEN = new 
PropertyDescriptor.Builder()
+            .name("OAuth2 Access Token Refresh Strategy")
+            .description("Specifies which strategy should be used to refresh 
the OAuth2 Access Token.")
+            .required(false)
+            .defaultValue(TokenRefreshStrategy.ON_TOKEN_EXPIRATION)
+            .allowableValues(TokenRefreshStrategy.class)
+            .dependsOn(REQUEST_OAUTH2_ACCESS_TOKEN_PROVIDER)
+            .build();
+
     public static final PropertyDescriptor REQUEST_USERNAME = new 
PropertyDescriptor.Builder()
             .name("Request Username")
             .description("The username provided for authentication of HTTP 
requests. Encoded using Base64 for HTTP Basic Authentication as described in 
RFC 7617.")
@@ -495,6 +506,7 @@ public class InvokeHTTP extends AbstractProcessor {
             SOCKET_IDLE_CONNECTIONS,
             PROXY_CONFIGURATION_SERVICE,
             REQUEST_OAUTH2_ACCESS_TOKEN_PROVIDER,
+            REQUEST_OAUTH2_REFRESH_TOKEN,
             REQUEST_USERNAME,
             REQUEST_PASSWORD,
             REQUEST_DIGEST_AUTHENTICATION_ENABLED,
@@ -1219,6 +1231,16 @@ public class InvokeHTTP extends AbstractProcessor {
 
             // 1xx, 3xx, 4xx -> NO RETRY
         } else {
+            final TokenRefreshStrategy tokenRefreshStrategy = 
context.getProperty(REQUEST_OAUTH2_REFRESH_TOKEN).asAllowableValue(TokenRefreshStrategy.class);
+            if (oauth2AccessTokenProviderOptional.isPresent()
+                    && TokenRefreshStrategy.ON_UNAUTHORIZED_RESPONSE == 
tokenRefreshStrategy
+                    && statusCode == 401) {
+                // we are using oauth2 and we got a 401 response
+                // it may be because the token has been revoked even though it 
has not expired
+                // yet, so we force the token to be refreshed if configured to 
do so
+                oauth2AccessTokenProviderOptional.get().refreshAccessDetails();
+            }
+
             if (request != null) {
                 if 
(context.getProperty(REQUEST_FAILURE_PENALIZATION_ENABLED).asBoolean()) {
                     request = session.penalize(request);
diff --git 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/InvokeHTTPTest.java
 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/InvokeHTTPTest.java
index a92f7adbd0..2c1015e325 100644
--- 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/InvokeHTTPTest.java
+++ 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/InvokeHTTPTest.java
@@ -25,6 +25,7 @@ import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.flowfile.attributes.CoreAttributes;
 import org.apache.nifi.oauth2.OAuth2AccessTokenProvider;
+import org.apache.nifi.oauth2.TokenRefreshStrategy;
 import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.processor.util.URLValidator;
 import org.apache.nifi.processors.standard.http.ContentEncodingStrategy;
@@ -60,6 +61,7 @@ import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.X509ExtendedTrustManager;
 import javax.security.auth.x500.X500Principal;
+
 import java.io.IOException;
 import java.net.Proxy;
 import java.net.URI;
@@ -963,6 +965,42 @@ public class InvokeHTTPTest {
 
     }
 
+    @Test
+    public void testOAuth2WithForcedRefresh() throws Exception {
+        String accessToken = "access_token";
+        String refreshedAccessToken = "refreshed_access_token";
+        String oauth2AccessTokenProviderId = "oauth2AccessTokenProviderId";
+
+        OAuth2AccessTokenProvider oauth2AccessTokenProvider = 
mock(OAuth2AccessTokenProvider.class, Answers.RETURNS_DEEP_STUBS);
+        
when(oauth2AccessTokenProvider.getIdentifier()).thenReturn(oauth2AccessTokenProviderId);
+        
when(oauth2AccessTokenProvider.getAccessDetails().getAccessToken()).thenReturn(accessToken);
+
+        runner.addControllerService(oauth2AccessTokenProviderId, 
oauth2AccessTokenProvider);
+        runner.enableControllerService(oauth2AccessTokenProvider);
+        runner.setProperty(InvokeHTTP.REQUEST_OAUTH2_ACCESS_TOKEN_PROVIDER, 
oauth2AccessTokenProviderId);
+        runner.setProperty(InvokeHTTP.REQUEST_OAUTH2_REFRESH_TOKEN, 
TokenRefreshStrategy.ON_UNAUTHORIZED_RESPONSE.name());
+
+        enqueueResponseCodeAndRun(HTTP_UNAUTHORIZED);
+
+        RecordedRequest recordedRequest = mockWebServer.takeRequest();
+        String actualAuthorizationHeader = 
recordedRequest.getHeader(HttpHeader.AUTHORIZATION.getHeader());
+        assertEquals("Bearer " + accessToken, actualAuthorizationHeader);
+
+        assertRelationshipStatusCodeEquals(InvokeHTTP.NO_RETRY, 
HTTP_UNAUTHORIZED);
+        runner.clearTransferState();
+
+        
when(oauth2AccessTokenProvider.getAccessDetails().getAccessToken()).thenReturn(refreshedAccessToken);
+
+        enqueueResponseCodeAndRun(HTTP_OK);
+
+        recordedRequest = mockWebServer.takeRequest();
+        actualAuthorizationHeader = 
recordedRequest.getHeader(HttpHeader.AUTHORIZATION.getHeader());
+        assertEquals("Bearer " + refreshedAccessToken, 
actualAuthorizationHeader);
+
+        assertResponseSuccessRelationships();
+        assertRelationshipStatusCodeEquals(InvokeHTTP.RESPONSE, HTTP_OK);
+    }
+
     private void setUrlProperty() {
         runner.setProperty(InvokeHTTP.HTTP_URL, getMockWebServerUrl());
     }
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/OAuth2AccessTokenProvider.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/OAuth2AccessTokenProvider.java
index 9cfc7578f5..ac0f763437 100644
--- 
a/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/OAuth2AccessTokenProvider.java
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/OAuth2AccessTokenProvider.java
@@ -27,4 +27,12 @@ public interface OAuth2AccessTokenProvider extends 
ControllerService {
      * @return A valid access token (refreshed automatically if needed) and 
additional metadata (provided by the OAuth2 access server)
      */
     AccessToken getAccessDetails();
+
+    /**
+     * Request a new Access Token based on configured properties regardless of
+     * current expiration status. The default implementation does not perform 
any
+     * action.
+     */
+    default void refreshAccessDetails() {
+    }
 }
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/OAuth2AccessTokenProvider.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/TokenRefreshStrategy.java
similarity index 52%
copy from 
nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/OAuth2AccessTokenProvider.java
copy to 
nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/TokenRefreshStrategy.java
index 9cfc7578f5..340289d2d0 100644
--- 
a/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/OAuth2AccessTokenProvider.java
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/TokenRefreshStrategy.java
@@ -16,15 +16,33 @@
  */
 package org.apache.nifi.oauth2;
 
-import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.components.DescribedValue;
 
-/**
- * Controller service that provides OAuth2 access details
- */
-public interface OAuth2AccessTokenProvider extends ControllerService {
+public enum TokenRefreshStrategy implements DescribedValue {
+
+    ON_TOKEN_EXPIRATION("The token will be refreshed based on its expiration 
time and the configured refresh window"),
+
+    ON_UNAUTHORIZED_RESPONSE("A new token will be requested in case of a 
non-authorized request (HTTP 401) even if the current token has not expired");
+
+    private final String description;
+
+    TokenRefreshStrategy(final String description) {
+        this.description = description;
+    }
+
+    @Override
+    public String getValue() {
+        return name();
+    }
+
+    @Override
+    public String getDisplayName() {
+        return name();
+    }
+
+    @Override
+    public String getDescription() {
+        return description;
+    }
 
-    /**
-     * @return A valid access token (refreshed automatically if needed) and 
additional metadata (provided by the OAuth2 access server)
-     */
-    AccessToken getAccessDetails();
 }
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 ad0f84416b..85c9ce0e32 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
@@ -48,6 +48,7 @@ import org.apache.nifi.ssl.SSLContextProvider;
 
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.X509TrustManager;
+
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.net.Proxy;
@@ -339,6 +340,29 @@ public class StandardOauth2AccessTokenProvider extends 
AbstractControllerService
         return accessDetails;
     }
 
+    @Override
+    public void refreshAccessDetails() {
+        if (this.accessDetails == null || this.accessDetails.getRefreshToken() 
== null) {
+            acquireAccessDetails();
+        } else {
+            getLogger().debug("Refresh Access Token request started [{}]", 
authorizationServerUrl);
+
+            FormBody.Builder refreshTokenBuilder = new FormBody.Builder()
+                    .add("grant_type", "refresh_token")
+                    .add("refresh_token", 
this.accessDetails.getRefreshToken());
+
+            addFormData(refreshTokenBuilder);
+
+            AccessToken newAccessDetails = requestToken(refreshTokenBuilder);
+
+            if (newAccessDetails.getRefreshToken() == null) {
+                
newAccessDetails.setRefreshToken(this.accessDetails.getRefreshToken());
+            }
+
+            this.accessDetails = newAccessDetails;
+        }
+    }
+
     private void getProperties(ConfigurationContext context) {
         authorizationServerUrl = 
context.getProperty(AUTHORIZATION_SERVER_URL).evaluateAttributeExpressions().getValue();
 
@@ -393,24 +417,6 @@ public class StandardOauth2AccessTokenProvider extends 
AbstractControllerService
         this.accessDetails = requestToken(acquireTokenBuilder);
     }
 
-    private void refreshAccessDetails() {
-        getLogger().debug("Refresh Access Token request started [{}]", 
authorizationServerUrl);
-
-        FormBody.Builder refreshTokenBuilder = new FormBody.Builder()
-                .add("grant_type", "refresh_token")
-                .add("refresh_token", this.accessDetails.getRefreshToken());
-
-        addFormData(refreshTokenBuilder);
-
-        AccessToken newAccessDetails = requestToken(refreshTokenBuilder);
-
-        if (newAccessDetails.getRefreshToken() == null) {
-            
newAccessDetails.setRefreshToken(this.accessDetails.getRefreshToken());
-        }
-
-        this.accessDetails = newAccessDetails;
-    }
-
     private void addFormData(FormBody.Builder formBuilder) {
         if (clientAuthenticationStrategy == 
ClientAuthenticationStrategy.REQUEST_BODY && clientId != null) {
             formBuilder.add("client_id", clientId);

Reply via email to