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);