Skip to content

Commit e506fc1

Browse files
psainicsAmit-CloudSufi
authored andcommitted
Add 'client_credentials' OAuth2 authentication flow
1 parent 333f916 commit e506fc1

9 files changed

Lines changed: 570 additions & 111 deletions

File tree

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

Lines changed: 172 additions & 97 deletions
Large diffs are not rendered by default.

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

Lines changed: 11 additions & 0 deletions
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;
@@ -133,6 +134,16 @@ public CloseableHttpClient createHttpClient(String pageUriStr) throws IOExceptio
133134
}
134135
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
135136

137+
ArrayList<Header> clientHeaders = new ArrayList<>();
138+
139+
// oAuth2
140+
if (config.getOauth2Enabled()) {
141+
AccessToken accessToken = OAuthUtil.getAccessToken(HttpClients.createDefault(), config);
142+
clientHeaders.add(new BasicHeader("Authorization", "Bearer " + accessToken.getTokenValue()));
143+
}
144+
145+
httpClientBuilder.setDefaultHeaders(clientHeaders);
146+
136147
return httpClientBuilder.build();
137148
}
138149

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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.http;
18+
19+
import io.cdap.plugin.http.common.EnumWithValue;
20+
import java.util.Objects;
21+
22+
/**
23+
* Enum encoding the handled Oauth2 Client Authentication
24+
*/
25+
public enum OAuthClientAuthentication implements EnumWithValue {
26+
BODY("body", "Body"),
27+
REQUEST_PARAMETER("request_parameter", "Request Parameter");
28+
29+
private final String value;
30+
private final String label;
31+
32+
OAuthClientAuthentication(String value, String label) {
33+
this.value = value;
34+
this.label = label;
35+
}
36+
37+
public static OAuthClientAuthentication getClientAuthentication(String clientAuthentication) {
38+
if (Objects.equals(clientAuthentication, BODY.getLabel())) {
39+
return BODY;
40+
} else {
41+
return REQUEST_PARAMETER;
42+
}
43+
}
44+
45+
@Override
46+
public String getValue() {
47+
return value;
48+
}
49+
50+
public String getLabel() {
51+
return label;
52+
}
53+
54+
@Override
55+
public String toString() {
56+
return this.getValue();
57+
}
58+
}
59+
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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.http;
18+
19+
import io.cdap.plugin.http.common.EnumWithValue;
20+
import java.util.Objects;
21+
22+
/**
23+
* Enum encoding the handled Oauth2 Grant Types
24+
*/
25+
public enum OAuthGrantType implements EnumWithValue {
26+
REFRESH_TOKEN("refresh token", "Refresh Token"),
27+
CLIENT_CREDENTIALS("client_credentials", "Client Credentials");
28+
29+
private final String value;
30+
private final String label;
31+
32+
OAuthGrantType(String value, String label) {
33+
this.value = value;
34+
this.label = label;
35+
}
36+
37+
public static OAuthGrantType getGrantType(String oauth2GrantType) {
38+
if (Objects.equals(oauth2GrantType, REFRESH_TOKEN.getLabel())) {
39+
return REFRESH_TOKEN;
40+
} else {
41+
return CLIENT_CREDENTIALS;
42+
}
43+
}
44+
45+
@Override
46+
public String getValue() {
47+
return value;
48+
}
49+
50+
public String getLabel() {
51+
return label;
52+
}
53+
54+
@Override
55+
public String toString() {
56+
return this.getValue();
57+
}
58+
}

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

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@
2323
import io.cdap.plugin.http.common.BaseHttpConfig;
2424
import io.cdap.plugin.http.common.pagination.page.JSONUtil;
2525
import io.cdap.plugin.http.source.common.BaseHttpSourceConfig;
26+
import org.apache.http.client.entity.UrlEncodedFormEntity;
2627
import org.apache.http.client.methods.CloseableHttpResponse;
2728
import org.apache.http.client.methods.HttpPost;
2829
import org.apache.http.client.utils.URIBuilder;
2930
import org.apache.http.impl.client.CloseableHttpClient;
3031
import org.apache.http.impl.client.HttpClients;
32+
import org.apache.http.message.BasicHeader;
33+
import org.apache.http.message.BasicNameValuePair;
3134
import org.apache.http.util.EntityUtils;
3235

