Skip to content

Commit f0dcfd2

Browse files
Merge pull request #192 from cloudsufi/basic_auth_header
[PLUGIN-1872] Basic Auth Header changes for client_credentials grant type.
2 parents f4f3805 + 697f36f commit f0dcfd2

File tree

10 files changed

+232
-46
lines changed

10 files changed

+232
-46
lines changed

docs/HTTP-batchsink.md

+30
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,36 @@ Skip on error - Ignores erroneous records.
8787

8888
**Wait Time Between Request:** Time in milliseconds to wait between HTTP requests. Defaults to 0. (Macro enabled)
8989

90+
### Authentication
91+
92+
* **OAuth2**
93+
* **Grant Type:** Which OAuth2 grant type flow is used. It can be Refresh Token or Client Credentials Flow.
94+
* **Client Authentication:** Send OAuth2 Credentials in the Request Body or as Query Parameter or as Basic Auth
95+
Header.
96+
* **Auth URL:** Endpoint for the authorization server used to retrieve the authorization code.
97+
* **Token URL:** Endpoint for the resource server, which exchanges the authorization code for an access token.
98+
* **Client ID:** Client identifier obtained during the Application registration process.
99+
* **Client Secret:** Client secret obtained during the Application registration process.
100+
* **Scopes:** Scope of the access request, which might have multiple space-separated values.
101+
* **Refresh Token:** Token used to receive accessToken, which is end product of OAuth2.
102+
* **Service Account** - service account key used for authorization
103+
* **File Path**: Path on the local file system of the service account key used for
104+
authorization. Can be set to 'auto-detect' when running on a Dataproc cluster.
105+
When running on other clusters, the file must be present on every node in the cluster.
106+
* **JSON**: Contents of the service account JSON file.
107+
* **Scope**: The additional Google credential scopes required to access entered url, cloud-platform is included by
108+
default, visit https://developers.google.com/identity/protocols/oauth2/scopes for more information.
109+
* Scope example:
110+
111+
```
112+
https://www.googleapis.com/auth/bigquery
113+
https://www.googleapis.com/auth/cloud-platform
114+
```
115+
116+
* **Basic Authentication**
117+
* **Username:** Username for basic authentication.
118+
* **Password:** Password for basic authentication.
119+
90120
### HTTP Proxy
91121

92122
**Proxy URL:** Proxy URL. Must contain a protocol, address and port.

docs/HTTP-batchsource.md

+3
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ The newline delimiter cannot be within quotes.
214214

215215
### Authentication
216216
* **OAuth2**
217+
* **Grant Type:** Which OAuth2 grant type flow is used. It can be Refresh Token or Client Credentials Flow.
218+
* **Client Authentication:** Send OAuth2 Credentials in the Request Body or as Query Parameter or as Basic Auth
219+
Header.
217220
* **Auth URL:** Endpoint for the authorization server used to retrieve the authorization code.
218221
* **Token URL:** Endpoint for the resource server, which exchanges the authorization code for an access token.
219222
* **Client ID:** Client identifier obtained during the Application registration process.

docs/HTTP-streamingsource.md

+3
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ can be omitted as long as the field is present in schema.
209209

210210
### Authentication
211211
* **OAuth2**
212+
* **Grant Type:** Which OAuth2 grant type flow is used. It can be Refresh Token or Client Credentials Flow.
213+
* **Client Authentication:** Send OAuth2 Credentials in the Request Body or as Query Parameter or as Basic Auth
214+
Header.
212215
* **Auth URL:** Endpoint for the authorization server used to retrieve the authorization code.
213216
* **Token URL:** Endpoint for the resource server, which exchanges the authorization code for an access token.
214217
* **Client ID:** Client identifier obtained during the Application registration process.

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

