Skip to content

Commit 0bb9655

Browse files
Added GrantType and Client Authentication for the HttpOauth2
1 parent 333f916 commit 0bb9655

9 files changed

Lines changed: 426 additions & 33 deletions

File tree

src/main/java/io/cdap/plugin/http/common/BaseHttpConfig.java

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import io.cdap.plugin.http.common.http.AuthType;
2828
import io.cdap.plugin.http.common.http.OAuthUtil;
2929

30+
import io.cdap.plugin.http.source.common.BaseHttpSourceConfig;
3031
import java.io.File;
3132
import java.util.Optional;
3233
import javax.annotation.Nullable;
@@ -48,6 +49,8 @@ public abstract class BaseHttpConfig extends ReferencePluginConfig {
4849
public static final String PROPERTY_PROXY_URL = "proxyUrl";
4950
public static final String PROPERTY_PROXY_USERNAME = "proxyUsername";
5051
public static final String PROPERTY_PROXY_PASSWORD = "proxyPassword";
52+
public static final String PROPERTY_OAUTH2_GRANT_TYPE = "oauth2GrantType";
53+
public static final String PROPERTY_OAUTH2_CLIENT_AUTHENTICATION = "oauth2ClientAuthentication";
5154

5255
public static final String PROPERTY_AUTH_TYPE_LABEL = "Auth type";
5356

@@ -93,6 +96,18 @@ public abstract class BaseHttpConfig extends ReferencePluginConfig {
9396
@Macro
9497
protected String authUrl;
9598

99+
@Nullable
100+
@Name(PROPERTY_OAUTH2_GRANT_TYPE)
101+
@Description("Which Oauth2 grant type flow is used.")
102+
@Macro
103+
protected String oauth2GrantType;
104+
105+
@Nullable
106+
@Name(PROPERTY_OAUTH2_CLIENT_AUTHENTICATION)
107+
@Description("Which Oauth2 client authentication flow is used.")
108+
@Macro
109+
protected String oauth2ClientAuthentication;
110+
96111
@Nullable
97112
@Name(PROPERTY_TOKEN_URL)
98113
@Description("Endpoint for the resource server, which exchanges the authorization code for an access token.")
@@ -208,6 +223,19 @@ public String getOAuth2Enabled() {
208223
return oauth2Enabled;
209224
}
210225

226+
public OAuthGrantType getOauth2GrantType() {
227+
OAuthGrantType grantType = OAuthGrantType.getGrantType(oauth2GrantType);
228+
return BaseHttpSourceConfig.getEnumValueByString(OAuthGrantType.class, grantType.getValue(),
229+
PROPERTY_OAUTH2_GRANT_TYPE);
230+
}
231+
232+
public OAuthClientAuthentication getOauth2ClientAuthentication() {
233+
OAuthClientAuthentication clientAuthentication = OAuthClientAuthentication.getClientAuthentication(
234+
oauth2ClientAuthentication);
235+
return BaseHttpSourceConfig.getEnumValueByString(OAuthClientAuthentication.class,
236+
clientAuthentication.getValue(), PROPERTY_OAUTH2_CLIENT_AUTHENTICATION);
237+
}
238+
211239
@Nullable
212240
public String getAuthUrl() {
213241
return authUrl;
@@ -365,21 +393,7 @@ public void validate(FailureCollector failureCollector) {
365393
AuthType authType = getAuthType();
366394
switch (authType) {
367395
case OAUTH2:
368-
String reasonOauth2 = "OAuth2 is enabled";
369-
if (!containsMacro(PROPERTY_TOKEN_URL)) {
370-
assertIsSetWithFailureCollector(getTokenUrl(), PROPERTY_TOKEN_URL, reasonOauth2, failureCollector);
371-
}
372-
if (!containsMacro(PROPERTY_CLIENT_ID)) {
373-
assertIsSetWithFailureCollector(getClientId(), PROPERTY_CLIENT_ID, reasonOauth2, failureCollector);
374-
}
375-
if (!containsMacro((PROPERTY_CLIENT_SECRET))) {
376-
assertIsSetWithFailureCollector(getClientSecret(), PROPERTY_CLIENT_SECRET, reasonOauth2,
377-
failureCollector);
378-
}
379-
if (!containsMacro(PROPERTY_REFRESH_TOKEN)) {
380-
assertIsSetWithFailureCollector(getRefreshToken(), PROPERTY_REFRESH_TOKEN, reasonOauth2,
381-
failureCollector);
382-
}
396+
validateOAuth2Fields(failureCollector);
383397
break;
384398
case SERVICE_ACCOUNT:
385399
String reasonSA = "Service Account is enabled";
@@ -423,4 +437,37 @@ public static void assertIsSetWithFailureCollector(Object propertyValue, String
423437
null).withConfigProperty(propertyName);
424438
}
425439
}
440+
441+
private void validateOAuth2Fields(FailureCollector failureCollector) {
442+
String reasonOauth2GrantType = String.format("OAuth2 is enabled and grant type is %s.",
443+
getOauth2GrantType().getValue());
444+
if (!containsMacro(PROPERTY_TOKEN_URL)) {
445+
assertIsSetWithFailureCollector(getTokenUrl(), PROPERTY_TOKEN_URL,
446+
reasonOauth2GrantType, failureCollector);
447+
}
448+
if (!containsMacro(PROPERTY_CLIENT_ID)) {
449+
assertIsSetWithFailureCollector(getClientId(), PROPERTY_CLIENT_ID,
450+
reasonOauth2GrantType, failureCollector);
451+
}
452+
if (!containsMacro(PROPERTY_CLIENT_SECRET)) {
453+
assertIsSetWithFailureCollector(getClientSecret(), PROPERTY_CLIENT_SECRET,
454+
reasonOauth2GrantType, failureCollector);
455+
}
456+
if (!containsMacro(PROPERTY_OAUTH2_CLIENT_AUTHENTICATION)) {
457+
assertIsSetWithFailureCollector(getOauth2ClientAuthentication(),
458+
PROPERTY_OAUTH2_CLIENT_AUTHENTICATION, reasonOauth2GrantType, failureCollector);
459+
}
460+
// in case of refresh token grant type, also check 2 additional fields
461+
if (OAuthGrantType.REFRESH_TOKEN.equals(getOauth2GrantType())) {
462+
if (!containsMacro(PROPERTY_AUTH_URL)) {
463+
assertIsSetWithFailureCollector(getAuthUrl(), PROPERTY_AUTH_URL,
464+
reasonOauth2GrantType, failureCollector);
465+
}
466+
if (!containsMacro(PROPERTY_REFRESH_TOKEN)) {
467+
assertIsSetWithFailureCollector(getRefreshToken(), PROPERTY_REFRESH_TOKEN,
468+
reasonOauth2GrantType, failureCollector);
469+
}
470+
}
471+
failureCollector.getOrThrowException();
472+
}
426473
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright © 2025 Cask Data, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package io.cdap.plugin.http.common;
18+
19+
import java.util.Objects;
20+
21+
/**
22+
* Enum encoding the handled Oauth2 Client Authentication
23+
*/
24+
public enum OAuthClientAuthentication implements EnumWithValue {
25+
BODY("body", "Body"),
26+
REQUEST_PARAMETER("request_parameter", "Request Parameter");
27+
28+
private final String value;
29+
private final String label;
30+
31+
OAuthClientAuthentication(String value, String label) {
32+
this.value = value;
33+
this.label = label;
34+
}
35+
36+
public static OAuthClientAuthentication getClientAuthentication(String clientAuthentication) {
37+
if (Objects.equals(clientAuthentication, BODY.getLabel())) {
38+
return BODY;
39+
} else {
40+
return REQUEST_PARAMETER;
41+
}
42+
}
43+
44+
@Override
45+
public String getValue() {
46+
return value;
47+
}
48+
49+
public String getLabel() {
50+
return label;
51+
}
52+
53+
@Override
54+
public String toString() {
55+
return this.getValue();
56+
}
57+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright © 2025 Cask Data, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package io.cdap.plugin.http.common;
18+
19+
import java.util.Objects;
20+
21+
/**
22+
* Enum encoding the handled Oauth2 Grant Types
23+
*/
24+
public enum OAuthGrantType implements EnumWithValue {
25+
REFRESH_TOKEN("refresh_token", "Refresh Token"),
26+
CLIENT_CREDENTIALS("client_credentials", "Client Credentials");
27+
28+
private final String value;
29+
private final String label;
30+
31+
OAuthGrantType(String value, String label) {
32+
this.value = value;
33+
this.label = label;
34+
}
35+
36+
public static OAuthGrantType getGrantType(String oauth2GrantType) {
37+
if (oauth2GrantType == null || Objects.equals(oauth2GrantType, REFRESH_TOKEN.getLabel())) {
38+
return REFRESH_TOKEN;
39+
} else {
40+
return CLIENT_CREDENTIALS;
41+
}
42+
}
43+
44+
@Override
45+
public String getValue() {
46+
return value;
47+
}
48+
49+
public String getLabel() {
50+
return label;
51+
}
52+
53+
@Override
54+
public String toString() {
55+
return this.getValue();
56+
}
57+
}

src/main/java/io/cdap/plugin/http/common/http/HttpClient.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.apache.http.impl.client.BasicCredentialsProvider;
3333
import org.apache.http.impl.client.CloseableHttpClient;
3434
import org.apache.http.impl.client.HttpClientBuilder;
35+
import org.apache.http.impl.client.HttpClients;
3536
import org.apache.http.message.BasicHeader;
3637

3738
import java.io.Closeable;
@@ -132,7 +133,14 @@ public CloseableHttpClient createHttpClient(String pageUriStr) throws IOExceptio
132133
httpClientBuilder.setProxy(proxyHost);
133134
}
134135
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
135-
136+
ArrayList<Header> clientHeaders = new ArrayList<>();
137+
// oAuth2
138+
if (config.getOauth2Enabled()) {
139+
AccessToken oauthAccessToken = OAuthUtil.getAccessToken(HttpClients.createDefault(), config);
140+
clientHeaders.add(new BasicHeader("Authorization",
141+
String.format("Bearer %s", oauthAccessToken.getTokenValue())));
142+
}
143+
httpClientBuilder.setDefaultHeaders(clientHeaders);
136144
return httpClientBuilder.build();
137145
}
138146

src/main/java/io/cdap/plugin/http/common/http/OAuthUtil.java

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,17 @@
2121
import com.google.common.collect.ImmutableSet;
2222
import com.google.gson.JsonElement;
2323
import io.cdap.plugin.http.common.BaseHttpConfig;
24+
import io.cdap.plugin.http.common.OAuthClientAuthentication;
25+
import io.cdap.plugin.http.common.OAuthGrantType;
2426
import io.cdap.plugin.http.common.pagination.page.JSONUtil;
2527
import io.cdap.plugin.http.source.common.BaseHttpSourceConfig;
28+
import org.apache.http.client.entity.UrlEncodedFormEntity;
2629
import org.apache.http.client.methods.CloseableHttpResponse;
2730
import org.apache.http.client.methods.HttpPost;
2831
import org.apache.http.client.utils.URIBuilder;
2932
import org.apache.http.impl.client.CloseableHttpClient;
3033
import org.apache.http.impl.client.HttpClients;
34+
import org.apache.http.message.BasicNameValuePair;
3135
import org.apache.http.util.EntityUtils;
3236

3337
import java.io.ByteArrayInputStream;
@@ -37,8 +41,12 @@
3741
import java.net.URI;
3842
import java.net.URISyntaxException;
3943
import java.nio.charset.StandardCharsets;
44+
import java.time.Duration;
4045
import java.time.Instant;
46+
import java.util.ArrayList;
4147
import java.util.Date;
48+
import java.util.List;
49+
import java.util.Objects;
4250
import javax.annotation.Nullable;
4351

4452
/**
@@ -70,12 +78,77 @@ public static AccessToken getAccessToken(BaseHttpConfig config) throws IOExcepti
7078
return OAuthUtil.getAccessTokenByServiceAccount(config);
7179
case OAUTH2:
7280
try (CloseableHttpClient client = HttpClients.createDefault()) {
73-
return OAuthUtil.getAccessTokenByRefreshToken(client, config);
81+
return getAccessToken(client, (BaseHttpSourceConfig) config);
7482
}
7583
}
7684
return null;
7785
}
7886

87+
public static AccessToken getAccessToken(CloseableHttpClient httpclient,
88+
BaseHttpSourceConfig config) throws IOException {
89+
switch (config.getOauth2GrantType()) {
90+
case REFRESH_TOKEN:
91+
return getAccessTokenByRefreshToken(httpclient, config);
92+
case CLIENT_CREDENTIALS:
93+
return getAccessTokenByClientCredentials(httpclient, config.getTokenUrl(),
94+
config.getClientId(), config.getClientSecret(), config.getScopes(),
95+
config.getOauth2ClientAuthentication().getValue());
96+
default:
97+
throw new IOException("Invalid Grant Type. Cannot retrieve access token.");
98+
}
99+
}
100+
101+
private static AccessToken getAccessTokenByClientCredentials(CloseableHttpClient httpclient,
102+
String tokenUrl, String clientId, String clientSecret, String scope,
103+
String clientAuthentication) throws IOException {
104+
URI uri;
105+
HttpPost httppost;
106+
try {
107+
if (Objects.equals(clientAuthentication, OAuthClientAuthentication.BODY.getValue())) {
108+
uri = new URIBuilder(tokenUrl).build();
109+
List<BasicNameValuePair> nameValuePairs = new ArrayList<>();
110+
nameValuePairs.add(new BasicNameValuePair("scope", scope));
111+
nameValuePairs.add(
112+
new BasicNameValuePair("grant_type", OAuthGrantType.CLIENT_CREDENTIALS.getValue()));
113+
nameValuePairs.add(new BasicNameValuePair("client_id", clientId));
114+
nameValuePairs.add(new BasicNameValuePair("client_secret", clientSecret));
115+
httppost = new HttpPost(uri);
116+
httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs));
117+
} else {
118+
uri = new URIBuilder(tokenUrl).setParameter("client_id", clientId)
119+
.setParameter("client_secret", clientSecret)
120+
.setParameter("grant_type", OAuthGrantType.CLIENT_CREDENTIALS.getValue()).build();
121+
httppost = new HttpPost(uri);
122+
}
123+
} catch (URISyntaxException e) {
124+
throw new IllegalArgumentException(
125+
"Failed to build access token URI for OAuth2 with grant type = "
126+
+ OAuthGrantType.CLIENT_CREDENTIALS.getValue(), e);
127+
}
128+
129+
CloseableHttpResponse response = httpclient.execute(httppost);
130+
String responseString = EntityUtils.toString(response.getEntity(), "UTF-8");
131+
132+
JsonElement accessTokenElement = JSONUtil.toJsonObject(responseString).get("access_token");
133+
134+
if (accessTokenElement == null) {
135+
throw new IllegalArgumentException("Access token not found");
136+
}
137+
138+
JsonElement expiresInElement = JSONUtil.toJsonObject(responseString).get("expires_in");
139+
Date expiresInDate = null;
140+
if (expiresInElement != null) {
141+
Instant now = Instant.now();
142+
Duration expiresIn = Duration.ofSeconds(expiresInElement.getAsInt());
143+
Duration buffer = Duration.ofMinutes(1);
144+
145+
Instant expiresAt = now.plus(expiresIn).minus(buffer);
146+
expiresInDate = Date.from(expiresAt);
147+
}
148+
149+
return new AccessToken(accessTokenElement.getAsString(), expiresInDate);
150+
}
151+
79152
/**
80153
* Returns true only if the expiration time set in the accessToken is before the current time.
81154
* @param accessToken AccessToken instance
@@ -131,9 +204,12 @@ public static AccessToken getAccessTokenByRefreshToken(CloseableHttpClient httpc
131204
JsonElement expiresInElement = JSONUtil.toJsonObject(responseString).get("expires_in");
132205
Date expiresInDate = null;
133206
if (expiresInElement != null) {
134-
long expiresAtMilliseconds = System.currentTimeMillis()
135-
+ (long) (expiresInElement.getAsInt() * 1000) - 60000L;
136-
expiresInDate = new Date(expiresAtMilliseconds);
207+
Instant now = Instant.now();
208+
Duration expiresIn = Duration.ofSeconds(expiresInElement.getAsInt());
209+
Duration buffer = Duration.ofMinutes(1);
210+
211+
Instant expiresAt = now.plus(expiresIn).minus(buffer);
212+
expiresInDate = Date.from(expiresAt);
137213
}
138214

139215
return new AccessToken(accessTokenElement.getAsString(), expiresInDate);

0 commit comments

Comments
 (0)