Skip to content

Commit 5be75c1

Browse files
Jan Waśnineinchnick
authored andcommitted
Improve getting OAuth tokens
1 parent 1bf4351 commit 5be75c1

12 files changed

Lines changed: 172 additions & 81 deletions

File tree

Dockerfile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ ADD catalog/ /etc/trino/catalog/disabled/
88
ADD docker-entrypoint.sh /usr/local/bin/
99

1010
ENV OPENAPI_AUTH_TYPE=none \
11-
OPENAPI_TOKEN_ENDPOINT=/oauth/token \
1211
OPENAPI_CLIENT_ID="" \
1312
OPENAPI_CLIENT_SECRET="" \
14-
OPENAPI_GRANT_TYPE=password \
1513
OPENAPI_USERNAME="" \
1614
OPENAPI_PASSWORD="" \
1715
OPENAPI_BEARER_TOKEN="" \

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,8 @@ After reloading Trino, you should be able to connect to the `openapi` catalog.
4848
| base-uri | `OPENAPI_BASE_URI` | Base URL for the API, often includes API version |
4949
| authentication.type | `OPENAPI_AUTH_TYPE` | Default authentication type if not set in the specification. One of: `none`, `http`, `api_key`, `oauth`. |
5050
| authentication.scheme | `OPENAPI_AUTH_SCHEME` | Authentication scheme for the `http` authentication type. One of: `basic`, `bearer`. |
51-
| authentication.token-endpoint | `OPENAPI_TOKEN_ENDPOINT` | OAuth token endpoint URL |
5251
| authentication.client-id | `OPENAPI_CLIENT_ID` | OAuth Client ID |
5352
| authentication.client-secret | `OPENAPI_CLIENT_SECRET` | OAuth Client secret |
54-
| authentication.grant-type | `OPENAPI_GRANT_TYPE` | OAuth grant type |
5553
| authentication.username | `OPENAPI_USERNAME` | Username used for the `http` and `oauth` authentication types |
5654
| authentication.password | `OPENAPI_PASSWORD` | Password used for the `http` and `oauth` authentication types |
5755
| authentication.bearer-token | `OPENAPI_BEARER_TOKEN` | Bearer token for `http` authentication |

catalog/jira.properties

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@ connector.name=openapi
22
spec-location=https://developer.atlassian.com/cloud/jira/platform/swagger-v3.v3.json
33
base-uri=${ENV:JIRA_URL}
44
authentication.type=basic
5-
authentication.token-endpoint=/oauth/token
65
authentication.username=${ENV:JIRA_USERNAME}
76
authentication.password=${ENV:JIRA_PASSWORD}

catalog/openapi.properties

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ connector.name=openapi
22
spec-location=${ENV:OPENAPI_SPEC_LOCATION}
33
base-uri=${ENV:OPENAPI_BASE_URI}
44
authentication.type=${ENV:OPENAPI_AUTH_TYPE}
5-
authentication.token-endpoint=${ENV:OPENAPI_TOKEN_ENDPOINT}
65
authentication.client-id=${ENV:OPENAPI_CLIENT_ID}
76
authentication.client-secret=${ENV:OPENAPI_CLIENT_SECRET}
8-
authentication.grant-type=${ENV:OPENAPI_GRANT_TYPE}
97
authentication.username=${ENV:OPENAPI_USERNAME}
108
authentication.password=${ENV:OPENAPI_PASSWORD}
119
authentication.bearer-token=${ENV:OPENAPI_BEARER_TOKEN}

catalog/petstore.properties

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ connector.name=openapi
22
spec-location=${ENV:PETSTORE_URL}/openapi.yaml
33
base-uri=${ENV:PETSTORE_URL}/v3
44
authentication.type=oauth
5-
authentication.token-endpoint=/oauth/token
65
authentication.client-id=sample-client-id
76
authentication.client-secret=secret
8-
authentication.grant-type=password
97
authentication.username=user
108
authentication.password=user
119
authentication.api-key-name=api_key

src/main/java/pl/net/was/OpenApiConfig.java

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,8 @@ public class OpenApiConfig
4545
private String apiKeyName;
4646
private String apiKeyValue;
4747

48-
private String tokenEndpoint = "/oauth/v2/token";
4948
private String clientId;
5049
private String clientSecret;
51-
private String grantType;
5250

5351
private double maxRequestsPerSecond = Double.MAX_VALUE;
5452
private double maxSplitsPerSecond = Double.MAX_VALUE;
@@ -214,19 +212,6 @@ public OpenApiConfig setApiKeyValue(String apiKeyValue)
214212
return this;
215213
}
216214

