Skip to content

Commit e0ff20d

Browse files
authored
Merge pull request #718 from treblereel/oauth_http
Add OAuth authentication support to HTTP call task
2 parents 989bbeb + f367116 commit e0ff20d

36 files changed

+2142
-8
lines changed

impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ public static Builder error(String type, int status) {
2727
}
2828

2929
public static Builder communication(int status, TaskContext context, Exception ex) {
30-
return new Builder(COMM_TYPE, status)
31-
.instance(context.position().jsonPointer())
32-
.title(ex.getMessage());
30+
return communication(status, context, ex.getMessage());
31+
}
32+
33+
public static Builder communication(int status, TaskContext context, String title) {
34+
return new Builder(COMM_TYPE, status).instance(context.position().jsonPointer()).title(title);
3335
}
3436

3537
public static Builder runtime(int status, TaskContext context, Exception ex) {

impl/http/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
<groupId>io.serverlessworkflow</groupId>
1717
<artifactId>serverlessworkflow-impl-core</artifactId>
1818
</dependency>
19+
<dependency>
20+
<groupId>io.serverlessworkflow</groupId>
21+
<artifactId>serverlessworkflow-jwt</artifactId>
22+
</dependency>
1923
<dependency>
2024
<groupId>org.glassfish.jersey.media</groupId>
2125
<artifactId>jersey-media-json-jackson</artifactId>
@@ -34,6 +38,11 @@
3438
<artifactId>serverlessworkflow-impl-jackson</artifactId>
3539
<scope>test</scope>
3640
</dependency>
41+
<dependency>
42+
<groupId>io.serverlessworkflow</groupId>
43+
<artifactId>serverlessworkflow-impl-jackson-jwt</artifactId>
44+
<scope>test</scope>
45+
</dependency>
3746
<dependency>
3847
<groupId>org.junit.jupiter</groupId>
3948
<artifactId>junit-jupiter-api</artifactId>
@@ -55,5 +64,10 @@
5564
<artifactId>logback-classic</artifactId>
5665
<scope>test</scope>
5766
</dependency>
67+
<dependency>
68+
<groupId>com.squareup.okhttp3</groupId>
69+
<artifactId>mockwebserver</artifactId>
70+
<scope>test</scope>
71+
</dependency>
5872
</dependencies>
5973
</project>

impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AuthProvider.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,17 @@
2020
import io.serverlessworkflow.impl.WorkflowModel;
2121
import jakarta.ws.rs.client.Invocation;
2222

23-
@FunctionalInterface
2423
interface AuthProvider {
24+
25+
default void preRequest(
26+
Invocation.Builder builder, WorkflowContext workflow, TaskContext task, WorkflowModel model) {
27+
// Default implementation does nothing
28+
}
29+
30+
default void postRequest(WorkflowContext workflow, TaskContext task, WorkflowModel model) {
31+
// Default implementation does nothing
32+
}
33+
2534
Invocation.Builder build(
2635
Invocation.Builder builder, WorkflowContext workflow, TaskContext task, WorkflowModel model);
2736
}

impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutor.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ public CompletableFuture<WorkflowModel> apply(
154154
return CompletableFuture.supplyAsync(
155155
() -> {
156156
try {
157-
return requestFunction.apply(request, workflow, taskContext, input);
157+
authProvider.ifPresent(auth -> auth.preRequest(request, workflow, taskContext, input));
158+
WorkflowModel result = requestFunction.apply(request, workflow, taskContext, input);
159+
authProvider.ifPresent(auth -> auth.postRequest(workflow, taskContext, input));
160+
return result;
158161
} catch (WebApplicationException exception) {
159162
throw new WorkflowException(
160163
WorkflowError.communication(

impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/OAuth2AuthProvider.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,46 @@
1616
package io.serverlessworkflow.impl.executors.http;
1717

1818
import io.serverlessworkflow.api.types.OAuth2AuthenticationPolicy;
19+
import io.serverlessworkflow.api.types.Oauth2;
1920
import io.serverlessworkflow.api.types.Workflow;
21+
import io.serverlessworkflow.http.jwt.JWT;
2022
import io.serverlessworkflow.impl.TaskContext;
2123
import io.serverlessworkflow.impl.WorkflowApplication;
2224
import io.serverlessworkflow.impl.WorkflowContext;
2325
import io.serverlessworkflow.impl.WorkflowModel;
26+
import io.serverlessworkflow.impl.executors.http.oauth.OAuthRequestBuilder;
27+
import jakarta.ws.rs.client.Invocation;
2428
import jakarta.ws.rs.client.Invocation.Builder;
2529

2630
public class OAuth2AuthProvider implements AuthProvider {
2731

32+
private OAuthRequestBuilder requestBuilder;
33+
34+
private static final String BEARER_TOKEN = "%s %s";
35+
2836
public OAuth2AuthProvider(
29-
WorkflowApplication app, Workflow workflow, OAuth2AuthenticationPolicy authPolicy) {
30-
throw new UnsupportedOperationException("Oauth2 auth not supported yet");
37+
WorkflowApplication application, Workflow workflow, OAuth2AuthenticationPolicy authPolicy) {
38+
Oauth2 oauth2 = authPolicy.getOauth2();
39+
if (oauth2.getOAuth2ConnectAuthenticationProperties() != null) {
40+
this.requestBuilder = new OAuthRequestBuilder(application, oauth2);
41+
} else if (oauth2.getOAuth2AuthenticationPolicySecret() != null) {
42+
throw new UnsupportedOperationException("Secrets are still not supported");
43+
}
3144
}
3245

3346
@Override
3447
public Builder build(
3548
Builder builder, WorkflowContext workflow, TaskContext task, WorkflowModel model) {
36-
// TODO Auto-generated method stub
3749
return builder;
3850
}
51+
52+
@Override
53+
public void preRequest(
54+
Invocation.Builder builder, WorkflowContext workflow, TaskContext task, WorkflowModel model) {
55+
JWT token = requestBuilder.build(workflow, task, model).validateAndGet();
56+
String tokenType = (String) token.getClaim("typ");
57+
builder.header(
58+
AuthProviderFactory.AUTH_HEADER_NAME,
59+
String.format(BEARER_TOKEN, tokenType, token.getToken()));
60+
}
3961
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of 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,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.impl.executors.http.oauth;
17+
18+
import io.serverlessworkflow.http.jwt.JWT;
19+
import io.serverlessworkflow.http.jwt.JWTConverter;
20+
import io.serverlessworkflow.impl.TaskContext;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.ServiceLoader;
24+
25+
public class AccessTokenProvider {
26+
27+
private final TokenResponseHandler tokenResponseHandler = new TokenResponseHandler();
28+
29+
private final TaskContext context;
30+
private final List<String> issuers;
31+
private final InvocationHolder invocation;
32+
33+
private final JWTConverter jwtConverter;
34+
35+
AccessTokenProvider(InvocationHolder invocation, TaskContext context, List<String> issuers) {
36+
this.invocation = invocation;
37+
this.issuers = issuers;
38+
this.context = context;
39+
40+
this.jwtConverter =
41+
ServiceLoader.load(JWTConverter.class)
42+
.findFirst()
43+
.orElseThrow(() -> new IllegalStateException("No JWTConverter implementation found"));
44+
}
45+
46+
public JWT validateAndGet() {
47+
Map<String, Object> token = tokenResponseHandler.apply(invocation, context);
48+
JWT jwt = jwtConverter.fromToken((String) token.get("access_token"));
49+
if (!(issuers == null || issuers.isEmpty())) {
50+
String tokenIssuer = (String) jwt.getClaim("iss");
51+
if (tokenIssuer == null || tokenIssuer.isEmpty() || !issuers.contains(tokenIssuer)) {
52+
throw new IllegalStateException("Token issuer is not valid: " + tokenIssuer);
53+
}
54+
}
55+
return jwt;
56+
}
57+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of 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,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.impl.executors.http.oauth;
17+
18+
import static io.serverlessworkflow.api.types.OAuth2AutenthicationData.OAuth2AutenthicationDataGrant.CLIENT_CREDENTIALS;
19+
import static io.serverlessworkflow.api.types.OAuth2AutenthicationData.OAuth2AutenthicationDataGrant.PASSWORD;
20+
21+
import io.serverlessworkflow.api.types.OAuth2AutenthicationData;
22+
import io.serverlessworkflow.api.types.Oauth2;
23+
import java.util.Base64;
24+
25+
class ClientSecretBasic {
26+
27+
private final Oauth2 oauth2;
28+
29+
public ClientSecretBasic(Oauth2 oauth2) {
30+
this.oauth2 = oauth2;
31+
}
32+
33+
public void execute(HttpRequestBuilder requestBuilder) {
34+
OAuth2AutenthicationData authenticationData =
35+
oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData();
36+
if (authenticationData.getGrant().equals(PASSWORD)) {
37+
password(requestBuilder, authenticationData);
38+
} else if (authenticationData.getGrant().equals(CLIENT_CREDENTIALS)) {
39+
clientCredentials(requestBuilder, authenticationData);
40+
} else {
41+
throw new UnsupportedOperationException(
42+
"Unsupported grant type: " + authenticationData.getGrant());
43+
}
44+
}
45+
46+
private void clientCredentials(
47+
HttpRequestBuilder requestBuilder, OAuth2AutenthicationData authenticationData) {
48+
if (authenticationData.getClient() == null
49+
|| authenticationData.getClient().getId() == null
50+
|| authenticationData.getClient().getSecret() == null) {
51+
throw new IllegalArgumentException(
52+
"Client ID and secret must be provided for client authentication");
53+
}
54+
55+
String idAndSecret =
56+
authenticationData.getClient().getId() + ":" + authenticationData.getClient().getSecret();
57+
String encodedAuth = Base64.getEncoder().encodeToString(idAndSecret.getBytes());
58+
59+
requestBuilder
60+
.addHeader("Authorization", "Basic " + encodedAuth)
61+
.withRequestContentType(authenticationData.getRequest())
62+
.withGrantType(authenticationData.getGrant());
63+
}
64+
65+
private void password(
66+
HttpRequestBuilder requestBuilder, OAuth2AutenthicationData authenticationData) {
67+
if (authenticationData.getUsername() == null || authenticationData.getPassword() == null) {
68+
throw new IllegalArgumentException(
69+
"Username and password must be provided for password grant type");
70+
}
71+
if (authenticationData.getClient() == null
72+
|| authenticationData.getClient().getId() == null
73+
|| authenticationData.getClient().getSecret() == null) {
74+
throw new IllegalArgumentException(
75+
"Client ID and secret must be provided for client authentication");
76+
}
77+
78+
String idAndSecret =
79+
authenticationData.getClient().getId() + ":" + authenticationData.getClient().getSecret();
80+
String encodedAuth = Base64.getEncoder().encodeToString(idAndSecret.getBytes());
81+
82+
requestBuilder
83+
.withGrantType(authenticationData.getGrant())
84+
.withRequestContentType(authenticationData.getRequest())
85+
.addHeader("Authorization", "Basic " + encodedAuth)
86+
.addQueryParam("username", authenticationData.getUsername())
87+
.addQueryParam("password", authenticationData.getPassword());
88+
}
89+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of 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,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.impl.executors.http.oauth;
17+
18+
import static io.serverlessworkflow.api.types.OAuth2AutenthicationData.OAuth2AutenthicationDataGrant.CLIENT_CREDENTIALS;
19+
import static io.serverlessworkflow.api.types.OAuth2AutenthicationData.OAuth2AutenthicationDataGrant.PASSWORD;
20+
21+
import io.serverlessworkflow.api.types.OAuth2AutenthicationData;
22+
import io.serverlessworkflow.api.types.Oauth2;
23+
24+
class ClientSecretPostStep {
25+
private final Oauth2 oauth2;
26+
27+
public ClientSecretPostStep(Oauth2 oauth2) {
28+
this.oauth2 = oauth2;
29+
}
30+
31+
public void execute(HttpRequestBuilder requestBuilder) {
32+
OAuth2AutenthicationData authenticationData =
33+
oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData();
34+
35+
if (authenticationData.getGrant().equals(PASSWORD)) {
36+
password(requestBuilder, authenticationData);
37+
} else if (authenticationData.getGrant().equals(CLIENT_CREDENTIALS)) {
38+
clientCredentials(requestBuilder, authenticationData);
39+
} else {
40+
throw new UnsupportedOperationException(
41+
"Unsupported grant type: " + authenticationData.getGrant());
42+
}
43+
}
44+
45+
private void clientCredentials(
46+
HttpRequestBuilder requestBuilder, OAuth2AutenthicationData authenticationData) {
47+
if (authenticationData.getClient() == null
48+
|| authenticationData.getClient().getId() == null
49+
|| authenticationData.getClient().getSecret() == null) {
50+
throw new IllegalArgumentException(
51+
"Client ID and secret must be provided for client authentication");
52+
}
53+
54+
requestBuilder
55+
.withGrantType(authenticationData.getGrant())
56+
.withRequestContentType(authenticationData.getRequest())
57+
.addQueryParam("client_id", authenticationData.getClient().getId())
58+
.addQueryParam("client_secret", authenticationData.getClient().getSecret());
59+
}
60+
61+
private void password(
62+
HttpRequestBuilder requestBuilder, OAuth2AutenthicationData authenticationData) {
63+
if (authenticationData.getUsername() == null || authenticationData.getPassword() == null) {
64+
throw new IllegalArgumentException(
65+
"Username and password must be provided for password grant type");
66+
}
67+
if (authenticationData.getClient() == null
68+
|| authenticationData.getClient().getId() == null
69+
|| authenticationData.getClient().getSecret() == null) {
70+
throw new IllegalArgumentException(
71+
"Client ID and secret must be provided for client authentication");
72+
}
73+
74+
requestBuilder
75+
.withGrantType(authenticationData.getGrant())
76+
.withRequestContentType(authenticationData.getRequest())
77+
.addQueryParam("client_id", authenticationData.getClient().getId())
78+
.addQueryParam("client_secret", authenticationData.getClient().getSecret())
79+
.addQueryParam("username", authenticationData.getUsername())
80+
.addQueryParam("password", authenticationData.getPassword());
81+
}
82+
}

0 commit comments

Comments
 (0)