3336
import java.io.ByteArrayInputStream;
@@ -38,7 +41,10 @@
3841
import java.net.URISyntaxException;
3942
import java.nio.charset.StandardCharsets;
4043
import java.time.Instant;
44+
import java.util.ArrayList;
45+
import java.util.Base64;
4146
import java.util.Date;
47+
import java.util.List;
4248
import javax.annotation.Nullable;
4349

4450
/**
@@ -70,12 +76,102 @@ public static AccessToken getAccessToken(BaseHttpConfig config) throws IOExcepti
7076
return OAuthUtil.getAccessTokenByServiceAccount(config);
7177
case OAUTH2:
7278
try (CloseableHttpClient client = HttpClients.createDefault()) {
73-
return OAuthUtil.getAccessTokenByRefreshToken(client, config);
79+
return getAccessToken(client, (BaseHttpSourceConfig) config);
7480
}
7581
}
7682
return null;
7783
}
7884

85+
public static AccessToken getAccessToken(CloseableHttpClient httpclient,
86+
BaseHttpSourceConfig config)
87+
throws IOException {
88+
switch (config.getOauth2GrantType()) {
89+
case REFRESH_TOKEN:
90+
return getAccessTokenByRefreshToken(httpclient, config);
91+
case CLIENT_CREDENTIALS:
92+
return getAccessTokenByClientCredentials(httpclient, config.getTokenUrl(),
93+
config.getClientId(), config.getClientSecret(), config.getScopes(),
94+
config.getOauth2ClientAuthentication().getValue());
95+
default:
96+
throw new IOException("Invalid Grant Type. Cannot retrieve access token.");
97+
}
98+
}
99+
100+
public static String getAccessTokenByRefreshToken(CloseableHttpClient httpclient, String tokenUrl,
101+
String clientId,
102+
String clientSecret, String refreshToken, String clientAuthentication)
103+
throws IOException {
104+
105+
URI uri;
106+
try {
107+
uri = new URIBuilder(tokenUrl)
108+
.setParameter("`client_id`", clientId)
109+
.setParameter("client_secret", clientSecret)
110+
.setParameter("refresh_token", refreshToken)
111+
.setParameter("grant_type", OAuthGrantType.REFRESH_TOKEN.getValue())
112+
.build();
113+
} catch (URISyntaxException e) {
114+
throw new IllegalArgumentException(
115+
"Failed to build access token URI for OAuth2 with grant type = " +
116+
OAuthGrantType.REFRESH_TOKEN.getValue(), e);
117+
}
118+
119+
HttpPost httppost = new HttpPost(uri);
120+
CloseableHttpResponse response = httpclient.execute(httppost);
121+
String responseString = EntityUtils.toString(response.getEntity(), "UTF-8");
122+
123+
JsonElement jsonElement = JSONUtil.toJsonObject(responseString).get("access_token");
124+
return jsonElement.getAsString();
125+
}
126+
127+
private static AccessToken getAccessTokenByClientCredentials(CloseableHttpClient httpclient,
128+
String tokenUrl,
129+
String clientId, String clientSecret, String scope, String clientAuthentication)
130+
throws IOException {
131+
URI uri;
132+
try {
133+
uri = new URIBuilder(tokenUrl)
134+
.build();
135+
} catch (URISyntaxException e) {
136+
throw new IllegalArgumentException(
137+
"Failed to build access token URI for OAuth2 with grant type = " +
138+
OAuthGrantType.CLIENT_CREDENTIALS.getValue(), e);
139+
}
140+
141+
HttpPost httppost = new HttpPost(uri);
142+
List<BasicNameValuePair> nameValuePairs = new ArrayList<>();
143+
nameValuePairs.add(new BasicNameValuePair("scope", scope));
144+
nameValuePairs.add(
145+
new BasicNameValuePair("grant_type", OAuthGrantType.CLIENT_CREDENTIALS.getValue()));
146+
nameValuePairs.add(new BasicNameValuePair("client_authentication", clientAuthentication));
147+
148+
httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs));
149+
150+
String authorizationKey =
151+
"Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes());
152+
153+
httppost.addHeader(new BasicHeader("Authorization", authorizationKey));
154+
155+
CloseableHttpResponse response = httpclient.execute(httppost);
156+
String responseString = EntityUtils.toString(response.getEntity(), "UTF-8");
157+
158+
JsonElement accessTokenElement = JSONUtil.toJsonObject(responseString).get("access_token");
159+
160+
if (accessTokenElement == null) {
161+
throw new IllegalArgumentException("Access token not found");
162+
}
163+
164+
JsonElement expiresInElement = JSONUtil.toJsonObject(responseString).get("expires_in");
165+
Date expiresInDate = null;
166+
if (expiresInElement != null) {
167+
long expiresAtMilliseconds = System.currentTimeMillis()
168+
+ (long) (expiresInElement.getAsInt() * 1000) - 60000L;
169+
expiresInDate = new Date(expiresAtMilliseconds);
170+
}
171+
172+
return new AccessToken(accessTokenElement.getAsString(), expiresInDate);
173+
}
174+
79175
/**
80176
* Returns true only if the expiration time set in the accessToken is before the current time.
81177
* @param accessToken AccessToken instance

src/main/java/io/cdap/plugin/http/source/batch/HttpBatchSourceConfig.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ private void validateOAuth2Credentials(FailureCollector collector) {
8383
}
8484

8585
try (CloseableHttpClient closeableHttpClient = httpclientBuilder.build()) {
86-
OAuthUtil.getAccessTokenByRefreshToken(closeableHttpClient, this);
86+
OAuthUtil.getAccessToken(closeableHttpClient, this);
8787
} catch (JsonSyntaxException | HttpHostConnectException e) {
8888
String errorMessage = "Error occurred during credential validation : " + e.getMessage();
8989
collector.addFailure(errorMessage, null);
@@ -151,6 +151,8 @@ private HttpBatchSourceConfig(HttpBatchSourceConfigBuilder builder) {
151151
this.proxyUrl = builder.proxyUrl;
152152
this.proxyUsername = builder.proxyUsername;
153153
this.proxyPassword = builder.proxyPassword;
154+
this.oauth2ClientAuthentication = builder.oauthClientAuthentication;
155+
this.oauth2GrantType = builder.oauthGrantType;
154156
}
155157

156158
public static HttpBatchSourceConfigBuilder builder() {
@@ -190,6 +192,8 @@ public static class HttpBatchSourceConfigBuilder {
190192
private String proxyPassword;
191193
private String username;
192194
private String password;
195+
private String oauthGrantType;
196+
private String oauthClientAuthentication;
193197

194198

195199
public HttpBatchSourceConfigBuilder setReferenceName(String referenceName) {

widgets/HTTP-batchsink.json

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,30 @@
275275
]
276276
}
277277
},
278+
{
279+
"widget-type": "select",
280+
"label": "Grant Type",
281+
"name": "oauth2GrantType",
282+
"widget-attributes": {
283+
"values": [
284+
"Refresh Token",
285+
"Client Credentials"
286+
],
287+
"default": "Refresh Token"
288+
}
289+
},
290+
{
291+
"widget-type": "select",
292+
"label": "Client Authentication",
293+
"name": "oauth2ClientAuthentication",
294+
"widget-attributes": {
295+
"values": [
296+
"Body",
297+
"Request Parameter"
298+
],
299+
"default": "Body"
300+
}
301+
},
278302
{
279303
"widget-type": "textbox",
280304
"label": "Auth URL",
@@ -466,6 +490,34 @@
466490
}
467491
]
468492
},
493+
{
494+
"name": "Authenticate with Grant type",
495+
"condition": {
496+
"property": "oauth2GrantType",
497+
"operator": "equal to",
498+
"value": "Client Credentials"
499+
},
500+
"show": [
501+
{
502+
"name": "oauth2ClientAuthentication",
503+
"type": "property"
504+
}
505+
]
506+
},
507+
{
508+
"name": "Authenticate with Grant type",
509+
"condition": {
510+
"property": "oauth2GrantType",
511+
"operator": "equal to",
512+
"value": "Refresh Token"
513+
},
514+
"show": [
515+
{
516+
"name": "refreshToken",
517+
"type": "property"
518+
}
519+
]
520+
},
469521
{
470522
"name": "Authenticate with OAuth2",
471523
"condition": {
@@ -474,6 +526,10 @@
474526
"value": "oAuth2"
475527
},
476528
"show": [
529+
{
530+
"name": "oauth2GrantType",
531+
"type": "property"
532+
},
477533
{
478534
"name": "authUrl",
479535
"type": "property"
@@ -493,10 +549,6 @@
493549
{
494550
"name": "scopes",
495551
"type": "property"
496-
},
497-
{
498-
"name": "refreshToken",
499-
"type": "property"
500552
}
501553
]
502554
},

0 commit comments

Comments
 (0)