217-
public String getTokenEndpoint()
218-
{
219-
return tokenEndpoint;
220-
}
221-
222-
@Config("authentication.token-endpoint")
223-
@ConfigDescription("OAuth token endpoint")
224-
public OpenApiConfig setTokenEndpoint(String tokenEndpoint)
225-
{
226-
this.tokenEndpoint = tokenEndpoint;
227-
return this;
228-
}
229-
230215
public String getClientId()
231216
{
232217
return clientId;
@@ -254,19 +239,6 @@ public OpenApiConfig setClientSecret(String clientSecret)
254239
return this;
255240
}
256241

257-
public String getGrantType()
258-
{
259-
return grantType;
260-
}
261-
262-
@Config("authentication.grant-type")
263-
@ConfigDescription("OAuth grant type")
264-
public OpenApiConfig setGrantType(String grantType)
265-
{
266-
this.grantType = grantType;
267-
return this;
268-
}
269-
270242
public double getMaxRequestsPerSecond()
271243
{
272244
return maxRequestsPerSecond;

src/main/java/pl/net/was/authentication/Authentication.java

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@
1414
package pl.net.was.authentication;
1515

1616
import com.fasterxml.jackson.annotation.JsonProperty;
17-
import com.google.common.base.Suppliers;
17+
import com.google.common.cache.CacheBuilder;
18+
import com.google.common.cache.CacheLoader;
19+
import com.google.common.cache.LoadingCache;
1820
import com.google.common.collect.ImmutableMap;
1921
import com.google.inject.Inject;
20-
import io.airlift.http.client.BodyGenerator;
2122
import io.airlift.http.client.HttpClient;
2223
import io.airlift.http.client.HttpRequestFilter;
2324
import io.airlift.http.client.Request;
2425
import io.swagger.v3.oas.models.PathItem;
26+
import io.swagger.v3.oas.models.security.OAuthFlow;
27+
import io.swagger.v3.oas.models.security.OAuthFlows;
2528
import io.swagger.v3.oas.models.security.SecurityRequirement;
2629
import io.swagger.v3.oas.models.security.SecurityScheme;
2730
import pl.net.was.OpenApiConfig;
@@ -33,10 +36,9 @@
3336
import java.util.List;
3437
import java.util.Locale;
3538
import java.util.Map;
36-
import java.util.function.Supplier;
3739

40+
import static com.google.common.base.MoreObjects.firstNonNull;
3841
import static com.google.common.io.BaseEncoding.base64Url;
39-
import static io.airlift.http.client.HttpUriBuilder.uriBuilderFrom;
4042
import static io.airlift.http.client.JsonResponseHandler.createJsonResponseHandler;
4143
import static io.airlift.http.client.Request.Builder.fromRequest;
4244
import static io.airlift.http.client.Request.Builder.preparePost;
@@ -66,11 +68,10 @@ public class Authentication
6668

6769
private final URI baseUri;
6870
private final HttpClient httpClient;
69-
private final BodyGenerator bodyGenerator;
70-
private final String tokenEndpoint;
7171
private final String clientId;
7272
private final String clientSecret;
73-
private final Supplier<String> token = Suppliers.memoize(this::getToken);
73+
private final LoadingCache<String, String> tokens = CacheBuilder.newBuilder()
74+
.build(CacheLoader.from(this::getToken));
7475

7576
@Inject
7677
public Authentication(OpenApiConfig config,
@@ -93,13 +94,6 @@ public Authentication(OpenApiConfig config,
9394

9495
this.baseUri = requireNonNull(config.getBaseUri(), "baseUri is null");
9596
this.httpClient = requireNonNull(httpClient, "httpClient is null");
96-
if (config.getGrantType() != null && !config.getGrantType().isEmpty()) {
97-
this.bodyGenerator = createStaticBodyGenerator(getBody(config.getGrantType(), config.getUsername(), config.getPassword()), UTF_8);
98-
}
99-
else {
100-
this.bodyGenerator = null;
101-
}
102-
this.tokenEndpoint = config.getTokenEndpoint();
10397
this.clientId = config.getClientId();
10498
this.clientSecret = config.getClientSecret();
10599
}
@@ -120,7 +114,7 @@ public Request filterRequest(Request request)
120114
applyApiKeyAuth(builder, uri, scheme);
121115
}
122116
case HTTP -> applyHttpAuth(builder, null);
123-
case OAUTH -> applyOAuth(builder);
117+
case OAUTH -> throw new UnsupportedOperationException("OAuth cannot be used as a default authentication method");
124118
}
125119
}
126120
return builder.build();
@@ -150,7 +144,17 @@ private void applyAuthFilters(Request.Builder builder, List<SecurityRequirement>
150144
switch (securitySchema.getType()) {
151145
case APIKEY -> applyApiKeyAuth(builder, uri, securitySchema);
152146
case HTTP -> applyHttpAuth(builder, securitySchema.getScheme());
153-
case OAUTH2 -> applyOAuth(builder);
147+
case OAUTH2 -> {
148+
OAuthFlows flows = requireNonNull(securitySchema.getFlows(), "flows are null");
149+
OAuthFlow flow = firstNonNull(
150+
flows.getPassword(),
151+
firstNonNull(
152+
flows.getAuthorizationCode(),
153+
firstNonNull(
154+
flows.getClientCredentials(),
155+
flows.getImplicit())));
156+
applyOAuth(builder, flow.getAuthorizationUrl());
157+
}
154158
default -> throw new IllegalArgumentException(format("Unsupported security schema %s", securitySchema.getType()));
155159
}
156160
});
@@ -208,7 +212,7 @@ private Request.Builder applyHttpAuth(Request.Builder builder, String scheme)
208212
return builder.addHeader("Authorization", value);
209213
}
210214

