6
6
7
7
import com .fasterxml .jackson .databind .JsonNode ;
8
8
import com .google .common .collect .ImmutableMap ;
9
+ import com .google .gson .Gson ;
10
+ import com .google .gson .reflect .TypeToken ;
9
11
import io .airbyte .commons .json .Jsons ;
10
12
import io .airbyte .config .persistence .ConfigNotFoundException ;
11
13
import io .airbyte .config .persistence .ConfigRepository ;
12
14
import java .io .IOException ;
15
+ import java .lang .reflect .Type ;
13
16
import java .net .URI ;
17
+ import java .net .URISyntaxException ;
14
18
import java .net .URLEncoder ;
15
19
import java .net .http .HttpClient ;
16
20
import java .net .http .HttpClient .Version ;
17
21
import java .net .http .HttpRequest ;
18
22
import java .net .http .HttpResponse ;
19
23
import java .nio .charset .StandardCharsets ;
24
+ import java .util .HashMap ;
20
25
import java .util .Map ;
21
26
import java .util .UUID ;
27
+ import java .util .function .Function ;
22
28
import java .util .function .Supplier ;
23
29
import org .apache .commons .lang3 .RandomStringUtils ;
30
+ import org .apache .http .client .utils .URIBuilder ;
24
31
25
32
/*
26
33
* Class implementing generic oAuth 2.0 flow.
27
34
*/
28
35
public abstract class BaseOAuthFlow extends BaseOAuthConfig {
29
36
30
- private final HttpClient httpClient ;
37
+ /**
38
+ * Simple enum of content type strings and their respective encoding functions used for POSTing the
39
+ * access token request
40
+ */
41
+ public enum TOKEN_REQUEST_CONTENT_TYPE {
42
+
43
+ URL_ENCODED ("application/x-www-form-urlencoded" , BaseOAuthFlow ::toUrlEncodedString ),
44
+ JSON ("application/json" , BaseOAuthFlow ::toJson );
45
+
46
+ String contentType ;
47
+ Function <Map <String , String >, String > converter ;
48
+
49
+ TOKEN_REQUEST_CONTENT_TYPE (String contentType , Function <Map <String , String >, String > converter ) {
50
+ this .contentType = contentType ;
51
+ this .converter = converter ;
52
+ }
53
+
54
+ }
55
+
56
+ protected final HttpClient httpClient ;
57
+ private final TOKEN_REQUEST_CONTENT_TYPE tokenReqContentType ;
31
58
private final Supplier <String > stateSupplier ;
32
59
33
60
public BaseOAuthFlow (final ConfigRepository configRepository ) {
34
61
this (configRepository , HttpClient .newBuilder ().version (Version .HTTP_1_1 ).build (), BaseOAuthFlow ::generateRandomState );
35
62
}
36
63
37
- public BaseOAuthFlow (final ConfigRepository configRepository , final HttpClient httpClient , final Supplier <String > stateSupplier ) {
64
+ public BaseOAuthFlow (ConfigRepository configRepository , TOKEN_REQUEST_CONTENT_TYPE tokenReqContentType ) {
65
+ this (configRepository ,
66
+ HttpClient .newBuilder ().version (Version .HTTP_1_1 ).build (),
67
+ BaseOAuthFlow ::generateRandomState ,
68
+ tokenReqContentType );
69
+ }
70
+
71
+ public BaseOAuthFlow (ConfigRepository configRepository , HttpClient httpClient , Supplier <String > stateSupplier ) {
72
+ this (configRepository , httpClient , stateSupplier , TOKEN_REQUEST_CONTENT_TYPE .URL_ENCODED );
73
+ }
74
+
75
+ public BaseOAuthFlow (ConfigRepository configRepository ,
76
+ HttpClient httpClient ,
77
+ Supplier <String > stateSupplier ,
78
+ TOKEN_REQUEST_CONTENT_TYPE tokenReqContentType ) {
38
79
super (configRepository );
39
80
this .httpClient = httpClient ;
40
81
this .stateSupplier = stateSupplier ;
82
+ this .tokenReqContentType = tokenReqContentType ;
41
83
}
42
84
43
85
@ Override
@@ -54,6 +96,31 @@ public String getDestinationConsentUrl(final UUID workspaceId, final UUID destin
54
96
return formatConsentUrl (destinationDefinitionId , getClientIdUnsafe (oAuthParamConfig ), redirectUrl );
55
97
}
56
98
99
+ protected String formatConsentUrl (String clientId ,
100
+ String redirectUrl ,
101
+ String host ,
102
+ String path ,
103
+ String scope ,
104
+ String responseType )
105
+ throws IOException {
106
+ final URIBuilder builder = new URIBuilder ()
107
+ .setScheme ("https" )
108
+ .setHost (host )
109
+ .setPath (path )
110
+ // required
111
+ .addParameter ("client_id" , clientId )
112
+ .addParameter ("redirect_uri" , redirectUrl )
113
+ .addParameter ("state" , getState ())
114
+ // optional
115
+ .addParameter ("response_type" , responseType )
116
+ .addParameter ("scope" , scope );
117
+ try {
118
+ return builder .build ().toString ();
119
+ } catch (URISyntaxException e ) {
120
+ throw new IOException ("Failed to format Consent URL for OAuth flow" , e );
121
+ }
122
+ }
123
+
57
124
/**
58
125
* Depending on the OAuth flow implementation, the URL to grant user's consent may differ,
59
126
* especially in the query parameters to be provided. This function should generate such consent URL
@@ -84,7 +151,8 @@ public Map<String, Object> completeSourceOAuth(
84
151
getClientIdUnsafe (oAuthParamConfig ),
85
152
getClientSecretUnsafe (oAuthParamConfig ),
86
153
extractCodeParameter (queryParams ),
87
- redirectUrl );
154
+ redirectUrl ,
155
+ oAuthParamConfig );
88
156
}
89
157
90
158
@ Override
@@ -98,20 +166,26 @@ public Map<String, Object> completeDestinationOAuth(final UUID workspaceId,
98
166
getClientIdUnsafe (oAuthParamConfig ),
99
167
getClientSecretUnsafe (oAuthParamConfig ),
100
168
extractCodeParameter (queryParams ),
101
- redirectUrl );
169
+ redirectUrl , oAuthParamConfig );
102
170
}
103
171
104
- private Map <String , Object > completeOAuthFlow (final String clientId , final String clientSecret , final String authCode , final String redirectUrl )
172
+ private Map <String , Object > completeOAuthFlow (final String clientId ,
173
+ final String clientSecret ,
174
+ final String authCode ,
175
+ final String redirectUrl ,
176
+ JsonNode oAuthParamConfig )
105
177
throws IOException {
178
+ var accessTokenUrl = getAccessTokenUrl (oAuthParamConfig );
106
179
final HttpRequest request = HttpRequest .newBuilder ()
107
- .POST (HttpRequest .BodyPublishers .ofString (toUrlEncodedString (getAccessTokenQueryParameters (clientId , clientSecret , authCode , redirectUrl ))))
108
- .uri (URI .create (getAccessTokenUrl ()))
109
- .header ("Content-Type" , "application/x-www-form-urlencoded" )
180
+ .POST (HttpRequest .BodyPublishers
181
+ .ofString (tokenReqContentType .converter .apply (getAccessTokenQueryParameters (clientId , clientSecret , authCode , redirectUrl ))))
182
+ .uri (URI .create (accessTokenUrl ))
183
+ .header ("Content-Type" , tokenReqContentType .contentType )
110
184
.build ();
111
185
// TODO: Handle error response to report better messages
112
186
try {
113
- final HttpResponse <String > response = httpClient .send (request , HttpResponse .BodyHandlers .ofString ());;
114
- return extractRefreshToken (Jsons .deserialize (response .body ()));
187
+ final HttpResponse <String > response = httpClient .send (request , HttpResponse .BodyHandlers .ofString ());
188
+ return extractRefreshToken (Jsons .deserialize (response .body ()), accessTokenUrl );
115
189
} catch (final InterruptedException e ) {
116
190
throw new IOException ("Failed to complete OAuth flow" , e );
117
191
}
@@ -146,13 +220,20 @@ protected String extractCodeParameter(Map<String, Object> queryParams) throws IO
146
220
/**
147
221
* Returns the URL where to retrieve the access token from.
148
222
*/
149
- protected abstract String getAccessTokenUrl ();
223
+ protected abstract String getAccessTokenUrl (JsonNode oAuthParamConfig );
150
224
151
- /**
152
- * Once the auth code is exchange for a refresh token, the oauth flow implementation can extract and
153
- * returns the values of fields to be used in the connector's configurations.
154
- */
155
- protected abstract Map <String , Object > extractRefreshToken (JsonNode data ) throws IOException ;
225
+ protected Map <String , Object > extractRefreshToken (final JsonNode data , String accessTokenUrl ) throws IOException {
226
+ final Map <String , Object > result = new HashMap <>();
227
+ if (data .has ("refresh_token" )) {
228
+ result .put ("refresh_token" , data .get ("refresh_token" ).asText ());
229
+ } else if (data .has ("access_token" )) {
230
+ result .put ("access_token" , data .get ("access_token" ).asText ());
231
+ } else {
232
+ throw new IOException (String .format ("Missing 'refresh_token' in query params from %s" , accessTokenUrl ));
233
+ }
234
+ return Map .of ("credentials" , result );
235
+
236
+ }
156
237
157
238
private static String urlEncode (final String s ) {
158
239
try {
@@ -173,4 +254,10 @@ private static String toUrlEncodedString(final Map<String, String> body) {
173
254
return result .toString ();
174
255
}
175
256
257
+ protected static String toJson (final Map <String , String > body ) {
258
+ final Gson gson = new Gson ();
259
+ Type gsonType = new TypeToken <Map <String , String >>() {}.getType ();
260
+ return gson .toJson (body , gsonType );
261
+ }
262
+
176
263
}
0 commit comments