+10-8
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
*/
2424
public enum OAuth2ClientAuthentication implements EnumWithValue {
2525
BODY("body", "Body"),
26-
REQUEST_PARAMETER("request_parameter", "Request Parameter");
26+
REQUEST_PARAMETER("request_parameter", "Request Parameter"),
27+
BASIC_AUTH_HEADER("basic_auth_header", "Basic Auth Header");
2728

2829
private final String value;
2930
private final String label;
@@ -37,21 +38,22 @@ public enum OAuth2ClientAuthentication implements EnumWithValue {
3738
* Determines the OAuth2 client authentication method based on the provided input.
3839
*
3940
* <p>This method checks if the given client authentication type matches the predefined
40-
* BODY authentication type. If it matches, the method returns the BODY authentication. Otherwise,
41-
* it defaults to REQUEST_PARAMETER authentication.</p>
41+
* authentication type. If it matches, the method returns the same authentication. Otherwise,
42+
* it defaults to BASIC_AUTH_HEADER authentication.</p>
4243
*
4344
* @param clientAuthentication The client authentication type as a {@link String}. It can be
44-
* either the value or the label of the BODY authentication method.
45-
* @return {@link OAuth2ClientAuthentication} The corresponding authentication type. Returns
46-
* {@code BODY} if the input matches its value or label; otherwise, returns
47-
* {@code REQUEST_PARAMETER}.
45+
* either the value or the label of the authentication method.
46+
* @return {@link OAuth2ClientAuthentication} The corresponding authentication type.
4847
*/
4948
public static OAuth2ClientAuthentication getClientAuthentication(String clientAuthentication) {
5049
if (Objects.equals(clientAuthentication, BODY.getValue()) || Objects.equals(
5150
clientAuthentication, BODY.getLabel())) {
5251
return BODY;
53-
} else {
52+
} else if (Objects.equals(clientAuthentication, REQUEST_PARAMETER.getValue()) || Objects.equals(
53+
clientAuthentication, REQUEST_PARAMETER.getLabel())) {
5454
return REQUEST_PARAMETER;
55+
} else {
56+
return BASIC_AUTH_HEADER;
5557
}
5658
}
5759

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

+73-32
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
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.OAuth2ClientAuthentication;
2524
import io.cdap.plugin.http.common.OAuth2GrantType;
2625
import io.cdap.plugin.http.common.pagination.page.JSONUtil;
2726
import io.cdap.plugin.http.source.common.BaseHttpSourceConfig;
@@ -31,6 +30,7 @@
3130
import org.apache.http.client.utils.URIBuilder;
3231
import org.apache.http.impl.client.CloseableHttpClient;
3332
import org.apache.http.impl.client.HttpClients;
33+
import org.apache.http.message.BasicHeader;
3434
import org.apache.http.message.BasicNameValuePair;
3535
import org.apache.http.util.EntityUtils;
3636

@@ -44,16 +44,22 @@
4444
import java.time.Duration;
4545
import java.time.Instant;
4646
import java.util.ArrayList;
47+
import java.util.Base64;
4748
import java.util.Date;
4849
import java.util.List;
49-
import java.util.Objects;
5050
import javax.annotation.Nullable;
5151

5252
/**
5353
* A class which contains utilities to make OAuth2 specific calls.
5454
*/
5555
public class OAuthUtil {
5656

57+
private static final String PARAM_GRANT_TYPE = "grant_type";
58+
private static final String PARAM_CLIENT_ID = "client_id";
59+
private static final String PARAM_CLIENT_SECRET = "client_secret";
60+
private static final String PARAM_REFRESH_TOKEN = "refresh_token";
61+
private static final String PARAM_SCOPE = "scope";
62+
5763
/**
5864
* Get Authorization header based on the config parameters provided
5965
*
@@ -116,8 +122,8 @@ public static AccessToken getAccessToken(CloseableHttpClient httpclient, BaseHtt
116122
* Retrieves an OAuth2 access token using the Client Credentials grant type.
117123
*
118124
* <p>This method constructs an HTTP POST request to fetch an access token from the authorization
119-
* server. The client authentication method (either "BODY" or "REQUEST") determines whether client
120-
* credentials are sent in the request body or as query parameters in the URL.</p>
125+
* server. The client authentication method (either "BODY" or "REQUEST" or "BASIC_AUTH_HEADER") determines whether
126+
* client credentials are sent in the request body or as query parameters or as basic auth header.</p>
121127
*
122128
* <p>Steps:
123129
* 1. If client authentication is set to "BODY": - Constructs a URI using the token URL. - Adds
@@ -127,7 +133,11 @@ public static AccessToken getAccessToken(CloseableHttpClient httpclient, BaseHtt
127133
* 2. If client authentication is set to "REQUEST": - Constructs a URI with client credentials as
128134
* query parameters. - Creates an HTTP POST request with the URI.
129135
* <br>
130-
* 3. Calls `fetchAccessToken(httpclient,httppost)` to execute the request and retrieve the
136+
* 3. If client authentication is set to "BASIC_AUTH_HEADER": - Constructs a URI with client credentials first
137+
* concatenated and encoded to Base64 and passed a Basic Authorization Header and
138+
* grant type and scope as part of body.
139+
* <br>
140+
* 4. Calls `fetchAccessToken(httpclient,httppost)` to execute the request and retrieve the
131141
* token.
132142
*
133143
* @param httpclient The HTTP client to execute the request.
@@ -139,29 +149,51 @@ public static AccessToken getAccessTokenByClientCredentials(CloseableHttpClient
139149
BaseHttpConfig config) throws IOException {
140150
URI uri;
141151
HttpPost httppost;
152+
142153
try {
143-
if (Objects.equals(config.getOauth2ClientAuthentication().getValue(),
144-
OAuth2ClientAuthentication.BODY.getValue())) {
145-
uri = new URIBuilder(config.getTokenUrl()).build();
146-
List<BasicNameValuePair> nameValuePairs = new ArrayList<>();
147-
nameValuePairs.add(
148-
new BasicNameValuePair("grant_type", OAuth2GrantType.CLIENT_CREDENTIALS.getValue()));
149-
nameValuePairs.add(new BasicNameValuePair("client_id", config.getClientId()));
150-
nameValuePairs.add(new BasicNameValuePair("client_secret", config.getClientSecret()));
151-
if (!Strings.isNullOrEmpty(config.getScopes())) {
152-
nameValuePairs.add(new BasicNameValuePair("scope", config.getScopes()));
153-
}
154-
httppost = new HttpPost(uri);
155-
httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs));
156-
} else {
157-
URIBuilder uriBuilder = new URIBuilder(config.getTokenUrl()).setParameter("client_id",
158-
config.getClientId()).setParameter("client_secret", config.getClientSecret())
159-
.setParameter("grant_type", OAuth2GrantType.CLIENT_CREDENTIALS.getValue());
160-
if (!Strings.isNullOrEmpty(config.getScopes())) {
161-
uriBuilder.setParameter("scope", config.getScopes());
162-
}
163-
uri = uriBuilder.build();
164-
httppost = new HttpPost(uri);
154+
List<BasicNameValuePair> nameValuePairs = new ArrayList<>();
155+
switch (config.getOauth2ClientAuthentication()) {
156+
case BODY:
157+
uri = new URIBuilder(config.getTokenUrl()).build();
158+
nameValuePairs.add(
159+
new BasicNameValuePair(PARAM_GRANT_TYPE, OAuth2GrantType.CLIENT_CREDENTIALS.getValue()));
160+
nameValuePairs.add(new BasicNameValuePair(PARAM_CLIENT_ID, config.getClientId()));
161+
nameValuePairs.add(new BasicNameValuePair(PARAM_CLIENT_SECRET, config.getClientSecret()));
162+
if (!Strings.isNullOrEmpty(config.getScopes())) {
163+
nameValuePairs.add(new BasicNameValuePair(PARAM_SCOPE, config.getScopes()));
164+
}
165+
httppost = new HttpPost(uri);
166+
httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs));
167+
break;
168+
169+
case REQUEST_PARAMETER:
170+
URIBuilder uriBuilder = new URIBuilder(config.getTokenUrl()).setParameter(PARAM_CLIENT_ID,
171+
config.getClientId())
172+
.setParameter(PARAM_CLIENT_SECRET, config.getClientSecret())
173+
.setParameter(PARAM_GRANT_TYPE, OAuth2GrantType.CLIENT_CREDENTIALS.getValue());
174+
if (!Strings.isNullOrEmpty(config.getScopes())) {
175+
uriBuilder.setParameter(PARAM_SCOPE, config.getScopes());
176+
}
177+
uri = uriBuilder.build();
178+
httppost = new HttpPost(uri);
179+
break;
180+
181+
case BASIC_AUTH_HEADER:
182+
String credentials = config.getClientId() + ":" + config.getClientSecret();
183+
String basicAuthHeader = String.format("Basic %s", Base64.getEncoder()
184+
.encodeToString(credentials.getBytes(StandardCharsets.UTF_8)));
185+
nameValuePairs.add(new BasicNameValuePair(PARAM_SCOPE, config.getScopes()));
186+
nameValuePairs.add(new BasicNameValuePair(PARAM_GRANT_TYPE, OAuth2GrantType.CLIENT_CREDENTIALS.getValue()));
187+
uri = new URIBuilder(config.getTokenUrl()).build();
188+
httppost = new HttpPost(uri);
189+
httppost.setHeader(new BasicHeader("Authorization", basicAuthHeader));
190+
httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs));
191+
break;
192+
193+
default:
194+
throw new IllegalArgumentException(
195+
String.format("Unknown OAuth client authentication '%s'",
196+
config.getOauth2ClientAuthentication().getValue()));
165197
}
166198
return fetchAccessToken(httpclient, httppost);
167199
} catch (URISyntaxException e) {
@@ -205,10 +237,10 @@ public static AccessToken getAccessTokenByRefreshToken(CloseableHttpClient httpc
205237
URI uri;
206238
try {
207239
uri = new URIBuilder(config.getTokenUrl())
208-
.setParameter("client_id", config.getClientId())
209-
.setParameter("client_secret", config.getClientSecret())
210-
.setParameter("refresh_token", config.getRefreshToken())
211-
.setParameter("grant_type", "refresh_token")
240+
.setParameter(PARAM_CLIENT_ID, config.getClientId())
241+
.setParameter(PARAM_CLIENT_SECRET, config.getClientSecret())
242+
.setParameter(PARAM_REFRESH_TOKEN, config.getRefreshToken())
243+
.setParameter(PARAM_GRANT_TYPE, OAuth2GrantType.REFRESH_TOKEN.getValue())
212244
.build();
213245
HttpPost httppost = new HttpPost(uri);
214246
return fetchAccessToken(httpclient, httppost);
@@ -232,7 +264,16 @@ private static AccessToken fetchAccessToken(CloseableHttpClient httpclient, Http
232264

233265
JsonElement accessTokenElement = JSONUtil.toJsonObject(responseString).get("access_token");
234266
if (accessTokenElement == null) {
235-
throw new IllegalArgumentException("Access token not found");
267+
String errorResponse;
268+
if (response.getStatusLine() != null) {
269+
errorResponse = String.format("Response Code: '%s', Error Message:'%s'.",
270+
response.getStatusLine().getStatusCode(),
271+
response.getStatusLine().getReasonPhrase());
272+
} else {
273+
errorResponse = response.toString();
274+
}
275+
throw new IllegalArgumentException(
276+
"Access token not found with Details: " + errorResponse);
236277
}
237278

238279
JsonElement expiresInElement = JSONUtil.toJsonObject(responseString).get("expires_in");

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import io.cdap.plugin.http.common.http.HttpClient;
2323
import io.cdap.plugin.http.common.http.OAuthUtil;
2424
import io.cdap.plugin.http.source.common.BaseHttpSourceConfig;
25-
2625
import org.apache.http.HttpEntity;
2726
import org.apache.http.HttpHost;
2827
import org.apache.http.HttpStatus;
@@ -69,7 +68,8 @@ private void validateOAuth2Credentials(FailureCollector collector) {
6968
if (!containsMacro(PROPERTY_CLIENT_ID) && !containsMacro(PROPERTY_CLIENT_SECRET) &&
7069
!containsMacro(PROPERTY_TOKEN_URL) && !containsMacro(PROPERTY_REFRESH_TOKEN) &&
7170
!containsMacro(PROPERTY_PROXY_PASSWORD) && !containsMacro(PROPERTY_PROXY_USERNAME) &&
72-
!containsMacro(PROPERTY_PROXY_URL)) {
71+
!containsMacro(PROPERTY_PROXY_URL) && !containsMacro(PROPERTY_OAUTH2_CLIENT_AUTHENTICATION) &&
72+
!containsMacro(PROPERTY_OAUTH2_GRANT_TYPE)) {
7373
HttpClientBuilder httpclientBuilder = HttpClients.custom();
7474
if (!Strings.isNullOrEmpty(getProxyUrl())) {
7575
HttpHost proxyHost = HttpHost.create(getProxyUrl());

0 commit comments

Comments
 (0)