211-
private Request.Builder applyOAuth(Request.Builder builder)
215+
private Request.Builder applyOAuth(Request.Builder builder, String authorizationUrl)
212216
{
213217
// TODO pick one of supported securitySchema.getFlows(), instead of hardcoding clientCredentials
214218
// TODO use options as scopes
@@ -227,7 +231,7 @@ private Request.Builder applyOAuth(Request.Builder builder)
227231
write:pets: modify pets in your account
228232
read:pets: read your pets
229233
*/
230-
return builder.addHeader("Authorization", "Bearer " + token.get());
234+
return builder.addHeader("Authorization", "Bearer " + tokens.getUnchecked(authorizationUrl));
231235
}
232236

233237
private static String getAuthHeader(String scheme, String username, String password)
@@ -240,32 +244,30 @@ private static String capitalize(String input)
240244
return input.substring(0, 1).toUpperCase(Locale.ENGLISH) + input.substring(1).toLowerCase(Locale.ENGLISH);
241245
}
242246

243-
private String getToken()
247+
private String getToken(String authorizationUrl)
244248
{
245-
requireNonNull(bodyGenerator, "bodyGenerator is null");
246249
return httpClient.execute(
247250
preparePost()
248-
.setUri(uriBuilderFrom(baseUri)
249-
.replacePath(tokenEndpoint)
250-
.build())
251+
.setUri(URI.create(authorizationUrl))
251252
.setHeader("Content-Type", "application/x-www-form-urlencoded")
252-
.setHeader("Authorization", "Basic " + base64Url().encode("%s:%s".formatted(clientId, clientSecret).getBytes(UTF_8)))
253-
.setBodyGenerator(bodyGenerator)
253+
.setBodyGenerator(createStaticBodyGenerator(
254+
getBody("client_credentials", clientId, clientSecret),
255+
UTF_8))
254256
.build(),
255257
createJsonResponseHandler(jsonCodec(Authentication.TokenResponse.class)))
256258
.accessToken();
257259
}
258260

