From c490a5ec0edba70bdc7382675d4225d2f9e324a0 Mon Sep 17 00:00:00 2001 From: psainics Date: Mon, 10 Feb 2025 15:41:23 +0530 Subject: [PATCH] Add 'client_credentials' OAuth2 authentication flow --- .../plugin/http/common/BaseHttpConfig.java | 255 +++++++++++------- .../plugin/http/common/http/HttpClient.java | 12 + .../http/OAuthClientAuthentication.java | 59 ++++ .../http/common/http/OAuthGrantType.java | 58 ++++ .../plugin/http/common/http/OAuthUtil.java | 79 +++++- .../source/batch/HttpBatchSourceConfig.java | 6 +- widgets/HTTP-batchsink.json | 56 +++- widgets/HTTP-batchsource.json | 56 +++- widgets/HTTP-streamingsource.json | 56 +++- 9 files changed, 523 insertions(+), 114 deletions(-) create mode 100644 src/main/java/io/cdap/plugin/http/common/http/OAuthClientAuthentication.java create mode 100644 src/main/java/io/cdap/plugin/http/common/http/OAuthGrantType.java diff --git a/src/main/java/io/cdap/plugin/http/common/BaseHttpConfig.java b/src/main/java/io/cdap/plugin/http/common/BaseHttpConfig.java index fdf5c376..dd0be9a4 100644 --- a/src/main/java/io/cdap/plugin/http/common/BaseHttpConfig.java +++ b/src/main/java/io/cdap/plugin/http/common/BaseHttpConfig.java @@ -25,8 +25,11 @@ import io.cdap.cdap.etl.api.validation.InvalidConfigPropertyException; import io.cdap.plugin.common.ReferencePluginConfig; import io.cdap.plugin.http.common.http.AuthType; +import io.cdap.plugin.http.common.http.OAuthClientAuthentication; +import io.cdap.plugin.http.common.http.OAuthGrantType; import io.cdap.plugin.http.common.http.OAuthUtil; +import io.cdap.plugin.http.source.common.BaseHttpSourceConfig; import java.io.File; import java.util.Optional; import javax.annotation.Nullable; @@ -41,6 +44,8 @@ public abstract class BaseHttpConfig extends ReferencePluginConfig { public static final String PROPERTY_OAUTH2_ENABLED = "oauth2Enabled"; public static final String PROPERTY_AUTH_URL = "authUrl"; public static final String PROPERTY_TOKEN_URL = "tokenUrl"; + public static final String PROPERTY_OAUTH2_GRANT_TYPE = "oauth2GrantType"; + public static final String PROPERTY_OAUTH2_CLIENT_AUTHENTICATION = "oauth2ClientAuthentication"; public static final String PROPERTY_CLIENT_ID = "clientId"; public static final String PROPERTY_CLIENT_SECRET = "clientSecret"; public static final String PROPERTY_SCOPES = "scopes"; @@ -82,7 +87,7 @@ public abstract class BaseHttpConfig extends ReferencePluginConfig { "OAuth2, Service account, Basic Authentication types are available.") protected String authType; - @Name(PROPERTY_OAUTH2_ENABLED) + @Name(PROPERTY_OAUTH2_ENABLED) @Description("If true, plugin will perform OAuth2 authentication.") @Nullable protected String oauth2Enabled; @@ -93,6 +98,18 @@ public abstract class BaseHttpConfig extends ReferencePluginConfig { @Macro protected String authUrl; + @Nullable + @Name(PROPERTY_OAUTH2_GRANT_TYPE) + @Description("Which Oauth2 grant type flow is used.") + @Macro + protected String oauth2GrantType; + + @Nullable + @Name(PROPERTY_OAUTH2_CLIENT_AUTHENTICATION) + @Description("Which Oauth2 client authentication flow is used.") + @Macro + protected String oauth2ClientAuthentication; + @Nullable @Name(PROPERTY_TOKEN_URL) @Description("Endpoint for the resource server, which exchanges the authorization code for an access token.") @@ -210,9 +227,22 @@ public String getOAuth2Enabled() { @Nullable public String getAuthUrl() { - return authUrl; + return authUrl; } + public OAuthGrantType getOauth2GrantType() { + OAuthGrantType grantType = OAuthGrantType.getGrantType(oauth2GrantType); + return BaseHttpSourceConfig.getEnumValueByString(OAuthGrantType.class, grantType.getValue(), + PROPERTY_OAUTH2_GRANT_TYPE); + } + + public OAuthClientAuthentication getOauth2ClientAuthentication() { + OAuthClientAuthentication clientAuthentication = OAuthClientAuthentication.getClientAuthentication( + oauth2ClientAuthentication); + return BaseHttpSourceConfig.getEnumValueByString(OAuthClientAuthentication.class, + clientAuthentication.getValue(), PROPERTY_OAUTH2_CLIENT_AUTHENTICATION); + } + @Nullable public String getTokenUrl() { return tokenUrl; @@ -314,113 +344,138 @@ public Boolean isServiceAccountFilePath() { } public boolean validateServiceAccount(FailureCollector collector) { - if (containsMacro(PROPERTY_NAME_SERVICE_ACCOUNT_FILE_PATH) || - containsMacro(PROPERTY_NAME_SERVICE_ACCOUNT_JSON)) { - return false; - } - final Boolean bServiceAccountFilePath = isServiceAccountFilePath(); - final Boolean bServiceAccountJson = isServiceAccountJson(); - - // we don't want the validation to fail because the VM used during the validation - // may be different from the VM used during runtime and may not have the Google Drive Api scope. - if (bServiceAccountFilePath && PROPERTY_AUTO_DETECT_VALUE.equalsIgnoreCase(serviceAccountFilePath)) { - return false; + if (containsMacro(PROPERTY_NAME_SERVICE_ACCOUNT_FILE_PATH) || + containsMacro(PROPERTY_NAME_SERVICE_ACCOUNT_JSON)) { + return false; + } + final Boolean bServiceAccountFilePath = isServiceAccountFilePath(); + final Boolean bServiceAccountJson = isServiceAccountJson(); + + // we don't want the validation to fail because the VM used during the validation + // may be different from the VM used during runtime and may not have the Google Drive Api scope. + if (bServiceAccountFilePath && PROPERTY_AUTO_DETECT_VALUE.equalsIgnoreCase( + serviceAccountFilePath)) { + return false; + } + + if (bServiceAccountFilePath != null && bServiceAccountFilePath) { + if (!PROPERTY_AUTO_DETECT_VALUE.equals(serviceAccountFilePath) && + !new File(serviceAccountFilePath).exists()) { + collector.addFailure("Service Account File Path is not available.", + "Please provide path to existing Service Account file.") + .withConfigProperty(PROPERTY_NAME_SERVICE_ACCOUNT_FILE_PATH); } + } + if (bServiceAccountJson != null && bServiceAccountJson) { + if (!Optional.ofNullable(getServiceAccountJson()).isPresent()) { + collector.addFailure("Service Account JSON can not be empty.", + "Please provide Service Account JSON.") + .withConfigProperty(PROPERTY_NAME_SERVICE_ACCOUNT_JSON); + } + } + return collector.getValidationFailures().size() == 0; + } + + public void validate(FailureCollector failureCollector) { + // Validate OAuth2 properties + if (!containsMacro(PROPERTY_OAUTH2_ENABLED) && this.getOauth2Enabled()) { + String reasonOauth2 = "OAuth2 is enabled"; + assertIsSetWithFailureCollector(getTokenUrl(), PROPERTY_TOKEN_URL, reasonOauth2, + failureCollector); + assertIsSetWithFailureCollector(getClientId(), PROPERTY_CLIENT_ID, reasonOauth2, + failureCollector); + assertIsSetWithFailureCollector(getClientSecret(), PROPERTY_CLIENT_SECRET, reasonOauth2, + failureCollector); + assertIsSetWithFailureCollector(getRefreshToken(), PROPERTY_REFRESH_TOKEN, reasonOauth2, + failureCollector); + } - if (bServiceAccountFilePath != null && bServiceAccountFilePath) { - if (!PROPERTY_AUTO_DETECT_VALUE.equals(serviceAccountFilePath) && - !new File(serviceAccountFilePath).exists()) { - collector.addFailure("Service Account File Path is not available.", - "Please provide path to existing Service Account file.") - .withConfigProperty(PROPERTY_NAME_SERVICE_ACCOUNT_FILE_PATH); - } - } - if (bServiceAccountJson != null && bServiceAccountJson) { - if (!Optional.ofNullable(getServiceAccountJson()).isPresent()) { - collector.addFailure("Service Account JSON can not be empty.", - "Please provide Service Account JSON.") - .withConfigProperty(PROPERTY_NAME_SERVICE_ACCOUNT_JSON); - } - } - return collector.getValidationFailures().size() == 0; + if (!containsMacro(PROPERTY_WAIT_TIME_BETWEEN_PAGES) && waitTimeBetweenPages != null + && waitTimeBetweenPages < 0) { + failureCollector.addFailure("Wait Time Between Pages cannot be a negative number.", + null).withConfigProperty(PROPERTY_WAIT_TIME_BETWEEN_PAGES); } - public void validate(FailureCollector failureCollector) { - // Validate OAuth2 properties - if (!containsMacro(PROPERTY_OAUTH2_ENABLED) && this.getOauth2Enabled()) { - String reasonOauth2 = "OAuth2 is enabled"; - assertIsSetWithFailureCollector(getTokenUrl(), PROPERTY_TOKEN_URL, reasonOauth2, failureCollector); - assertIsSetWithFailureCollector(getClientId(), PROPERTY_CLIENT_ID, reasonOauth2, failureCollector); - assertIsSetWithFailureCollector(getClientSecret(), PROPERTY_CLIENT_SECRET, reasonOauth2, failureCollector); - assertIsSetWithFailureCollector(getRefreshToken(), PROPERTY_REFRESH_TOKEN, reasonOauth2, failureCollector); + // Validate Authentication properties + AuthType authType = getAuthType(); + switch (authType) { + case OAUTH2: + validateOAuth2Fields(failureCollector); + break; + case SERVICE_ACCOUNT: + String reasonSA = "Service Account is enabled"; + assertIsSet(getServiceAccountType(), PROPERTY_NAME_SERVICE_ACCOUNT_TYPE, reasonSA); + boolean propertiesAreValid = validateServiceAccount(failureCollector); + if (propertiesAreValid) { + try { + AccessToken accessToken = OAuthUtil.getAccessToken(this); + } catch (Exception e) { + failureCollector.addFailure("Unable to authenticate given service account info. ", + "Please make sure all infomation entered correctly") + .withStacktrace(e.getStackTrace()); + } } - - if (!containsMacro(PROPERTY_WAIT_TIME_BETWEEN_PAGES) && waitTimeBetweenPages != null - && waitTimeBetweenPages < 0) { - failureCollector.addFailure("Wait Time Between Pages cannot be a negative number.", - null).withConfigProperty(PROPERTY_WAIT_TIME_BETWEEN_PAGES); + break; + case BASIC_AUTH: + String reasonBasicAuth = "Basic Authentication is enabled"; + if (!containsMacro(PROPERTY_USERNAME)) { + assertIsSetWithFailureCollector(getUsername(), PROPERTY_USERNAME, reasonBasicAuth, + failureCollector); } - - // Validate Authentication properties - AuthType authType = getAuthType(); - switch (authType) { - case OAUTH2: - String reasonOauth2 = "OAuth2 is enabled"; - if (!containsMacro(PROPERTY_TOKEN_URL)) { - assertIsSetWithFailureCollector(getTokenUrl(), PROPERTY_TOKEN_URL, reasonOauth2, failureCollector); - } - if (!containsMacro(PROPERTY_CLIENT_ID)) { - assertIsSetWithFailureCollector(getClientId(), PROPERTY_CLIENT_ID, reasonOauth2, failureCollector); - } - if (!containsMacro((PROPERTY_CLIENT_SECRET))) { - assertIsSetWithFailureCollector(getClientSecret(), PROPERTY_CLIENT_SECRET, reasonOauth2, - failureCollector); - } - if (!containsMacro(PROPERTY_REFRESH_TOKEN)) { - assertIsSetWithFailureCollector(getRefreshToken(), PROPERTY_REFRESH_TOKEN, reasonOauth2, - failureCollector); - } - break; - case SERVICE_ACCOUNT: - String reasonSA = "Service Account is enabled"; - assertIsSet(getServiceAccountType(), PROPERTY_NAME_SERVICE_ACCOUNT_TYPE, reasonSA); - boolean propertiesAreValid = validateServiceAccount(failureCollector); - if (propertiesAreValid) { - try { - AccessToken accessToken = OAuthUtil.getAccessToken(this); - } catch (Exception e) { - failureCollector.addFailure("Unable to authenticate given service account info. ", - "Please make sure all infomation entered correctly") - .withStacktrace(e.getStackTrace()); - } - } - break; - case BASIC_AUTH: - String reasonBasicAuth = "Basic Authentication is enabled"; - if (!containsMacro(PROPERTY_USERNAME)) { - assertIsSetWithFailureCollector(getUsername(), PROPERTY_USERNAME, reasonBasicAuth, - failureCollector); - } - if (!containsMacro(PROPERTY_PASSWORD)) { - assertIsSetWithFailureCollector(getPassword(), PROPERTY_PASSWORD, reasonBasicAuth, - failureCollector); - } - break; + if (!containsMacro(PROPERTY_PASSWORD)) { + assertIsSetWithFailureCollector(getPassword(), PROPERTY_PASSWORD, reasonBasicAuth, + failureCollector); } + break; } - - public static void assertIsSet(Object propertyValue, String propertyName, String reason) { - if (propertyValue == null) { - throw new InvalidConfigPropertyException( - String.format("Property '%s' must be set, since %s", propertyName, reason), propertyName); - } + } + + private void validateOAuth2Fields(FailureCollector failureCollector) { + String reasonOauth2GrantType = String.format("OAuth2 is enabled and grant type is %s.", + getOauth2GrantType().getValue()); + if (!containsMacro(PROPERTY_TOKEN_URL)) { + assertIsSetWithFailureCollector(getTokenUrl(), PROPERTY_TOKEN_URL, reasonOauth2GrantType, + failureCollector); } + if (!containsMacro(PROPERTY_CLIENT_ID)) { + assertIsSetWithFailureCollector(getClientId(), PROPERTY_CLIENT_ID, reasonOauth2GrantType, + failureCollector); + } + if (!containsMacro(PROPERTY_CLIENT_SECRET)) { + assertIsSetWithFailureCollector(getClientSecret(), PROPERTY_CLIENT_SECRET, + reasonOauth2GrantType, failureCollector); + } + if (!containsMacro(PROPERTY_OAUTH2_CLIENT_AUTHENTICATION)) { + assertIsSetWithFailureCollector(getOauth2ClientAuthentication(), + PROPERTY_OAUTH2_CLIENT_AUTHENTICATION, reasonOauth2GrantType, failureCollector); + } + // in case of refresh token grant type, also check 2 additional fields + if (OAuthGrantType.REFRESH_TOKEN.equals(getOauth2GrantType())) { + if (!containsMacro(PROPERTY_AUTH_URL)) { + assertIsSetWithFailureCollector(getAuthUrl(), PROPERTY_AUTH_URL, reasonOauth2GrantType, + failureCollector); + } + if (!containsMacro(PROPERTY_REFRESH_TOKEN)) { + assertIsSetWithFailureCollector(getRefreshToken(), PROPERTY_REFRESH_TOKEN, + reasonOauth2GrantType, failureCollector); + } + } + } - public static void assertIsSetWithFailureCollector(Object propertyValue, String propertyName, String reason, - FailureCollector failureCollector) { - if (propertyValue == null) { - failureCollector.addFailure(String.format("Property '%s' must be set, since %s", propertyName, reason), - null).withConfigProperty(propertyName); - } + public static void assertIsSet(Object propertyValue, String propertyName, String reason) { + if (propertyValue == null) { + throw new InvalidConfigPropertyException( + String.format("Property '%s' must be set, since %s", propertyName, reason), propertyName); + } + } + + public static void assertIsSetWithFailureCollector(Object propertyValue, String propertyName, + String reason, + FailureCollector failureCollector) { + if (propertyValue == null) { + failureCollector.addFailure( + String.format("Property '%s' must be set, since %s", propertyName, reason), + null).withConfigProperty(propertyName); } + } } diff --git a/src/main/java/io/cdap/plugin/http/common/http/HttpClient.java b/src/main/java/io/cdap/plugin/http/common/http/HttpClient.java index 51a6880e..15f63f3c 100644 --- a/src/main/java/io/cdap/plugin/http/common/http/HttpClient.java +++ b/src/main/java/io/cdap/plugin/http/common/http/HttpClient.java @@ -32,6 +32,7 @@ import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicHeader; import java.io.Closeable; @@ -133,6 +134,17 @@ public CloseableHttpClient createHttpClient(String pageUriStr) throws IOExceptio } httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); + ArrayList
clientHeaders = new ArrayList<>(); + + // oAuth2 + if (config.getOauth2Enabled()) { + clientHeaders.add(new BasicHeader("Authorization", + "Bearer " + OAuthUtil.getAccessToken(HttpClients.createDefault(), config) + .getTokenValue())); + } + + httpClientBuilder.setDefaultHeaders(clientHeaders); + return httpClientBuilder.build(); } diff --git a/src/main/java/io/cdap/plugin/http/common/http/OAuthClientAuthentication.java b/src/main/java/io/cdap/plugin/http/common/http/OAuthClientAuthentication.java new file mode 100644 index 00000000..cef530de --- /dev/null +++ b/src/main/java/io/cdap/plugin/http/common/http/OAuthClientAuthentication.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2025 Cask Data, Inc. + * + * Licensed 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 io.cdap.plugin.http.common.http; + +import io.cdap.plugin.http.common.EnumWithValue; +import java.util.Objects; + +/** + * Enum encoding the handled Oauth2 Client Authentication + */ +public enum OAuthClientAuthentication implements EnumWithValue { + BODY("body", "Body"), + REQUEST_PARAMETER("request_parameter", "Request Parameter"); + + private final String value; + private final String label; + + OAuthClientAuthentication(String value, String label) { + this.value = value; + this.label = label; + } + + public static OAuthClientAuthentication getClientAuthentication(String clientAuthentication) { + if (Objects.equals(clientAuthentication, BODY.getLabel())) { + return BODY; + } else { + return REQUEST_PARAMETER; + } + } + + @Override + public String getValue() { + return value; + } + + public String getLabel() { + return label; + } + + @Override + public String toString() { + return this.getValue(); + } +} + diff --git a/src/main/java/io/cdap/plugin/http/common/http/OAuthGrantType.java b/src/main/java/io/cdap/plugin/http/common/http/OAuthGrantType.java new file mode 100644 index 00000000..ad3b0df9 --- /dev/null +++ b/src/main/java/io/cdap/plugin/http/common/http/OAuthGrantType.java @@ -0,0 +1,58 @@ +/* + * Copyright © 2025 Cask Data, Inc. + * + * Licensed 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 io.cdap.plugin.http.common.http; + +import io.cdap.plugin.http.common.EnumWithValue; +import java.util.Objects; + +/** + * Enum encoding the handled Oauth2 Grant Types + */ +public enum OAuthGrantType implements EnumWithValue { + REFRESH_TOKEN("refresh token", "Refresh Token"), CLIENT_CREDENTIALS("client_credentials", + "Client Credentials"); + + private final String value; + private final String label; + + OAuthGrantType(String value, String label) { + this.value = value; + this.label = label; + } + + public static OAuthGrantType getGrantType(String oauth2GrantType) { + if (Objects.equals(oauth2GrantType, REFRESH_TOKEN.getLabel())) { + return REFRESH_TOKEN; + } else { + return CLIENT_CREDENTIALS; + } + } + + @Override + public String getValue() { + return value; + } + + public String getLabel() { + return label; + } + + @Override + public String toString() { + return this.getValue(); + } +} diff --git a/src/main/java/io/cdap/plugin/http/common/http/OAuthUtil.java b/src/main/java/io/cdap/plugin/http/common/http/OAuthUtil.java index 7bb307c1..9e87ab9a 100644 --- a/src/main/java/io/cdap/plugin/http/common/http/OAuthUtil.java +++ b/src/main/java/io/cdap/plugin/http/common/http/OAuthUtil.java @@ -23,11 +23,14 @@ import io.cdap.plugin.http.common.BaseHttpConfig; import io.cdap.plugin.http.common.pagination.page.JSONUtil; import io.cdap.plugin.http.source.common.BaseHttpSourceConfig; +import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.URIBuilder; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import java.io.ByteArrayInputStream; @@ -38,7 +41,10 @@ import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.time.Instant; +import java.util.ArrayList; +import java.util.Base64; import java.util.Date; +import java.util.List; import javax.annotation.Nullable; /** @@ -70,12 +76,79 @@ public static AccessToken getAccessToken(BaseHttpConfig config) throws IOExcepti return OAuthUtil.getAccessTokenByServiceAccount(config); case OAUTH2: try (CloseableHttpClient client = HttpClients.createDefault()) { - return OAuthUtil.getAccessTokenByRefreshToken(client, config); + return getAccessToken(client, (BaseHttpSourceConfig) config); } } return null; } + public static AccessToken getAccessToken(CloseableHttpClient httpclient, + BaseHttpSourceConfig config) + throws IOException { + switch (config.getOauth2GrantType()) { + case REFRESH_TOKEN: + return getAccessTokenByRefreshToken(httpclient, config); + case CLIENT_CREDENTIALS: + return getAccessTokenByClientCredentials(httpclient, config.getTokenUrl(), + config.getClientId(), config.getClientSecret(), config.getScopes(), + config.getOauth2ClientAuthentication().getValue()); + default: + throw new IOException("Invalid Grant Type. Cannot retrieve access token."); + } + } + + private static AccessToken getAccessTokenByClientCredentials(CloseableHttpClient httpclient, + String tokenUrl, + String clientId, String clientSecret, String scope, String clientAuthentication) + throws IOException { + URI uri; + try { + uri = new URIBuilder(tokenUrl) + .build(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException( + "Failed to build access token URI for OAuth2 with grant type = " + + OAuthGrantType.CLIENT_CREDENTIALS.getValue(), e); + } catch (NullPointerException e) { + throw new IllegalArgumentException( + "One or more required OAuth2 parameters (Client ID, Client Secret, " + + "or Token URL) are missing.", e); + } + + HttpPost httppost = new HttpPost(uri); + List nameValuePairs = new ArrayList<>(); + nameValuePairs.add(new BasicNameValuePair("scope", scope)); + nameValuePairs.add( + new BasicNameValuePair("grant_type", OAuthGrantType.CLIENT_CREDENTIALS.getValue())); + nameValuePairs.add(new BasicNameValuePair("client_authentication", clientAuthentication)); + + httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs)); + + String authorizationKey = + "Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes()); + + httppost.addHeader(new BasicHeader("Authorization", authorizationKey)); + + CloseableHttpResponse response = httpclient.execute(httppost); + String responseString = EntityUtils.toString(response.getEntity(), "UTF-8"); + + JsonElement accessTokenElement = JSONUtil.toJsonObject(responseString).get("access_token"); + + if (accessTokenElement == null) { + throw new IllegalArgumentException("Access token not found"); + } + + JsonElement expiresInElement = JSONUtil.toJsonObject(responseString).get("expires_in"); + Date expiresInDate = null; + if (expiresInElement != null) { + long expiresAtMilliseconds = System.currentTimeMillis() + + (long) (expiresInElement.getAsInt() * 1000) - 60000L; + expiresInDate = new Date(expiresAtMilliseconds); + } + + return new AccessToken(accessTokenElement.getAsString(), expiresInDate); + } + /** * Returns true only if the expiration time set in the accessToken is before the current time. * @param accessToken AccessToken instance @@ -117,6 +190,10 @@ public static AccessToken getAccessTokenByRefreshToken(CloseableHttpClient httpc .build(); } catch (URISyntaxException e) { throw new IllegalArgumentException("Failed to build token URI for OAuth2", e); + } catch (NullPointerException e) { + throw new IllegalArgumentException( + "One or more required OAuth2 parameters (Auth URL, Token URL, Client ID, Client Secret, " + + "or Refresh Token) are missing.", e); } HttpPost httppost = new HttpPost(uri); diff --git a/src/main/java/io/cdap/plugin/http/source/batch/HttpBatchSourceConfig.java b/src/main/java/io/cdap/plugin/http/source/batch/HttpBatchSourceConfig.java index c0b32ef8..89a4c2b6 100644 --- a/src/main/java/io/cdap/plugin/http/source/batch/HttpBatchSourceConfig.java +++ b/src/main/java/io/cdap/plugin/http/source/batch/HttpBatchSourceConfig.java @@ -83,7 +83,7 @@ private void validateOAuth2Credentials(FailureCollector collector) { } try (CloseableHttpClient closeableHttpClient = httpclientBuilder.build()) { - OAuthUtil.getAccessTokenByRefreshToken(closeableHttpClient, this); + OAuthUtil.getAccessToken(closeableHttpClient, this); } catch (JsonSyntaxException | HttpHostConnectException e) { String errorMessage = "Error occurred during credential validation : " + e.getMessage(); collector.addFailure(errorMessage, null); @@ -151,6 +151,8 @@ private HttpBatchSourceConfig(HttpBatchSourceConfigBuilder builder) { this.proxyUrl = builder.proxyUrl; this.proxyUsername = builder.proxyUsername; this.proxyPassword = builder.proxyPassword; + this.oauth2ClientAuthentication = builder.oauthClientAuthentication; + this.oauth2GrantType = builder.oauthGrantType; } public static HttpBatchSourceConfigBuilder builder() { @@ -190,6 +192,8 @@ public static class HttpBatchSourceConfigBuilder { private String proxyPassword; private String username; private String password; + private String oauthGrantType; + private String oauthClientAuthentication; public HttpBatchSourceConfigBuilder setReferenceName(String referenceName) { diff --git a/widgets/HTTP-batchsink.json b/widgets/HTTP-batchsink.json index 4741833a..7fea704d 100644 --- a/widgets/HTTP-batchsink.json +++ b/widgets/HTTP-batchsink.json @@ -275,6 +275,30 @@ ] } }, + { + "widget-type": "select", + "label": "Grant Type", + "name": "oauth2GrantType", + "widget-attributes": { + "values": [ + "Refresh Token", + "Client Credentials" + ], + "default": "Refresh Token" + } + }, + { + "widget-type": "select", + "label": "Client Authentication", + "name": "oauth2ClientAuthentication", + "widget-attributes": { + "values": [ + "Body", + "Request Parameter" + ], + "default": "Body" + } + }, { "widget-type": "textbox", "label": "Auth URL", @@ -466,6 +490,30 @@ } ] }, + { + "name": "Authenticate with Grant type", + "condition": { + "expression": "authType == 'oAuth2' && oauth2GrantType == 'Client Credentials'" + }, + "show": [ + { + "name": "oauth2ClientAuthentication", + "type": "property" + } + ] + }, + { + "name": "Show Refresh_Token when Grant type is Refresh Token", + "condition": { + "expression": "authType == 'oAuth2' && oauth2GrantType == 'Refresh Token'" + }, + "show": [ + { + "name": "refreshToken", + "type": "property" + } + ] + }, { "name": "Authenticate with OAuth2", "condition": { @@ -474,6 +522,10 @@ "value": "oAuth2" }, "show": [ + { + "name": "oauth2GrantType", + "type": "property" + }, { "name": "authUrl", "type": "property" @@ -493,10 +545,6 @@ { "name": "scopes", "type": "property" - }, - { - "name": "refreshToken", - "type": "property" } ] }, diff --git a/widgets/HTTP-batchsource.json b/widgets/HTTP-batchsource.json index 9eed8feb..014cdc54 100644 --- a/widgets/HTTP-batchsource.json +++ b/widgets/HTTP-batchsource.json @@ -165,6 +165,30 @@ ] } }, + { + "widget-type": "select", + "label": "Grant Type", + "name": "oauth2GrantType", + "widget-attributes": { + "values": [ + "Refresh Token", + "Client Credentials" + ], + "default": "Refresh Token" + } + }, + { + "widget-type": "select", + "label": "Client Authentication", + "name": "oauth2ClientAuthentication", + "widget-attributes": { + "values": [ + "Body", + "Request Parameter" + ], + "default": "Body" + } + }, { "widget-type": "textbox", "label": "Auth URL", @@ -720,6 +744,30 @@ } ] }, + { + "name": "Authenticate with Grant type", + "condition": { + "expression": "authType == 'oAuth2' && oauth2GrantType == 'Client Credentials'" + }, + "show": [ + { + "name": "oauth2ClientAuthentication", + "type": "property" + } + ] + }, + { + "name": "Show Refresh_Token when Grant type is Refresh Token", + "condition": { + "expression": "authType == 'oAuth2' && oauth2GrantType == 'Refresh Token'" + }, + "show": [ + { + "name": "refreshToken", + "type": "property" + } + ] + }, { "name": "Authenticate with OAuth2", "condition": { @@ -728,6 +776,10 @@ "value": "oAuth2" }, "show": [ + { + "name": "oauth2GrantType", + "type": "property" + }, { "name": "authUrl", "type": "property" @@ -747,10 +799,6 @@ { "name": "scopes", "type": "property" - }, - { - "name": "refreshToken", - "type": "property" } ] }, diff --git a/widgets/HTTP-streamingsource.json b/widgets/HTTP-streamingsource.json index e47c0b0a..49a2a0fd 100644 --- a/widgets/HTTP-streamingsource.json +++ b/widgets/HTTP-streamingsource.json @@ -133,6 +133,30 @@ ] } }, + { + "widget-type": "select", + "label": "Grant Type", + "name": "oauth2GrantType", + "widget-attributes": { + "values": [ + "Refresh Token", + "Client Credentials" + ], + "default": "Refresh Token" + } + }, + { + "widget-type": "select", + "label": "Client Authentication", + "name": "oauth2ClientAuthentication", + "widget-attributes": { + "values": [ + "Body", + "Request Parameter" + ], + "default": "Body" + } + }, { "widget-type": "textbox", "label": "Auth URL", @@ -676,6 +700,30 @@ } ] }, + { + "name": "Authenticate with Grant type", + "condition": { + "expression": "authType == 'oAuth2' && oauth2GrantType == 'Client Credentials'" + }, + "show": [ + { + "name": "oauth2ClientAuthentication", + "type": "property" + } + ] + }, + { + "name": "Show Refresh_Token when Grant type is Refresh Token", + "condition": { + "expression": "authType == 'oAuth2' && oauth2GrantType == 'Refresh Token'" + }, + "show": [ + { + "name": "refreshToken", + "type": "property" + } + ] + }, { "name": "Authenticate with OAuth2", "condition": { @@ -684,6 +732,10 @@ "value": "oAuth2" }, "show": [ + { + "name": "oauth2GrantType", + "type": "property" + }, { "name": "authUrl", "type": "property" @@ -703,10 +755,6 @@ { "name": "scopes", "type": "property" - }, - { - "name": "refreshToken", - "type": "property" } ] },