From 050361ed30c33a0bb9336d58050add79e7cb8410 Mon Sep 17 00:00:00 2001 From: Victor Duvert Date: Mon, 13 Sep 2021 16:53:13 +0200 Subject: [PATCH 1/2] add new Oauth2 othent flow --- docs/HTTP-batchsource.md | 2 + docs/HTTP-streamingsource.md | 2 + .../source/common/BaseHttpSourceConfig.java | 35 +++++++++-- .../http/source/common/http/HttpClient.java | 4 +- .../source/common/http/OAuthGrantType.java | 42 +++++++++++++ .../http/source/common/http/OAuthUtil.java | 62 ++++++++++++++++++- widgets/HTTP-batchsource.json | 28 ++++++++- widgets/HTTP-streamingsource.json | 26 +++++++- 8 files changed, 188 insertions(+), 13 deletions(-) create mode 100644 src/main/java/io/cdap/plugin/http/source/common/http/OAuthGrantType.java diff --git a/docs/HTTP-batchsource.md b/docs/HTTP-batchsource.md index 37ed1e2d..f51701e2 100644 --- a/docs/HTTP-batchsource.md +++ b/docs/HTTP-batchsource.md @@ -378,6 +378,8 @@ is stopped. **OAuth2 Enabled:** If true, plugin will perform OAuth2 authentication. +**Grant Type:** The OAuth2 authentication flow that will be used. + **Auth URL:** Endpoint for the authorization server used to retrieve the authorization code. **Token URL:** Endpoint for the resource server, which exchanges the authorization code for an access token. diff --git a/docs/HTTP-streamingsource.md b/docs/HTTP-streamingsource.md index 4cad538e..86a318da 100644 --- a/docs/HTTP-streamingsource.md +++ b/docs/HTTP-streamingsource.md @@ -385,6 +385,8 @@ is stopped. **OAuth2 Enabled:** If true, plugin will perform OAuth2 authentication. +**Grant Type:** The OAuth2 authentication flow that will be used. + **Auth URL:** Endpoint for the authorization server used to retrieve the authorization code. **Token URL:** Endpoint for the resource server, which exchanges the authorization code for an access token. diff --git a/src/main/java/io/cdap/plugin/http/source/common/BaseHttpSourceConfig.java b/src/main/java/io/cdap/plugin/http/source/common/BaseHttpSourceConfig.java index a554dd6c..2c22d399 100644 --- a/src/main/java/io/cdap/plugin/http/source/common/BaseHttpSourceConfig.java +++ b/src/main/java/io/cdap/plugin/http/source/common/BaseHttpSourceConfig.java @@ -27,6 +27,7 @@ import io.cdap.plugin.http.source.common.error.HttpErrorHandlerEntity; import io.cdap.plugin.http.source.common.error.RetryableErrorHandling; import io.cdap.plugin.http.source.common.http.KeyStoreType; +import io.cdap.plugin.http.source.common.http.OAuthGrantType; import io.cdap.plugin.http.source.common.pagination.PaginationIteratorFactory; import io.cdap.plugin.http.source.common.pagination.PaginationType; import io.cdap.plugin.http.source.common.pagination.page.PageFormat; @@ -81,6 +82,7 @@ public abstract class BaseHttpSourceConfig extends ReferencePluginConfig { public static final String PROPERTY_CUSTOM_PAGINATION_CODE = "customPaginationCode"; public static final String PROPERTY_WAIT_TIME_BETWEEN_PAGES = "waitTimeBetweenPages"; public static final String PROPERTY_OAUTH2_ENABLED = "oauth2Enabled"; + public static final String PROPERTY_OAUTH2_GRANT_TYPE = "oauth2GrantType"; public static final String PROPERTY_AUTH_URL = "authUrl"; public static final String PROPERTY_TOKEN_URL = "tokenUrl"; public static final String PROPERTY_CLIENT_ID = "clientId"; @@ -280,6 +282,12 @@ public abstract class BaseHttpSourceConfig extends ReferencePluginConfig { @Description("If true, plugin will perform OAuth2 authentication.") protected String oauth2Enabled; + @Nullable + @Name(PROPERTY_OAUTH2_GRANT_TYPE) + @Description("Which Oauth2 credential flow is used.") + @Macro + protected String oauth2GrantType; + @Nullable @Name(PROPERTY_AUTH_URL) @Description("Endpoint for the authorization server used to retrieve the authorization code.") @@ -533,6 +541,11 @@ public Boolean getOauth2Enabled() { return Boolean.parseBoolean(oauth2Enabled); } + @Nullable + public OAuthGrantType getOauth2GrantType() { + return getEnumValueByString(OAuthGrantType.class, oauth2GrantType, PROPERTY_OAUTH2_GRANT_TYPE); + } + @Nullable public String getAuthUrl() { return authUrl; @@ -787,11 +800,23 @@ PAGINATION_INDEX_PLACEHOLDER, getPaginationType()), // Validate OAuth2 properties if (!containsMacro(PROPERTY_OAUTH2_ENABLED) && this.getOauth2Enabled()) { String reasonOauth2 = "OAuth2 is enabled"; - assertIsSet(getAuthUrl(), PROPERTY_AUTH_URL, reasonOauth2); - assertIsSet(getTokenUrl(), PROPERTY_TOKEN_URL, reasonOauth2); - assertIsSet(getClientId(), PROPERTY_CLIENT_ID, reasonOauth2); - assertIsSet(getClientSecret(), PROPERTY_CLIENT_SECRET, reasonOauth2); - assertIsSet(getRefreshToken(), PROPERTY_REFRESH_TOKEN, reasonOauth2); + String reasonOauth2GrantType = reasonOauth2 + " and grant type is " + getOauth2GrantType().getValue(); + + switch (getOauth2GrantType()) { + case CLIENT_CREDENTIALS: + assertIsSet(getTokenUrl(), PROPERTY_TOKEN_URL, reasonOauth2GrantType); + assertIsSet(getClientId(), PROPERTY_CLIENT_ID, reasonOauth2GrantType); + assertIsSet(getClientSecret(), PROPERTY_CLIENT_SECRET, reasonOauth2GrantType); + break; + case REFRESH_TOKEN: + + assertIsSet(getAuthUrl(), PROPERTY_AUTH_URL, reasonOauth2GrantType); + assertIsSet(getTokenUrl(), PROPERTY_TOKEN_URL, reasonOauth2GrantType); + assertIsSet(getClientId(), PROPERTY_CLIENT_ID, reasonOauth2GrantType); + assertIsSet(getClientSecret(), PROPERTY_CLIENT_SECRET, reasonOauth2GrantType); + assertIsSet(getRefreshToken(), PROPERTY_REFRESH_TOKEN, reasonOauth2GrantType); + break; + } } if (!containsMacro(PROPERTY_VERIFY_HTTPS) && !getVerifyHttps()) { diff --git a/src/main/java/io/cdap/plugin/http/source/common/http/HttpClient.java b/src/main/java/io/cdap/plugin/http/source/common/http/HttpClient.java index bb41293c..0a6ae1bd 100644 --- a/src/main/java/io/cdap/plugin/http/source/common/http/HttpClient.java +++ b/src/main/java/io/cdap/plugin/http/source/common/http/HttpClient.java @@ -128,9 +128,7 @@ private CloseableHttpClient createHttpClient() throws IOException { // oAuth2 if (config.getOauth2Enabled()) { - String accessToken = OAuthUtil.getAccessTokenByRefreshToken(HttpClients.createDefault(), config.getTokenUrl(), - config.getClientId(), config.getClientSecret(), - config.getRefreshToken()); + String accessToken = OAuthUtil.getAccessToken(HttpClients.createDefault(), config); clientHeaders.add(new BasicHeader("Authorization", "Bearer " + accessToken)); } diff --git a/src/main/java/io/cdap/plugin/http/source/common/http/OAuthGrantType.java b/src/main/java/io/cdap/plugin/http/source/common/http/OAuthGrantType.java new file mode 100644 index 00000000..75edbe32 --- /dev/null +++ b/src/main/java/io/cdap/plugin/http/source/common/http/OAuthGrantType.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2019 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.source.common.http; + +import io.cdap.plugin.http.source.common.EnumWithValue; + +/** + * Enum encoding the handled Oauth2 Grant Types + */ +public enum OAuthGrantType implements EnumWithValue { + REFRESH_TOKEN("refresh_token"), + CLIENT_CREDENTIALS("client_credentials"); + + private final String value; + + OAuthGrantType(String value) { + this.value = value; + } + + @Override + public String getValue() { + return value; + } + + @Override + public String toString() { + return this.getValue(); + } +} \ No newline at end of file diff --git a/src/main/java/io/cdap/plugin/http/source/common/http/OAuthUtil.java b/src/main/java/io/cdap/plugin/http/source/common/http/OAuthUtil.java index 27ac280a..01b4b5c0 100644 --- a/src/main/java/io/cdap/plugin/http/source/common/http/OAuthUtil.java +++ b/src/main/java/io/cdap/plugin/http/source/common/http/OAuthUtil.java @@ -16,21 +16,47 @@ package io.cdap.plugin.http.source.common.http; import com.google.gson.JsonElement; +import io.cdap.plugin.http.source.common.BaseHttpSourceConfig; import io.cdap.plugin.http.source.common.pagination.page.JSONUtil; +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.message.BasicHeader; +import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +import static io.cdap.plugin.http.source.common.http.OAuthGrantType.CLIENT_CREDENTIALS; +import static io.cdap.plugin.http.source.common.http.OAuthGrantType.REFRESH_TOKEN; /** * A class which contains utilities to make OAuth2 specific calls. */ public class OAuthUtil { + + public static String getAccessToken(CloseableHttpClient httpclient, BaseHttpSourceConfig config) throws IOException { + switch (config.getOauth2GrantType()) { + case REFRESH_TOKEN: + return getAccessTokenByRefreshToken(httpclient, config.getTokenUrl(), + config.getClientId(), config.getClientSecret(), + config.getRefreshToken()); + case CLIENT_CREDENTIALS: + return getAccessTokenByClientCredentials(httpclient, config.getTokenUrl(), + config.getClientId(), config.getClientSecret(), config.getScopes()); + default: + throw new IOException("Invalid Grant Type. Cannot retrieve access token."); + } + } + + public static String getAccessTokenByRefreshToken(CloseableHttpClient httpclient, String tokenUrl, String clientId, String clientSecret, String refreshToken) throws IOException { @@ -41,16 +67,48 @@ public static String getAccessTokenByRefreshToken(CloseableHttpClient httpclient .setParameter("client_id", clientId) .setParameter("client_secret", clientSecret) .setParameter("refresh_token", refreshToken) - .setParameter("grant_type", "refresh_token") + .setParameter("grant_type", OAuthGrantType.REFRESH_TOKEN.getValue()) .build(); } catch (URISyntaxException e) { - throw new IllegalArgumentException("Failed to build token URI for OAuth2", e); + throw new IllegalArgumentException("Failed to build access token URI for OAuth2 with grant type = " + + OAuthGrantType.REFRESH_TOKEN.getValue(), e); } HttpPost httppost = new HttpPost(uri); CloseableHttpResponse response = httpclient.execute(httppost); String responseString = EntityUtils.toString(response.getEntity(), "UTF-8"); + JsonElement jsonElement = JSONUtil.toJsonObject(responseString).get("access_token"); + return jsonElement.getAsString(); + } + + private static String getAccessTokenByClientCredentials(CloseableHttpClient httpclient, String tokenUrl, + String clientId, String clientSecret, String scope) + 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); + } + + 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())); + + 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 jsonElement = JSONUtil.toJsonObject(responseString).get("access_token"); return jsonElement.getAsString(); } diff --git a/widgets/HTTP-batchsource.json b/widgets/HTTP-batchsource.json index 6c053781..3695b7b8 100644 --- a/widgets/HTTP-batchsource.json +++ b/widgets/HTTP-batchsource.json @@ -121,6 +121,18 @@ } } }, + { + "widget-type": "select", + "label": "Grant Type", + "name": "oauth2GrantType", + "widget-attributes": { + "values": [ + "refresh_token", + "client_credentials" + ], + "default": "refresh_token" + } + }, { "widget-type": "textbox", "label": "Auth URL", @@ -503,7 +515,7 @@ "name": "Proxy authentication", "condition": { "property": "proxyUrl", - "operator": "exists", + "operator": "exists" }, "show": [ { @@ -625,7 +637,7 @@ }, "show": [ { - "name": "authUrl", + "name": "oauth2GrantType", "type": "property" }, { @@ -643,6 +655,18 @@ { "name": "scopes", "type": "property" + } + ] + }, + { + "name": "OAuth 2 enabled and Grant Type = refresh_token", + "condition": { + "expression": "oauth2Enabled == true && oauth2GrantType == 'refresh_token'" + }, + "show": [ + { + "name": "authUrl", + "type": "property" }, { "name": "refreshToken", diff --git a/widgets/HTTP-streamingsource.json b/widgets/HTTP-streamingsource.json index e7abdb68..7661bef1 100644 --- a/widgets/HTTP-streamingsource.json +++ b/widgets/HTTP-streamingsource.json @@ -126,6 +126,18 @@ } } }, + { + "widget-type": "select", + "label": "Grant Type", + "name": "oauth2GrantType", + "widget-attributes": { + "values": [ + "refresh_token", + "client_credentials" + ], + "default": "refresh_token" + } + }, { "widget-type": "textbox", "label": "Auth URL", @@ -625,7 +637,7 @@ }, "show": [ { - "name": "authUrl", + "name": "oauth2GrantType", "type": "property" }, { @@ -643,6 +655,18 @@ { "name": "scopes", "type": "property" + } + ] + }, + { + "name": "OAuth 2 enabled and Grant Type = refresh_token", + "condition": { + "expression": "oauth2Enabled == true && oauth2GrantType == 'refresh_token'" + }, + "show": [ + { + "name": "authUrl", + "type": "property" }, { "name": "refreshToken", From 6c83905c838f0e0c23143ccb1c1ee43e18807f5c Mon Sep 17 00:00:00 2001 From: Victor Duvert Date: Mon, 13 Sep 2021 17:14:50 +0200 Subject: [PATCH 2/2] fix checkstyle error --- .../io/cdap/plugin/http/source/common/http/OAuthGrantType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/cdap/plugin/http/source/common/http/OAuthGrantType.java b/src/main/java/io/cdap/plugin/http/source/common/http/OAuthGrantType.java index 75edbe32..492a6643 100644 --- a/src/main/java/io/cdap/plugin/http/source/common/http/OAuthGrantType.java +++ b/src/main/java/io/cdap/plugin/http/source/common/http/OAuthGrantType.java @@ -39,4 +39,4 @@ public String getValue() { public String toString() { return this.getValue(); } -} \ No newline at end of file +}