259-
private static String getBody(String grantType, String username, String password)
261+
private static String getBody(String grantType, String clientId, String clientSecret)
260262
{
261263
requireNonNull(grantType, "grantType is null");
262264
ImmutableMap.Builder<String, String> params = ImmutableMap.<String, String>builder()
263265
.put("grant_type", grantType);
264-
if (username != null && !username.isEmpty()) {
265-
params.put("username", username);
266+
if (clientId != null && !clientId.isEmpty()) {
267+
params.put("client_id", clientId);
266268
}
267-
if (password != null && !password.isEmpty()) {
268-
params.put("password", password);
269+
if (clientSecret != null && !clientSecret.isEmpty()) {
270+
params.put("client_secret", clientSecret);
269271
}
270272

271273
return params.build().entrySet().stream()
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package pl.net.was;
16+
17+
import io.trino.testing.containers.junit.ReportLeakedContainers;
18+
import org.testcontainers.containers.GenericContainer;
19+
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
20+
import org.testcontainers.utility.MountableFile;
21+
22+
import java.io.Closeable;
23+
24+
import static java.lang.String.format;
25+
import static java.util.Objects.requireNonNull;
26+
27+
public class KeycloakServer
28+
implements Closeable
29+
{
30+
private static final int API_PORT = 8080;
31+
private static final String TOKEN_PATH = "/realms/trino-realm/protocol/openid-connect/token";
32+
private final GenericContainer<?> dockerContainer;
33+
34+
public KeycloakServer()
35+
{
36+
dockerContainer = new GenericContainer<>("quay.io/keycloak/keycloak:26.1.4")
37+
.withExposedPorts(8080)
38+
.withStartupAttempts(3)
39+
.withEnv("KC_BOOTSTRAP_ADMIN_USERNAME", "admin")
40+
.withEnv("KC_BOOTSTRAP_ADMIN_PASSWORD", "admin")
41+
.withCopyFileToContainer(MountableFile.forClasspathResource("test-keycloak-realm.json", 0644), "/opt/keycloak/data/import/realm.json")
42+
.waitingFor(new HttpWaitStrategy().forPort(8080).forPath("/realms/master").forStatusCode(200));
43+
dockerContainer.withCreateContainerCmdModifier(cmd -> cmd
44+
.withCmd("start-dev", "--import-realm")
45+
.withHostConfig(requireNonNull(cmd.getHostConfig(), "hostConfig is null")
46+
.withPublishAllPorts(true)));
47+
dockerContainer.start();
48+
ReportLeakedContainers.ignoreContainerId(dockerContainer.getContainerId());
49+
}
50+
51+
public String getTokenUrl()
52+
{
53+
return format("http://%s:%s%s", dockerContainer.getHost(), dockerContainer.getMappedPort(API_PORT), TOKEN_PATH);
54+
}
55+
56+
@Override
57+
public void close()
58+
{
59+
dockerContainer.close();
60+
}
61+
}

src/test/java/pl/net/was/OpenApiQueryRunner.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public static void main(String[] args)
7272
{
7373
ImmutableMap.Builder<String, String> properties = ImmutableMap.builder();
7474
if (System.getenv("OPENAPI_SPEC_LOCATION") == null || System.getenv("OPENAPI_BASE_URI") == null) {
75-
PetStoreServer server = new PetStoreServer();
75+
PetStoreServer server = new PetStoreServer(new KeycloakServer());
7676
properties.put("spec-location", server.getSpecUrl());
7777
properties.put("base-uri", server.getApiUrl());
7878
}
@@ -83,17 +83,15 @@ public static void main(String[] args)
8383
properties.putAll(Map.of(
8484
"openApi.http-client.log.enabled", "true",
8585
"openApi.http-client.log.path", "logs",
86-
"authentication.type", requireNonNullElse(System.getenv("OPENAPI_AUTH_TYPE"), "oauth"),
86+
"authentication.type", requireNonNullElse(System.getenv("OPENAPI_AUTH_TYPE"), "api_key"),
8787
"authentication.scheme", requireNonNullElse(System.getenv("OPENAPI_AUTH_SCHEME"), "basic"),
88-
"authentication.username", requireNonNullElse(System.getenv("OPENAPI_USERNAME"), "user"),
89-
"authentication.password", requireNonNullElse(System.getenv("OPENAPI_PASSWORD"), "user"),
88+
"authentication.username", requireNonNullElse(System.getenv("OPENAPI_USERNAME"), "test"),
89+
"authentication.password", requireNonNullElse(System.getenv("OPENAPI_PASSWORD"), "abc123"),
9090
"authentication.bearer-token", requireNonNullElse(System.getenv("OPENAPI_BEARER_TOKEN"), "")));
91-
if (System.getenv("OPENAPI_GRANT_TYPE") != null) {
91+
if (System.getenv("OPENAPI_CLIENT_ID") != null) {
9292
properties.putAll(Map.of(
93-
"authentication.token-endpoint", requireNonNullElse(System.getenv("OPENAPI_TOKEN_ENDPOINT"), "/oauth/token"),
9493
"authentication.client-id", requireNonNullElse(System.getenv("OPENAPI_CLIENT_ID"), "sample-client-id"),
95-
"authentication.client-secret", requireNonNullElse(System.getenv("OPENAPI_CLIENT_SECRET"), "secret"),
96-
"authentication.grant-type", System.getenv("OPENAPI_GRANT_TYPE")));
94+
"authentication.client-secret", requireNonNullElse(System.getenv("OPENAPI_CLIENT_SECRET"), "secret")));
9795
}
9896
if (System.getenv("OPENAPI_API_KEYS") != null) {
9997
properties.put("authentication.api-keys", System.getenv("OPENAPI_API_KEYS"));

0 commit comments

Comments
 (0)