diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java
index b72cdbb0..7381d4d9 100644
--- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java
+++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java
@@ -27,9 +27,11 @@ public static Builder error(String type, int status) {
}
public static Builder communication(int status, TaskContext context, Exception ex) {
- return new Builder(COMM_TYPE, status)
- .instance(context.position().jsonPointer())
- .title(ex.getMessage());
+ return communication(status, context, ex.getMessage());
+ }
+
+ public static Builder communication(int status, TaskContext context, String title) {
+ return new Builder(COMM_TYPE, status).instance(context.position().jsonPointer()).title(title);
}
public static Builder runtime(int status, TaskContext context, Exception ex) {
diff --git a/impl/http/pom.xml b/impl/http/pom.xml
index 516669a3..4b128a45 100644
--- a/impl/http/pom.xml
+++ b/impl/http/pom.xml
@@ -16,6 +16,10 @@
io.serverlessworkflow
serverlessworkflow-impl-core
+
+ io.serverlessworkflow
+ serverlessworkflow-jwt
+
org.glassfish.jersey.media
jersey-media-json-jackson
@@ -34,6 +38,11 @@
serverlessworkflow-impl-jackson
test
+
+ io.serverlessworkflow
+ serverlessworkflow-impl-jackson-jwt
+ test
+
org.junit.jupiter
junit-jupiter-api
@@ -55,5 +64,10 @@
logback-classic
test
+
+ com.squareup.okhttp3
+ mockwebserver
+ test
+
\ No newline at end of file
diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AuthProvider.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AuthProvider.java
index 7a902435..ef90301d 100644
--- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AuthProvider.java
+++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AuthProvider.java
@@ -20,8 +20,17 @@
import io.serverlessworkflow.impl.WorkflowModel;
import jakarta.ws.rs.client.Invocation;
-@FunctionalInterface
interface AuthProvider {
+
+ default void preRequest(
+ Invocation.Builder builder, WorkflowContext workflow, TaskContext task, WorkflowModel model) {
+ // Default implementation does nothing
+ }
+
+ default void postRequest(WorkflowContext workflow, TaskContext task, WorkflowModel model) {
+ // Default implementation does nothing
+ }
+
Invocation.Builder build(
Invocation.Builder builder, WorkflowContext workflow, TaskContext task, WorkflowModel model);
}
diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutor.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutor.java
index ee812055..80d6f14f 100644
--- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutor.java
+++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutor.java
@@ -154,7 +154,10 @@ public CompletableFuture apply(
return CompletableFuture.supplyAsync(
() -> {
try {
- return requestFunction.apply(request, workflow, taskContext, input);
+ authProvider.ifPresent(auth -> auth.preRequest(request, workflow, taskContext, input));
+ WorkflowModel result = requestFunction.apply(request, workflow, taskContext, input);
+ authProvider.ifPresent(auth -> auth.postRequest(workflow, taskContext, input));
+ return result;
} catch (WebApplicationException exception) {
throw new WorkflowException(
WorkflowError.communication(
diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/OAuth2AuthProvider.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/OAuth2AuthProvider.java
index 9a558be2..05aa263d 100644
--- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/OAuth2AuthProvider.java
+++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/OAuth2AuthProvider.java
@@ -16,24 +16,46 @@
package io.serverlessworkflow.impl.executors.http;
import io.serverlessworkflow.api.types.OAuth2AuthenticationPolicy;
+import io.serverlessworkflow.api.types.Oauth2;
import io.serverlessworkflow.api.types.Workflow;
+import io.serverlessworkflow.http.jwt.JWT;
import io.serverlessworkflow.impl.TaskContext;
import io.serverlessworkflow.impl.WorkflowApplication;
import io.serverlessworkflow.impl.WorkflowContext;
import io.serverlessworkflow.impl.WorkflowModel;
+import io.serverlessworkflow.impl.executors.http.oauth.OAuthRequestBuilder;
+import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.client.Invocation.Builder;
public class OAuth2AuthProvider implements AuthProvider {
+ private OAuthRequestBuilder requestBuilder;
+
+ private static final String BEARER_TOKEN = "%s %s";
+
public OAuth2AuthProvider(
- WorkflowApplication app, Workflow workflow, OAuth2AuthenticationPolicy authPolicy) {
- throw new UnsupportedOperationException("Oauth2 auth not supported yet");
+ WorkflowApplication application, Workflow workflow, OAuth2AuthenticationPolicy authPolicy) {
+ Oauth2 oauth2 = authPolicy.getOauth2();
+ if (oauth2.getOAuth2ConnectAuthenticationProperties() != null) {
+ this.requestBuilder = new OAuthRequestBuilder(application, oauth2);
+ } else if (oauth2.getOAuth2AuthenticationPolicySecret() != null) {
+ throw new UnsupportedOperationException("Secrets are still not supported");
+ }
}
@Override
public Builder build(
Builder builder, WorkflowContext workflow, TaskContext task, WorkflowModel model) {
- // TODO Auto-generated method stub
return builder;
}
+
+ @Override
+ public void preRequest(
+ Invocation.Builder builder, WorkflowContext workflow, TaskContext task, WorkflowModel model) {
+ JWT token = requestBuilder.build(workflow, task, model).validateAndGet();
+ String tokenType = (String) token.getClaim("typ");
+ builder.header(
+ AuthProviderFactory.AUTH_HEADER_NAME,
+ String.format(BEARER_TOKEN, tokenType, token.getToken()));
+ }
}
diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/AccessTokenProvider.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/AccessTokenProvider.java
new file mode 100644
index 00000000..0b4f6cce
--- /dev/null
+++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/AccessTokenProvider.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.serverlessworkflow.impl.executors.http.oauth;
+
+import io.serverlessworkflow.http.jwt.JWT;
+import io.serverlessworkflow.http.jwt.JWTConverter;
+import io.serverlessworkflow.impl.TaskContext;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+public class AccessTokenProvider {
+
+ private final TokenResponseHandler tokenResponseHandler = new TokenResponseHandler();
+
+ private final TaskContext context;
+ private final List issuers;
+ private final InvocationHolder invocation;
+
+ private final JWTConverter jwtConverter;
+
+ AccessTokenProvider(InvocationHolder invocation, TaskContext context, List issuers) {
+ this.invocation = invocation;
+ this.issuers = issuers;
+ this.context = context;
+
+ this.jwtConverter =
+ ServiceLoader.load(JWTConverter.class)
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("No JWTConverter implementation found"));
+ }
+
+ public JWT validateAndGet() {
+ Map token = tokenResponseHandler.apply(invocation, context);
+ JWT jwt = jwtConverter.fromToken((String) token.get("access_token"));
+ if (!(issuers == null || issuers.isEmpty())) {
+ String tokenIssuer = (String) jwt.getClaim("iss");
+ if (tokenIssuer == null || tokenIssuer.isEmpty() || !issuers.contains(tokenIssuer)) {
+ throw new IllegalStateException("Token issuer is not valid: " + tokenIssuer);
+ }
+ }
+ return jwt;
+ }
+}
diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/ClientSecretBasic.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/ClientSecretBasic.java
new file mode 100644
index 00000000..6dfbe260
--- /dev/null
+++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/ClientSecretBasic.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.serverlessworkflow.impl.executors.http.oauth;
+
+import static io.serverlessworkflow.api.types.OAuth2AutenthicationData.OAuth2AutenthicationDataGrant.CLIENT_CREDENTIALS;
+import static io.serverlessworkflow.api.types.OAuth2AutenthicationData.OAuth2AutenthicationDataGrant.PASSWORD;
+
+import io.serverlessworkflow.api.types.OAuth2AutenthicationData;
+import io.serverlessworkflow.api.types.Oauth2;
+import java.util.Base64;
+
+class ClientSecretBasic {
+
+ private final Oauth2 oauth2;
+
+ public ClientSecretBasic(Oauth2 oauth2) {
+ this.oauth2 = oauth2;
+ }
+
+ public void execute(HttpRequestBuilder requestBuilder) {
+ OAuth2AutenthicationData authenticationData =
+ oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData();
+ if (authenticationData.getGrant().equals(PASSWORD)) {
+ password(requestBuilder, authenticationData);
+ } else if (authenticationData.getGrant().equals(CLIENT_CREDENTIALS)) {
+ clientCredentials(requestBuilder, authenticationData);
+ } else {
+ throw new UnsupportedOperationException(
+ "Unsupported grant type: " + authenticationData.getGrant());
+ }
+ }
+
+ private void clientCredentials(
+ HttpRequestBuilder requestBuilder, OAuth2AutenthicationData authenticationData) {
+ if (authenticationData.getClient() == null
+ || authenticationData.getClient().getId() == null
+ || authenticationData.getClient().getSecret() == null) {
+ throw new IllegalArgumentException(
+ "Client ID and secret must be provided for client authentication");
+ }
+
+ String idAndSecret =
+ authenticationData.getClient().getId() + ":" + authenticationData.getClient().getSecret();
+ String encodedAuth = Base64.getEncoder().encodeToString(idAndSecret.getBytes());
+
+ requestBuilder
+ .addHeader("Authorization", "Basic " + encodedAuth)
+ .withRequestContentType(authenticationData.getRequest())
+ .withGrantType(authenticationData.getGrant());
+ }
+
+ private void password(
+ HttpRequestBuilder requestBuilder, OAuth2AutenthicationData authenticationData) {
+ if (authenticationData.getUsername() == null || authenticationData.getPassword() == null) {
+ throw new IllegalArgumentException(
+ "Username and password must be provided for password grant type");
+ }
+ if (authenticationData.getClient() == null
+ || authenticationData.getClient().getId() == null
+ || authenticationData.getClient().getSecret() == null) {
+ throw new IllegalArgumentException(
+ "Client ID and secret must be provided for client authentication");
+ }
+
+ String idAndSecret =
+ authenticationData.getClient().getId() + ":" + authenticationData.getClient().getSecret();
+ String encodedAuth = Base64.getEncoder().encodeToString(idAndSecret.getBytes());
+
+ requestBuilder
+ .withGrantType(authenticationData.getGrant())
+ .withRequestContentType(authenticationData.getRequest())
+ .addHeader("Authorization", "Basic " + encodedAuth)
+ .addQueryParam("username", authenticationData.getUsername())
+ .addQueryParam("password", authenticationData.getPassword());
+ }
+}
diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/ClientSecretPostStep.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/ClientSecretPostStep.java
new file mode 100644
index 00000000..e00ca28a
--- /dev/null
+++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/ClientSecretPostStep.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.serverlessworkflow.impl.executors.http.oauth;
+
+import static io.serverlessworkflow.api.types.OAuth2AutenthicationData.OAuth2AutenthicationDataGrant.CLIENT_CREDENTIALS;
+import static io.serverlessworkflow.api.types.OAuth2AutenthicationData.OAuth2AutenthicationDataGrant.PASSWORD;
+
+import io.serverlessworkflow.api.types.OAuth2AutenthicationData;
+import io.serverlessworkflow.api.types.Oauth2;
+
+class ClientSecretPostStep {
+ private final Oauth2 oauth2;
+
+ public ClientSecretPostStep(Oauth2 oauth2) {
+ this.oauth2 = oauth2;
+ }
+
+ public void execute(HttpRequestBuilder requestBuilder) {
+ OAuth2AutenthicationData authenticationData =
+ oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData();
+
+ if (authenticationData.getGrant().equals(PASSWORD)) {
+ password(requestBuilder, authenticationData);
+ } else if (authenticationData.getGrant().equals(CLIENT_CREDENTIALS)) {
+ clientCredentials(requestBuilder, authenticationData);
+ } else {
+ throw new UnsupportedOperationException(
+ "Unsupported grant type: " + authenticationData.getGrant());
+ }
+ }
+
+ private void clientCredentials(
+ HttpRequestBuilder requestBuilder, OAuth2AutenthicationData authenticationData) {
+ if (authenticationData.getClient() == null
+ || authenticationData.getClient().getId() == null
+ || authenticationData.getClient().getSecret() == null) {
+ throw new IllegalArgumentException(
+ "Client ID and secret must be provided for client authentication");
+ }
+
+ requestBuilder
+ .withGrantType(authenticationData.getGrant())
+ .withRequestContentType(authenticationData.getRequest())
+ .addQueryParam("client_id", authenticationData.getClient().getId())
+ .addQueryParam("client_secret", authenticationData.getClient().getSecret());
+ }
+
+ private void password(
+ HttpRequestBuilder requestBuilder, OAuth2AutenthicationData authenticationData) {
+ if (authenticationData.getUsername() == null || authenticationData.getPassword() == null) {
+ throw new IllegalArgumentException(
+ "Username and password must be provided for password grant type");
+ }
+ if (authenticationData.getClient() == null
+ || authenticationData.getClient().getId() == null
+ || authenticationData.getClient().getSecret() == null) {
+ throw new IllegalArgumentException(
+ "Client ID and secret must be provided for client authentication");
+ }
+
+ requestBuilder
+ .withGrantType(authenticationData.getGrant())
+ .withRequestContentType(authenticationData.getRequest())
+ .addQueryParam("client_id", authenticationData.getClient().getId())
+ .addQueryParam("client_secret", authenticationData.getClient().getSecret())
+ .addQueryParam("username", authenticationData.getUsername())
+ .addQueryParam("password", authenticationData.getPassword());
+ }
+}
diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/HttpRequestBuilder.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/HttpRequestBuilder.java
new file mode 100644
index 00000000..fff003d6
--- /dev/null
+++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/HttpRequestBuilder.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.serverlessworkflow.impl.executors.http.oauth;
+
+import static io.serverlessworkflow.api.types.OAuth2TokenRequest.Oauth2TokenRequestEncoding;
+import static io.serverlessworkflow.api.types.OAuth2TokenRequest.Oauth2TokenRequestEncoding.APPLICATION_X_WWW_FORM_URLENCODED;
+
+import io.serverlessworkflow.api.types.OAuth2AutenthicationData;
+import io.serverlessworkflow.api.types.OAuth2TokenRequest;
+import io.serverlessworkflow.impl.TaskContext;
+import io.serverlessworkflow.impl.WorkflowApplication;
+import io.serverlessworkflow.impl.WorkflowContext;
+import io.serverlessworkflow.impl.WorkflowModel;
+import io.serverlessworkflow.impl.WorkflowUtils;
+import io.serverlessworkflow.impl.WorkflowValueResolver;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.Invocation;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Form;
+import jakarta.ws.rs.core.MediaType;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+class HttpRequestBuilder {
+
+ private final Map> headers;
+
+ private final Map> queryParams;
+
+ private final WorkflowApplication app;
+
+ private URI uri;
+
+ private OAuth2AutenthicationData.OAuth2AutenthicationDataGrant grantType;
+
+ private Oauth2TokenRequestEncoding requestContentType = APPLICATION_X_WWW_FORM_URLENCODED;
+
+ HttpRequestBuilder(WorkflowApplication app) {
+ this.app = app;
+ headers = new HashMap<>();
+ queryParams = new HashMap<>();
+ }
+
+ HttpRequestBuilder addHeader(String key, String token) {
+ headers.put(key, WorkflowUtils.buildStringFilter(app, token));
+ return this;
+ }
+
+ HttpRequestBuilder addQueryParam(String key, String token) {
+ queryParams.put(key, WorkflowUtils.buildStringFilter(app, token));
+ return this;
+ }
+
+ HttpRequestBuilder withUri(URI uri) {
+ this.uri = uri;
+ return this;
+ }
+
+ HttpRequestBuilder withRequestContentType(OAuth2TokenRequest oAuth2TokenRequest) {
+ if (oAuth2TokenRequest != null) {
+ this.requestContentType = oAuth2TokenRequest.getEncoding();
+ }
+ return this;
+ }
+
+ HttpRequestBuilder withGrantType(
+ OAuth2AutenthicationData.OAuth2AutenthicationDataGrant grantType) {
+ this.grantType = grantType;
+ return this;
+ }
+
+ InvocationHolder build(WorkflowContext workflow, TaskContext task, WorkflowModel model) {
+ validate();
+
+ Client client = ClientBuilder.newClient();
+ WebTarget target = client.target(uri);
+
+ Invocation.Builder builder = target.request(MediaType.APPLICATION_JSON);
+
+ builder.header("grant_type", grantType.name().toLowerCase());
+ builder.header("User-Agent", "OAuth2-Client-Credentials/1.0");
+ builder.header("Accept", MediaType.APPLICATION_JSON);
+ builder.header("Cache-Control", "no-cache");
+
+ for (var entry : headers.entrySet()) {
+ String headerValue = entry.getValue().apply(workflow, task, model);
+ if (headerValue != null) {
+ builder.header(entry.getKey(), headerValue);
+ }
+ }
+
+ Entity> entity;
+ if (requestContentType.equals(APPLICATION_X_WWW_FORM_URLENCODED)) {
+ Form form = new Form();
+ form.param("grant_type", grantType.value());
+ queryParams.forEach(
+ (key, value) -> {
+ String resolved = value.apply(workflow, task, model);
+ form.param(key, resolved);
+ });
+ entity = Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED);
+ } else {
+ Map jsonData = new HashMap<>();
+ jsonData.put("grant_type", grantType.value());
+ queryParams.forEach(
+ (key, value) -> {
+ String resolved = value.apply(workflow, task, model);
+ jsonData.put(key, resolved);
+ });
+ entity = Entity.entity(jsonData, MediaType.APPLICATION_JSON);
+ }
+
+ return new InvocationHolder(client, () -> builder.post(entity));
+ }
+
+ private void validate() {
+ Objects.requireNonNull(uri, "URI must be set before building the request");
+ Objects.requireNonNull(grantType, "Grant type must be set before building the request");
+ }
+}
diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/InvocationHolder.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/InvocationHolder.java
new file mode 100644
index 00000000..6655be8e
--- /dev/null
+++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/InvocationHolder.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.serverlessworkflow.impl.executors.http.oauth;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Response;
+import java.io.Closeable;
+import java.util.concurrent.Callable;
+import java.util.function.Supplier;
+
+class InvocationHolder implements Callable, Closeable {
+
+ private final Client client;
+ private final Supplier call;
+
+ InvocationHolder(Client client, Supplier call) {
+ this.client = client;
+ this.call = call;
+ }
+
+ public Response call() {
+ return call.get();
+ }
+
+ public void close() {
+ if (client != null) {
+ client.close();
+ }
+ }
+}
diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/OAuthRequestBuilder.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/OAuthRequestBuilder.java
new file mode 100644
index 00000000..c78e16f2
--- /dev/null
+++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/OAuthRequestBuilder.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.serverlessworkflow.impl.executors.http.oauth;
+
+import static io.serverlessworkflow.api.types.OAuth2AutenthicationDataClient.ClientAuthentication.CLIENT_SECRET_POST;
+
+import io.serverlessworkflow.api.types.OAuth2AutenthicationData;
+import io.serverlessworkflow.api.types.OAuth2AutenthicationDataClient;
+import io.serverlessworkflow.api.types.OAuth2AuthenticationPropertiesEndpoints;
+import io.serverlessworkflow.api.types.Oauth2;
+import io.serverlessworkflow.impl.TaskContext;
+import io.serverlessworkflow.impl.WorkflowApplication;
+import io.serverlessworkflow.impl.WorkflowContext;
+import io.serverlessworkflow.impl.WorkflowModel;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class OAuthRequestBuilder {
+
+ private final Oauth2 oauth2;
+
+ private final OAuth2AutenthicationData authenticationData;
+
+ private final WorkflowApplication application;
+
+ private List issuers;
+
+ private final Map defaults =
+ Map.of(
+ "endpoints.token", "oauth2/token",
+ "endpoints.revocation", "oauth2/revoke",
+ "endpoints.introspection", "oauth2/introspect");
+
+ public OAuthRequestBuilder(WorkflowApplication application, Oauth2 oauth2) {
+ this.oauth2 = oauth2;
+ this.authenticationData =
+ oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData();
+ this.application = application;
+ }
+
+ public AccessTokenProvider build(
+ WorkflowContext workflow, TaskContext task, WorkflowModel model) {
+ HttpRequestBuilder requestBuilder = new HttpRequestBuilder(application);
+
+ requestEncoding(requestBuilder);
+ authenticationURI(requestBuilder);
+ audience(requestBuilder);
+ scope(requestBuilder);
+ issuers();
+ authenticationMethod(requestBuilder);
+
+ return new AccessTokenProvider(requestBuilder.build(workflow, task, model), task, issuers);
+ }
+
+ private void authenticationMethod(HttpRequestBuilder requestBuilder) {
+ switch (getClientAuthentication()) {
+ case CLIENT_SECRET_BASIC:
+ clientSecretBasic(requestBuilder);
+ case CLIENT_SECRET_JWT:
+ throw new UnsupportedOperationException("Client Secret JWT is not supported yet");
+ case PRIVATE_KEY_JWT:
+ throw new UnsupportedOperationException("Private Key JWT is not supported yet");
+ default:
+ clientSecretPost(requestBuilder);
+ }
+ }
+
+ private void clientSecretBasic(HttpRequestBuilder requestBuilder) {
+ new ClientSecretBasic(oauth2).execute(requestBuilder);
+ }
+
+ private void clientSecretPost(HttpRequestBuilder requestBuilder) {
+ new ClientSecretPostStep(oauth2).execute(requestBuilder);
+ }
+
+ private OAuth2AutenthicationDataClient.ClientAuthentication getClientAuthentication() {
+ if (authenticationData.getClient() == null
+ || authenticationData.getClient().getAuthentication() == null) {
+ return CLIENT_SECRET_POST;
+ }
+ return authenticationData.getClient().getAuthentication();
+ }
+
+ private void issuers() {
+ issuers =
+ oauth2
+ .getOAuth2ConnectAuthenticationProperties()
+ .getOAuth2AutenthicationData()
+ .getIssuers();
+ }
+
+ public void audience(HttpRequestBuilder requestBuilder) {
+ if (authenticationData.getAudiences() != null && !authenticationData.getAudiences().isEmpty()) {
+ String audiences = String.join(" ", authenticationData.getAudiences());
+ requestBuilder.addQueryParam("audience", audiences);
+ }
+ }
+
+ private void scope(HttpRequestBuilder requestBuilder) {
+ List scopesList = authenticationData.getScopes();
+ if (scopesList == null || scopesList.isEmpty()) {
+ return;
+ }
+ String scope =
+ scopesList.stream()
+ .filter(Objects::nonNull)
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .flatMap(s -> Arrays.stream(s.split("\\s+")))
+ .distinct()
+ .collect(Collectors.joining(" "));
+
+ if (!scope.isEmpty()) {
+ requestBuilder.addQueryParam("scope", scope);
+ }
+ }
+
+ private void authenticationURI(HttpRequestBuilder requestBuilder) {
+ OAuth2AuthenticationPropertiesEndpoints endpoints =
+ oauth2
+ .getOAuth2ConnectAuthenticationProperties()
+ .getOAuth2ConnectAuthenticationProperties()
+ .getEndpoints();
+
+ String baseUri =
+ oauth2
+ .getOAuth2ConnectAuthenticationProperties()
+ .getOAuth2AutenthicationData()
+ .getAuthority()
+ .getLiteralUri()
+ .toString()
+ .replaceAll("/$", "");
+ String tokenPath = defaults.get("endpoints.token");
+ if (endpoints != null && endpoints.getToken() != null) {
+ tokenPath = endpoints.getToken().replaceAll("^/", "");
+ }
+ requestBuilder.withUri(URI.create(baseUri + "/" + tokenPath));
+ }
+
+ public void requestEncoding(HttpRequestBuilder requestBuilder) {
+ if (authenticationData.getRequest() != null
+ && authenticationData.getRequest().getEncoding() != null) {
+ requestBuilder.addHeader(
+ "Content-Type", authenticationData.getRequest().getEncoding().value());
+ } else {
+ requestBuilder.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
+ }
+ }
+}
diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/TokenResponseHandler.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/TokenResponseHandler.java
new file mode 100644
index 00000000..a04998e8
--- /dev/null
+++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/TokenResponseHandler.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.serverlessworkflow.impl.executors.http.oauth;
+
+import io.serverlessworkflow.impl.TaskContext;
+import io.serverlessworkflow.impl.WorkflowError;
+import io.serverlessworkflow.impl.WorkflowException;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.ResponseProcessingException;
+import jakarta.ws.rs.core.GenericType;
+import jakarta.ws.rs.core.Response;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+public class TokenResponseHandler
+ implements BiFunction> {
+
+ @Override
+ public Map apply(InvocationHolder invocation, TaskContext context) {
+ try (Response response = invocation.call()) {
+ if (response.getStatus() < 200 || response.getStatus() >= 300) {
+ throw new WorkflowException(
+ WorkflowError.communication(
+ response.getStatus(),
+ context,
+ "Failed to obtain token: HTTP "
+ + response.getStatus()
+ + " — "
+ + response.getEntity())
+ .build());
+ }
+ return response.readEntity(new GenericType<>() {});
+ } catch (ResponseProcessingException e) {
+ throw new WorkflowException(
+ WorkflowError.communication(
+ e.getResponse().getStatus(),
+ context,
+ "Failed to process response: " + e.getMessage())
+ .build(),
+ e);
+ } catch (ProcessingException e) {
+ throw new WorkflowException(
+ WorkflowError.communication(
+ -1, context, "Failed to connect or process request: " + e.getMessage())
+ .build(),
+ e);
+ } finally {
+ invocation.close();
+ }
+ }
+}
diff --git a/impl/http/src/test/java/io/serverlessworkflow/impl/OAuthHTTPWorkflowDefinitionTest.java b/impl/http/src/test/java/io/serverlessworkflow/impl/OAuthHTTPWorkflowDefinitionTest.java
new file mode 100644
index 00000000..aa9d5859
--- /dev/null
+++ b/impl/http/src/test/java/io/serverlessworkflow/impl/OAuthHTTPWorkflowDefinitionTest.java
@@ -0,0 +1,900 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.serverlessworkflow.impl;
+
+import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.serverlessworkflow.api.types.Workflow;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Map;
+import okhttp3.OkHttpClient;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class OAuthHTTPWorkflowDefinitionTest {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private static final String RESPONSE =
+ """
+ {
+ "message": "Hello World"
+ }
+ """;
+
+ String TOKEN_RESPONSE_TEMPLATE =
+ """
+ {
+ "access_token": "%s",
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": "read write"
+ }
+ """;
+
+ private MockWebServer authServer;
+ private MockWebServer apiServer;
+ private OkHttpClient httpClient;
+ private String authBaseUrl;
+ private String apiBaseUrl;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ authServer = new MockWebServer();
+ authServer.start(8888);
+ authBaseUrl = "http://localhost:8888";
+
+ apiServer = new MockWebServer();
+ apiServer.start(8081);
+ apiBaseUrl = "http://localhost:8081";
+
+ httpClient = new OkHttpClient();
+ }
+
+ @AfterEach
+ void tearDown() throws IOException {
+ authServer.shutdown();
+ apiServer.shutdown();
+ }
+
+ @Test
+ public void testOAuthClientSecretPostPasswordWorkflowExecution() throws Exception {
+ String jwt = fakeAccessToken();
+ String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt);
+
+ authServer.enqueue(
+ new MockResponse()
+ .setBody(tokenResponse)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ apiServer.enqueue(
+ new MockResponse()
+ .setBody(RESPONSE)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ Workflow workflow = readWorkflowFromClasspath("oAuthClientSecretPostPasswordHttpCall.yaml");
+ Map result;
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ result =
+ app.workflowDefinition(workflow).instance(Map.of()).start().get().asMap().orElseThrow();
+ } catch (Exception e) {
+ throw new RuntimeException("Workflow execution failed", e);
+ }
+
+ assertTrue(result.containsKey("message"));
+ assertTrue(result.get("message").toString().contains("Hello World"));
+
+ RecordedRequest tokenRequest = authServer.takeRequest();
+ assertEquals("POST", tokenRequest.getMethod());
+ assertEquals("/realms/test-realm/protocol/openid-connect/token", tokenRequest.getPath());
+ assertEquals("application/x-www-form-urlencoded", tokenRequest.getHeader("Content-Type"));
+
+ String tokenRequestBody = tokenRequest.getBody().readUtf8();
+ assertTrue(tokenRequestBody.contains("grant_type=password"));
+ assertTrue(tokenRequestBody.contains("username=serverless-workflow-test"));
+ assertTrue(tokenRequestBody.contains("password=serverless-workflow-test"));
+
+ RecordedRequest petRequest = apiServer.takeRequest();
+ assertEquals("GET", petRequest.getMethod());
+ assertEquals("/hello", petRequest.getPath());
+ assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization"));
+ }
+
+ @Test
+ public void testOAuthClientSecretPostWithArgsWorkflowExecution() throws Exception {
+ String jwt = fakeAccessToken();
+ String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt);
+
+ authServer.enqueue(
+ new MockResponse()
+ .setBody(tokenResponse)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ apiServer.enqueue(
+ new MockResponse()
+ .setBody(RESPONSE)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ Workflow workflow =
+ readWorkflowFromClasspath("oAuthClientSecretPostPasswordAsArgHttpCall.yaml");
+ Map result;
+ Map params =
+ Map.of(
+ "clientId", "serverless-workflow",
+ "clientSecret", "D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT",
+ "username", "serverless-workflow-test",
+ "password", "serverless-workflow-test");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ result =
+ app.workflowDefinition(workflow).instance(params).start().get().asMap().orElseThrow();
+ } catch (Exception e) {
+ throw new RuntimeException("Workflow execution failed", e);
+ }
+
+ assertTrue(result.containsKey("message"));
+ assertTrue(result.get("message").toString().contains("Hello World"));
+
+ RecordedRequest tokenRequest = authServer.takeRequest();
+ assertEquals("POST", tokenRequest.getMethod());
+ assertEquals("/realms/test-realm/protocol/openid-connect/token", tokenRequest.getPath());
+ assertEquals("application/x-www-form-urlencoded", tokenRequest.getHeader("Content-Type"));
+
+ String tokenRequestBody = tokenRequest.getBody().readUtf8();
+ assertTrue(tokenRequestBody.contains("grant_type=password"));
+ assertTrue(tokenRequestBody.contains("username=serverless-workflow-test"));
+ assertTrue(tokenRequestBody.contains("password=serverless-workflow-test"));
+
+ RecordedRequest petRequest = apiServer.takeRequest();
+ assertEquals("GET", petRequest.getMethod());
+ assertEquals("/hello", petRequest.getPath());
+ assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization"));
+ }
+
+ @Test
+ public void testOAuthClientSecretPostWithArgsNoEndPointWorkflowExecution() throws Exception {
+ String jwt = fakeAccessToken();
+ String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt);
+
+ authServer.enqueue(
+ new MockResponse()
+ .setBody(tokenResponse)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ apiServer.enqueue(
+ new MockResponse()
+ .setBody(RESPONSE)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ Workflow workflow =
+ readWorkflowFromClasspath("oAuthClientSecretPostPasswordNoEndpointsHttpCall.yaml");
+ Map result;
+ Map params =
+ Map.of(
+ "clientId", "serverless-workflow",
+ "clientSecret", "D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT",
+ "username", "serverless-workflow-test",
+ "password", "serverless-workflow-test");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ result =
+ app.workflowDefinition(workflow).instance(params).start().get().asMap().orElseThrow();
+ } catch (Exception e) {
+ throw new RuntimeException("Workflow execution failed", e);
+ }
+
+ assertTrue(result.containsKey("message"));
+ assertTrue(result.get("message").toString().contains("Hello World"));
+
+ RecordedRequest tokenRequest = authServer.takeRequest();
+ assertEquals("POST", tokenRequest.getMethod());
+ assertEquals("/realms/test-realm/oauth2/token", tokenRequest.getPath());
+ assertEquals("application/x-www-form-urlencoded", tokenRequest.getHeader("Content-Type"));
+
+ String tokenRequestBody = tokenRequest.getBody().readUtf8();
+ assertTrue(tokenRequestBody.contains("grant_type=password"));
+ assertTrue(tokenRequestBody.contains("username=serverless-workflow-test"));
+ assertTrue(tokenRequestBody.contains("password=serverless-workflow-test"));
+
+ RecordedRequest petRequest = apiServer.takeRequest();
+ assertEquals("GET", petRequest.getMethod());
+ assertEquals("/hello", petRequest.getPath());
+ assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization"));
+ }
+
+ @Test
+ public void testOAuthClientSecretPostWithArgsAllGrantsWorkflowExecution() throws Exception {
+ String jwt = fakeAccessToken();
+ String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt);
+
+ authServer.enqueue(
+ new MockResponse()
+ .setBody(tokenResponse)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ apiServer.enqueue(
+ new MockResponse()
+ .setBody(RESPONSE)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ Workflow workflow =
+ readWorkflowFromClasspath("oAuthClientSecretPostPasswordAllGrantsHttpCall.yaml");
+ Map result;
+ Map params =
+ Map.of(
+ "clientId", "serverless-workflow",
+ "clientSecret", "D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT",
+ "username", "serverless-workflow-test",
+ "password", "serverless-workflow-test",
+ "openidScope", "openidScope",
+ "audience", "account");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ result =
+ app.workflowDefinition(workflow).instance(params).start().get().asMap().orElseThrow();
+ } catch (Exception e) {
+ throw new RuntimeException("Workflow execution failed", e);
+ }
+
+ assertTrue(result.containsKey("message"));
+ assertTrue(result.get("message").toString().contains("Hello World"));
+
+ RecordedRequest tokenRequest = authServer.takeRequest();
+ assertEquals("POST", tokenRequest.getMethod());
+ assertEquals("/realms/test-realm/oauth2/token", tokenRequest.getPath());
+ assertEquals("application/x-www-form-urlencoded", tokenRequest.getHeader("Content-Type"));
+
+ String tokenRequestBody = tokenRequest.getBody().readUtf8();
+ assertTrue(tokenRequestBody.contains("grant_type=password"));
+ assertTrue(tokenRequestBody.contains("username=serverless-workflow-test"));
+ assertTrue(tokenRequestBody.contains("password=serverless-workflow-test"));
+
+ assertTrue(
+ tokenRequestBody.contains("scope=pets%3Aread+pets%3Awrite+pets%3Adelete+pets%3Acreate"));
+ assertTrue(
+ tokenRequestBody.contains("audience=serverless-workflow+another-audience+third-audience"));
+
+ RecordedRequest petRequest = apiServer.takeRequest();
+ assertEquals("GET", petRequest.getMethod());
+ assertEquals("/hello", petRequest.getPath());
+ assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization"));
+ }
+
+ @Test
+ public void testOAuthClientSecretPostClientCredentialsWorkflowExecution() throws Exception {
+ String jwt = fakeAccessToken();
+
+ String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt);
+
+ authServer.enqueue(
+ new MockResponse()
+ .setBody(tokenResponse)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ apiServer.enqueue(
+ new MockResponse()
+ .setBody(RESPONSE)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ Workflow workflow =
+ readWorkflowFromClasspath("oAuthClientSecretPostClientCredentialsHttpCall.yaml");
+ Map result;
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ result =
+ app.workflowDefinition(workflow).instance(Map.of()).start().get().asMap().orElseThrow();
+ } catch (Exception e) {
+ throw new RuntimeException("Workflow execution failed", e);
+ }
+
+ assertTrue(result.containsKey("message"));
+ assertTrue(result.get("message").toString().contains("Hello World"));
+
+ RecordedRequest tokenRequest = authServer.takeRequest();
+ assertEquals("POST", tokenRequest.getMethod());
+ assertEquals("/realms/test-realm/protocol/openid-connect/token", tokenRequest.getPath());
+ assertEquals("application/x-www-form-urlencoded", tokenRequest.getHeader("Content-Type"));
+
+ String tokenRequestBody = tokenRequest.getBody().readUtf8();
+ assertTrue(tokenRequestBody.contains("grant_type=client_credentials"));
+ assertTrue(tokenRequestBody.contains("client_id=serverless-workflow"));
+ assertTrue(tokenRequestBody.contains("secret=D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT"));
+
+ RecordedRequest petRequest = apiServer.takeRequest();
+ assertEquals("GET", petRequest.getMethod());
+ assertEquals("/hello", petRequest.getPath());
+ assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization"));
+ }
+
+ @Test
+ public void testOAuthClientSecretPostClientCredentialsParamsWorkflowExecution() throws Exception {
+ String jwt = fakeAccessToken();
+
+ String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt);
+
+ authServer.enqueue(
+ new MockResponse()
+ .setBody(tokenResponse)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ apiServer.enqueue(
+ new MockResponse()
+ .setBody(RESPONSE)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ Workflow workflow =
+ readWorkflowFromClasspath("oAuthClientSecretPostClientCredentialsParamsHttpCall.yaml");
+ Map result;
+ Map params =
+ Map.of(
+ "clientId", "serverless-workflow",
+ "clientSecret", "D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ result =
+ app.workflowDefinition(workflow).instance(params).start().get().asMap().orElseThrow();
+ } catch (Exception e) {
+ throw new RuntimeException("Workflow execution failed", e);
+ }
+
+ assertTrue(result.containsKey("message"));
+ assertTrue(result.get("message").toString().contains("Hello World"));
+
+ RecordedRequest tokenRequest = authServer.takeRequest();
+ assertEquals("POST", tokenRequest.getMethod());
+ assertEquals("/realms/test-realm/protocol/openid-connect/token", tokenRequest.getPath());
+ assertEquals("application/x-www-form-urlencoded", tokenRequest.getHeader("Content-Type"));
+
+ String tokenRequestBody = tokenRequest.getBody().readUtf8();
+ assertTrue(tokenRequestBody.contains("grant_type=client_credentials"));
+ assertTrue(tokenRequestBody.contains("client_id=serverless-workflow"));
+ assertTrue(tokenRequestBody.contains("secret=D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT"));
+
+ RecordedRequest petRequest = apiServer.takeRequest();
+ assertEquals("GET", petRequest.getMethod());
+ assertEquals("/hello", petRequest.getPath());
+ assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization"));
+ }
+
+ @Test
+ public void testOAuthClientSecretPostClientCredentialsParamsNoEndpointWorkflowExecution()
+ throws Exception {
+ String jwt = fakeAccessToken();
+
+ String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt);
+
+ authServer.enqueue(
+ new MockResponse()
+ .setBody(tokenResponse)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ apiServer.enqueue(
+ new MockResponse()
+ .setBody(RESPONSE)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ Workflow workflow =
+ readWorkflowFromClasspath(
+ "oAuthClientSecretPostClientCredentialsParamsNoEndPointHttpCall.yaml");
+ Map result;
+ Map params =
+ Map.of(
+ "clientId", "serverless-workflow",
+ "clientSecret", "D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ result =
+ app.workflowDefinition(workflow).instance(params).start().get().asMap().orElseThrow();
+ } catch (Exception e) {
+ throw new RuntimeException("Workflow execution failed", e);
+ }
+
+ assertTrue(result.containsKey("message"));
+ assertTrue(result.get("message").toString().contains("Hello World"));
+
+ RecordedRequest tokenRequest = authServer.takeRequest();
+ assertEquals("POST", tokenRequest.getMethod());
+ assertEquals("/realms/test-realm/oauth2/token", tokenRequest.getPath());
+ assertEquals("application/x-www-form-urlencoded", tokenRequest.getHeader("Content-Type"));
+
+ String tokenRequestBody = tokenRequest.getBody().readUtf8();
+ assertTrue(tokenRequestBody.contains("grant_type=client_credentials"));
+ assertTrue(tokenRequestBody.contains("client_id=serverless-workflow"));
+ assertTrue(tokenRequestBody.contains("secret=D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT"));
+
+ RecordedRequest petRequest = apiServer.takeRequest();
+ assertEquals("GET", petRequest.getMethod());
+ assertEquals("/hello", petRequest.getPath());
+ assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization"));
+ }
+
+ @Test
+ public void testOAuthJSONPasswordWorkflowExecution() throws Exception {
+ String jwt = fakeAccessToken();
+ String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt);
+
+ authServer.enqueue(
+ new MockResponse()
+ .setBody(tokenResponse)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ apiServer.enqueue(
+ new MockResponse()
+ .setBody(RESPONSE)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ Workflow workflow = readWorkflowFromClasspath("oAuthJSONPasswordHttpCall.yaml");
+ Map result;
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ result =
+ app.workflowDefinition(workflow).instance(Map.of()).start().get().asMap().orElseThrow();
+ } catch (Exception e) {
+ throw new RuntimeException("Workflow execution failed", e);
+ }
+
+ assertTrue(result.containsKey("message"));
+ assertTrue(result.get("message").toString().contains("Hello World"));
+
+ RecordedRequest tokenRequest = authServer.takeRequest();
+ assertEquals("POST", tokenRequest.getMethod());
+
+ assertEquals("/realms/test-realm/protocol/openid-connect/token", tokenRequest.getPath());
+ assertEquals("application/json", tokenRequest.getHeader("Content-Type"));
+
+ String tokenRequestBody = tokenRequest.getBody().readUtf8();
+
+ Map asJson = MAPPER.readValue(tokenRequestBody, Map.class);
+ assertTrue(asJson.containsKey("grant_type") && asJson.get("grant_type").equals("password"));
+
+ assertTrue(
+ asJson.containsKey("client_id") && asJson.get("client_id").equals("serverless-workflow"));
+ assertTrue(
+ asJson.containsKey("client_secret")
+ && asJson.get("client_secret").equals("D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT"));
+
+ assertTrue(
+ asJson.containsKey("username")
+ && asJson.get("username").equals("serverless-workflow-test"));
+ assertTrue(
+ asJson.containsKey("password")
+ && asJson.get("password").equals("serverless-workflow-test"));
+
+ RecordedRequest petRequest = apiServer.takeRequest();
+ assertEquals("GET", petRequest.getMethod());
+ assertEquals("/hello", petRequest.getPath());
+ assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization"));
+ }
+
+ @Test
+ public void testOAuthJSONWithArgsWorkflowExecution() throws Exception {
+ String jwt = fakeAccessToken();
+ String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt);
+
+ authServer.enqueue(
+ new MockResponse()
+ .setBody(tokenResponse)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ apiServer.enqueue(
+ new MockResponse()
+ .setBody(RESPONSE)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ Workflow workflow = readWorkflowFromClasspath("oAuthJSONPasswordAsArgHttpCall.yaml");
+ Map result;
+ Map params =
+ Map.of(
+ "clientId", "serverless-workflow",
+ "clientSecret", "D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT",
+ "username", "serverless-workflow-test",
+ "password", "serverless-workflow-test");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ result =
+ app.workflowDefinition(workflow).instance(params).start().get().asMap().orElseThrow();
+ } catch (Exception e) {
+ throw new RuntimeException("Workflow execution failed", e);
+ }
+
+ assertTrue(result.containsKey("message"));
+ assertTrue(result.get("message").toString().contains("Hello World"));
+
+ RecordedRequest tokenRequest = authServer.takeRequest();
+ assertEquals("POST", tokenRequest.getMethod());
+ assertEquals("/realms/test-realm/protocol/openid-connect/token", tokenRequest.getPath());
+ assertEquals("application/json", tokenRequest.getHeader("Content-Type"));
+
+ String tokenRequestBody = tokenRequest.getBody().readUtf8();
+
+ Map asJson = MAPPER.readValue(tokenRequestBody, Map.class);
+ assertTrue(asJson.containsKey("grant_type") && asJson.get("grant_type").equals("password"));
+ assertTrue(
+ asJson.containsKey("client_id") && asJson.get("client_id").equals("serverless-workflow"));
+ assertTrue(
+ asJson.containsKey("client_secret")
+ && asJson.get("client_secret").equals("D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT"));
+ assertTrue(
+ asJson.containsKey("username")
+ && asJson.get("username").equals("serverless-workflow-test"));
+ assertTrue(
+ asJson.containsKey("password")
+ && asJson.get("password").equals("serverless-workflow-test"));
+
+ RecordedRequest petRequest = apiServer.takeRequest();
+ assertEquals("GET", petRequest.getMethod());
+ assertEquals("/hello", petRequest.getPath());
+ assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization"));
+ }
+
+ @Test
+ public void testOAuthJSONWithArgsNoEndPointWorkflowExecution() throws Exception {
+ String jwt = fakeAccessToken();
+ String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt);
+
+ authServer.enqueue(
+ new MockResponse()
+ .setBody(tokenResponse)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ apiServer.enqueue(
+ new MockResponse()
+ .setBody(RESPONSE)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ Workflow workflow = readWorkflowFromClasspath("oAuthJSONPasswordNoEndpointsHttpCall.yaml");
+ Map result;
+ Map params =
+ Map.of(
+ "clientId", "serverless-workflow",
+ "clientSecret", "D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT",
+ "username", "serverless-workflow-test",
+ "password", "serverless-workflow-test");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ result =
+ app.workflowDefinition(workflow).instance(params).start().get().asMap().orElseThrow();
+ } catch (Exception e) {
+ throw new RuntimeException("Workflow execution failed", e);
+ }
+
+ assertTrue(result.containsKey("message"));
+ assertTrue(result.get("message").toString().contains("Hello World"));
+
+ RecordedRequest tokenRequest = authServer.takeRequest();
+ assertEquals("POST", tokenRequest.getMethod());
+ assertEquals("/realms/test-realm/oauth2/token", tokenRequest.getPath());
+ assertEquals("application/json", tokenRequest.getHeader("Content-Type"));
+
+ String tokenRequestBody = tokenRequest.getBody().readUtf8();
+ Map asJson = MAPPER.readValue(tokenRequestBody, Map.class);
+ assertTrue(asJson.containsKey("grant_type") && asJson.get("grant_type").equals("password"));
+ assertTrue(
+ asJson.containsKey("client_id") && asJson.get("client_id").equals("serverless-workflow"));
+ assertTrue(
+ asJson.containsKey("client_secret")
+ && asJson.get("client_secret").equals("D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT"));
+ assertTrue(
+ asJson.containsKey("username")
+ && asJson.get("username").equals("serverless-workflow-test"));
+ assertTrue(
+ asJson.containsKey("password")
+ && asJson.get("password").equals("serverless-workflow-test"));
+
+ RecordedRequest petRequest = apiServer.takeRequest();
+ assertEquals("GET", petRequest.getMethod());
+ assertEquals("/hello", petRequest.getPath());
+ assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization"));
+ }
+
+ @Test
+ public void testOAuthJSONWithArgsAllGrantsWorkflowExecution() throws Exception {
+ String jwt = fakeAccessToken();
+ String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt);
+
+ authServer.enqueue(
+ new MockResponse()
+ .setBody(tokenResponse)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ apiServer.enqueue(
+ new MockResponse()
+ .setBody(RESPONSE)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ Workflow workflow = readWorkflowFromClasspath("oAuthJSONPasswordAllGrantsHttpCall.yaml");
+ Map result;
+ Map params =
+ Map.of(
+ "clientId", "serverless-workflow",
+ "clientSecret", "D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT",
+ "username", "serverless-workflow-test",
+ "password", "serverless-workflow-test",
+ "openidScope", "openidScope",
+ "audience", "account");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ result =
+ app.workflowDefinition(workflow).instance(params).start().get().asMap().orElseThrow();
+ } catch (Exception e) {
+ throw new RuntimeException("Workflow execution failed", e);
+ }
+
+ assertTrue(result.containsKey("message"));
+ assertTrue(result.get("message").toString().contains("Hello World"));
+
+ RecordedRequest tokenRequest = authServer.takeRequest();
+ assertEquals("POST", tokenRequest.getMethod());
+ assertEquals("/realms/test-realm/oauth2/token", tokenRequest.getPath());
+ assertEquals("application/json", tokenRequest.getHeader("Content-Type"));
+
+ String tokenRequestBody = tokenRequest.getBody().readUtf8();
+
+ Map asJson = MAPPER.readValue(tokenRequestBody, Map.class);
+ assertTrue(asJson.containsKey("grant_type") && asJson.get("grant_type").equals("password"));
+ assertTrue(
+ asJson.containsKey("client_id") && asJson.get("client_id").equals("serverless-workflow"));
+ assertTrue(
+ asJson.containsKey("client_secret")
+ && asJson.get("client_secret").equals("D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT"));
+ assertTrue(
+ asJson.containsKey("username")
+ && asJson.get("username").equals("serverless-workflow-test"));
+ assertTrue(
+ asJson.containsKey("password")
+ && asJson.get("password").equals("serverless-workflow-test"));
+
+ assertTrue(
+ asJson.containsKey("scope")
+ && asJson.get("scope").equals("pets:read pets:write pets:delete pets:create"));
+
+ assertTrue(
+ asJson.containsKey("audience")
+ && asJson
+ .get("audience")
+ .equals("serverless-workflow another-audience third-audience"));
+
+ RecordedRequest petRequest = apiServer.takeRequest();
+ assertEquals("GET", petRequest.getMethod());
+ assertEquals("/hello", petRequest.getPath());
+ assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization"));
+ }
+
+ @Test
+ public void testOAuthJSONClientCredentialsWorkflowExecution() throws Exception {
+ String jwt = fakeAccessToken();
+
+ String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt);
+
+ authServer.enqueue(
+ new MockResponse()
+ .setBody(tokenResponse)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ apiServer.enqueue(
+ new MockResponse()
+ .setBody(RESPONSE)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ Workflow workflow = readWorkflowFromClasspath("oAuthJSONClientCredentialsHttpCall.yaml");
+ Map result;
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ result =
+ app.workflowDefinition(workflow).instance(Map.of()).start().get().asMap().orElseThrow();
+ } catch (Exception e) {
+ throw new RuntimeException("Workflow execution failed", e);
+ }
+
+ assertTrue(result.containsKey("message"));
+ assertTrue(result.get("message").toString().contains("Hello World"));
+
+ RecordedRequest tokenRequest = authServer.takeRequest();
+ assertEquals("POST", tokenRequest.getMethod());
+ assertEquals("/realms/test-realm/protocol/openid-connect/token", tokenRequest.getPath());
+ assertEquals("application/json", tokenRequest.getHeader("Content-Type"));
+
+ String tokenRequestBody = tokenRequest.getBody().readUtf8();
+ Map asJson = MAPPER.readValue(tokenRequestBody, Map.class);
+ assertTrue(
+ asJson.containsKey("grant_type") && asJson.get("grant_type").equals("client_credentials"));
+ assertTrue(
+ asJson.containsKey("client_id") && asJson.get("client_id").equals("serverless-workflow"));
+ assertTrue(
+ asJson.containsKey("client_secret")
+ && asJson.get("client_secret").equals("D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT"));
+
+ RecordedRequest petRequest = apiServer.takeRequest();
+ assertEquals("GET", petRequest.getMethod());
+ assertEquals("/hello", petRequest.getPath());
+ assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization"));
+ }
+
+ @Test
+ public void testOAuthJSONClientCredentialsParamsWorkflowExecution() throws Exception {
+ String jwt = fakeAccessToken();
+
+ String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt);
+
+ authServer.enqueue(
+ new MockResponse()
+ .setBody(tokenResponse)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ apiServer.enqueue(
+ new MockResponse()
+ .setBody(RESPONSE)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ Workflow workflow = readWorkflowFromClasspath("oAuthJSONClientCredentialsParamsHttpCall.yaml");
+ Map result;
+ Map params =
+ Map.of(
+ "clientId", "serverless-workflow",
+ "clientSecret", "D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ result =
+ app.workflowDefinition(workflow).instance(params).start().get().asMap().orElseThrow();
+ } catch (Exception e) {
+ throw new RuntimeException("Workflow execution failed", e);
+ }
+
+ assertTrue(result.containsKey("message"));
+ assertTrue(result.get("message").toString().contains("Hello World"));
+
+ RecordedRequest tokenRequest = authServer.takeRequest();
+ assertEquals("POST", tokenRequest.getMethod());
+ assertEquals("/realms/test-realm/protocol/openid-connect/token", tokenRequest.getPath());
+ assertEquals("application/json", tokenRequest.getHeader("Content-Type"));
+
+ String tokenRequestBody = tokenRequest.getBody().readUtf8();
+ Map asJson = MAPPER.readValue(tokenRequestBody, Map.class);
+ assertTrue(
+ asJson.containsKey("grant_type") && asJson.get("grant_type").equals("client_credentials"));
+ assertTrue(
+ asJson.containsKey("client_id") && asJson.get("client_id").equals("serverless-workflow"));
+ assertTrue(
+ asJson.containsKey("client_secret")
+ && asJson.get("client_secret").equals("D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT"));
+
+ RecordedRequest petRequest = apiServer.takeRequest();
+ assertEquals("GET", petRequest.getMethod());
+ assertEquals("/hello", petRequest.getPath());
+ assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization"));
+ }
+
+ @Test
+ public void testOAuthJSONClientCredentialsParamsNoEndpointWorkflowExecution() throws Exception {
+ String jwt = fakeAccessToken();
+
+ String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt);
+
+ authServer.enqueue(
+ new MockResponse()
+ .setBody(tokenResponse)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ apiServer.enqueue(
+ new MockResponse()
+ .setBody(RESPONSE)
+ .setHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ Workflow workflow =
+ readWorkflowFromClasspath("oAuthJSONClientCredentialsParamsNoEndPointHttpCall.yaml");
+ Map result;
+ Map params =
+ Map.of(
+ "clientId", "serverless-workflow",
+ "clientSecret", "D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT");
+
+ try (WorkflowApplication app = WorkflowApplication.builder().build()) {
+ result =
+ app.workflowDefinition(workflow).instance(params).start().get().asMap().orElseThrow();
+ } catch (Exception e) {
+ throw new RuntimeException("Workflow execution failed", e);
+ }
+
+ assertTrue(result.containsKey("message"));
+ assertTrue(result.get("message").toString().contains("Hello World"));
+
+ RecordedRequest tokenRequest = authServer.takeRequest();
+ assertEquals("POST", tokenRequest.getMethod());
+ assertEquals("/realms/test-realm/oauth2/token", tokenRequest.getPath());
+ assertEquals("application/json", tokenRequest.getHeader("Content-Type"));
+
+ String tokenRequestBody = tokenRequest.getBody().readUtf8();
+ Map asJson = MAPPER.readValue(tokenRequestBody, Map.class);
+ assertTrue(
+ asJson.containsKey("grant_type") && asJson.get("grant_type").equals("client_credentials"));
+ assertTrue(
+ asJson.containsKey("client_id") && asJson.get("client_id").equals("serverless-workflow"));
+ assertTrue(
+ asJson.containsKey("client_secret")
+ && asJson.get("client_secret").equals("D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT"));
+
+ RecordedRequest petRequest = apiServer.takeRequest();
+ assertEquals("GET", petRequest.getMethod());
+ assertEquals("/hello", petRequest.getPath());
+ assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization"));
+ }
+
+ public static String fakeJwt(Map payload) throws Exception {
+ String headerJson =
+ MAPPER.writeValueAsString(
+ Map.of(
+ "alg", "RS256",
+ "typ", "JWT",
+ "kid", "test"));
+ String payloadJson = MAPPER.writeValueAsString(payload);
+ return b64Url(headerJson) + "." + b64Url(payloadJson) + ".sig";
+ }
+
+ private static String b64Url(String s) {
+ return Base64.getUrlEncoder()
+ .withoutPadding()
+ .encodeToString(s.getBytes(StandardCharsets.UTF_8));
+ }
+
+ public static String fakeAccessToken() throws Exception {
+ long now = Instant.now().getEpochSecond();
+ return fakeJwt(
+ Map.of(
+ "iss", "http://localhost:8888/realms/test-realm",
+ "aud", "account",
+ "sub", "test-subject",
+ "azp", "serverless-workflow",
+ "typ", "Bearer",
+ "scope", "profile email",
+ "exp", now + 3600,
+ "iat", now));
+ }
+}
diff --git a/impl/http/src/test/resources/oAuthClientSecretPostClientCredentialsHttpCall.yaml b/impl/http/src/test/resources/oAuthClientSecretPostClientCredentialsHttpCall.yaml
new file mode 100644
index 00000000..6ba976c8
--- /dev/null
+++ b/impl/http/src/test/resources/oAuthClientSecretPostClientCredentialsHttpCall.yaml
@@ -0,0 +1,23 @@
+document:
+ dsl: '1.0.0-alpha5'
+ namespace: examples
+ name: oauth2-authentication
+ version: '0.1.0'
+do:
+ - getPet:
+ call: http
+ with:
+ method: get
+ endpoint:
+ uri: http://localhost:8081/hello
+ authentication:
+ oauth2:
+ authority: http://localhost:8888/realms/test-realm
+ endpoints:
+ token: protocol/openid-connect/token
+ grant: client_credentials
+ client:
+ id: serverless-workflow
+ secret: D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT
+ issuers:
+ - http://localhost:8888/realms/test-realm
diff --git a/impl/http/src/test/resources/oAuthClientSecretPostClientCredentialsParamsHttpCall.yaml b/impl/http/src/test/resources/oAuthClientSecretPostClientCredentialsParamsHttpCall.yaml
new file mode 100644
index 00000000..9a441fcc
--- /dev/null
+++ b/impl/http/src/test/resources/oAuthClientSecretPostClientCredentialsParamsHttpCall.yaml
@@ -0,0 +1,25 @@
+document:
+ dsl: '1.0.0-alpha5'
+ namespace: examples
+ name: oauth2-authentication
+ version: '0.1.0'
+do:
+ - getPet:
+ call: http
+ with:
+ method: get
+ endpoint:
+ uri: http://localhost:8081/hello
+ authentication:
+ oauth2:
+ authority: http://localhost:8888/realms/test-realm
+ endpoints:
+ token: protocol/openid-connect/token
+ grant: client_credentials
+ request:
+ encoding: application/x-www-form-urlencoded
+ client:
+ id: ${ .clientId }
+ secret: ${ .clientSecret }
+ issuers:
+ - http://localhost:8888/realms/test-realm
diff --git a/impl/http/src/test/resources/oAuthClientSecretPostClientCredentialsParamsNoEndPointHttpCall.yaml b/impl/http/src/test/resources/oAuthClientSecretPostClientCredentialsParamsNoEndPointHttpCall.yaml
new file mode 100644
index 00000000..5e616d34
--- /dev/null
+++ b/impl/http/src/test/resources/oAuthClientSecretPostClientCredentialsParamsNoEndPointHttpCall.yaml
@@ -0,0 +1,21 @@
+document:
+ dsl: '1.0.0-alpha5'
+ namespace: examples
+ name: oauth2-authentication
+ version: '0.1.0'
+do:
+ - getPet:
+ call: http
+ with:
+ method: get
+ endpoint:
+ uri: http://localhost:8081/hello
+ authentication:
+ oauth2:
+ authority: http://localhost:8888/realms/test-realm
+ grant: client_credentials
+ client:
+ id: ${ .clientId }
+ secret: ${ .clientSecret }
+ issuers:
+ - http://localhost:8888/realms/test-realm
diff --git a/impl/http/src/test/resources/oAuthClientSecretPostPasswordAllGrantsHttpCall.yaml b/impl/http/src/test/resources/oAuthClientSecretPostPasswordAllGrantsHttpCall.yaml
new file mode 100644
index 00000000..ff350683
--- /dev/null
+++ b/impl/http/src/test/resources/oAuthClientSecretPostPasswordAllGrantsHttpCall.yaml
@@ -0,0 +1,29 @@
+document:
+ dsl: '1.0.0-alpha5'
+ namespace: examples
+ name: oauth2-authentication
+ version: '0.1.0'
+do:
+ - getPet:
+ call: http
+ with:
+ method: get
+ endpoint:
+ uri: http://localhost:8081/hello
+ authentication:
+ oauth2:
+ authority: http://localhost:8888/realms/test-realm
+ grant: password
+ request:
+ encoding: application/x-www-form-urlencoded
+ client:
+ id: ${ .clientId }
+ secret: ${ .clientSecret }
+ username: ${ .username }
+ password: ${ .password }
+ scopes:
+ - pets:read
+ - pets:write
+ - pets:delete
+ - pets:create
+ audiences: [ serverless-workflow, another-audience, third-audience ]
\ No newline at end of file
diff --git a/impl/http/src/test/resources/oAuthClientSecretPostPasswordAsArgHttpCall.yaml b/impl/http/src/test/resources/oAuthClientSecretPostPasswordAsArgHttpCall.yaml
new file mode 100644
index 00000000..817844a3
--- /dev/null
+++ b/impl/http/src/test/resources/oAuthClientSecretPostPasswordAsArgHttpCall.yaml
@@ -0,0 +1,25 @@
+document:
+ dsl: '1.0.0-alpha5'
+ namespace: examples
+ name: oauth2-authentication
+ version: '0.1.0'
+do:
+ - getPet:
+ call: http
+ with:
+ method: get
+ endpoint:
+ uri: http://localhost:8081/hello
+ authentication:
+ oauth2:
+ authority: http://localhost:8888/realms/test-realm
+ endpoints:
+ token: protocol/openid-connect/token
+ grant: password
+ client:
+ id: ${ .clientId }
+ secret: ${ .clientSecret }
+ username: ${ .username }
+ password: ${ .password }
+ issuers:
+ - http://localhost:8888/realms/test-realm
diff --git a/impl/http/src/test/resources/oAuthClientSecretPostPasswordHttpCall.yaml b/impl/http/src/test/resources/oAuthClientSecretPostPasswordHttpCall.yaml
new file mode 100644
index 00000000..891ce62f
--- /dev/null
+++ b/impl/http/src/test/resources/oAuthClientSecretPostPasswordHttpCall.yaml
@@ -0,0 +1,25 @@
+document:
+ dsl: '1.0.0-alpha5'
+ namespace: examples
+ name: oauth2-authentication
+ version: '0.1.0'
+do:
+ - getPet:
+ call: http
+ with:
+ method: get
+ endpoint:
+ uri: http://localhost:8081/hello
+ authentication:
+ oauth2:
+ authority: http://localhost:8888/realms/test-realm
+ endpoints:
+ token: protocol/openid-connect/token
+ grant: password
+ client:
+ id: serverless-workflow
+ secret: D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT
+ username: serverless-workflow-test
+ password: serverless-workflow-test
+ issuers:
+ - http://localhost:8888/realms/test-realm
diff --git a/impl/http/src/test/resources/oAuthClientSecretPostPasswordNoEndpointsHttpCall.yaml b/impl/http/src/test/resources/oAuthClientSecretPostPasswordNoEndpointsHttpCall.yaml
new file mode 100644
index 00000000..3a0fc402
--- /dev/null
+++ b/impl/http/src/test/resources/oAuthClientSecretPostPasswordNoEndpointsHttpCall.yaml
@@ -0,0 +1,21 @@
+document:
+ dsl: '1.0.0-alpha5'
+ namespace: examples
+ name: oauth2-authentication
+ version: '0.1.0'
+do:
+ - getPet:
+ call: http
+ with:
+ method: get
+ endpoint:
+ uri: http://localhost:8081/hello
+ authentication:
+ oauth2:
+ authority: http://localhost:8888/realms/test-realm
+ grant: password
+ client:
+ id: ${ .clientId }
+ secret: ${ .clientSecret }
+ username: ${ .username }
+ password: ${ .password }
diff --git a/impl/http/src/test/resources/oAuthJSONClientCredentialsHttpCall.yaml b/impl/http/src/test/resources/oAuthJSONClientCredentialsHttpCall.yaml
new file mode 100644
index 00000000..5c50cfd9
--- /dev/null
+++ b/impl/http/src/test/resources/oAuthJSONClientCredentialsHttpCall.yaml
@@ -0,0 +1,25 @@
+document:
+ dsl: '1.0.0-alpha5'
+ namespace: examples
+ name: oauth2-authentication
+ version: '0.1.0'
+do:
+ - getPet:
+ call: http
+ with:
+ method: get
+ endpoint:
+ uri: http://localhost:8081/hello
+ authentication:
+ oauth2:
+ authority: http://localhost:8888/realms/test-realm
+ endpoints:
+ token: protocol/openid-connect/token
+ grant: client_credentials
+ request:
+ encoding: application/json
+ client:
+ id: serverless-workflow
+ secret: D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT
+ issuers:
+ - http://localhost:8888/realms/test-realm
diff --git a/impl/http/src/test/resources/oAuthJSONClientCredentialsParamsHttpCall.yaml b/impl/http/src/test/resources/oAuthJSONClientCredentialsParamsHttpCall.yaml
new file mode 100644
index 00000000..d6480d03
--- /dev/null
+++ b/impl/http/src/test/resources/oAuthJSONClientCredentialsParamsHttpCall.yaml
@@ -0,0 +1,25 @@
+document:
+ dsl: '1.0.0-alpha5'
+ namespace: examples
+ name: oauth2-authentication
+ version: '0.1.0'
+do:
+ - getPet:
+ call: http
+ with:
+ method: get
+ endpoint:
+ uri: http://localhost:8081/hello
+ authentication:
+ oauth2:
+ authority: http://localhost:8888/realms/test-realm
+ endpoints:
+ token: protocol/openid-connect/token
+ grant: client_credentials
+ request:
+ encoding: application/json
+ client:
+ id: ${ .clientId }
+ secret: ${ .clientSecret }
+ issuers:
+ - http://localhost:8888/realms/test-realm
diff --git a/impl/http/src/test/resources/oAuthJSONClientCredentialsParamsNoEndPointHttpCall.yaml b/impl/http/src/test/resources/oAuthJSONClientCredentialsParamsNoEndPointHttpCall.yaml
new file mode 100644
index 00000000..8c442372
--- /dev/null
+++ b/impl/http/src/test/resources/oAuthJSONClientCredentialsParamsNoEndPointHttpCall.yaml
@@ -0,0 +1,23 @@
+document:
+ dsl: '1.0.0-alpha5'
+ namespace: examples
+ name: oauth2-authentication
+ version: '0.1.0'
+do:
+ - getPet:
+ call: http
+ with:
+ method: get
+ endpoint:
+ uri: http://localhost:8081/hello
+ authentication:
+ oauth2:
+ authority: http://localhost:8888/realms/test-realm
+ grant: client_credentials
+ request:
+ encoding: application/json
+ client:
+ id: ${ .clientId }
+ secret: ${ .clientSecret }
+ issuers:
+ - http://localhost:8888/realms/test-realm
diff --git a/impl/http/src/test/resources/oAuthJSONPasswordAllGrantsHttpCall.yaml b/impl/http/src/test/resources/oAuthJSONPasswordAllGrantsHttpCall.yaml
new file mode 100644
index 00000000..b5546842
--- /dev/null
+++ b/impl/http/src/test/resources/oAuthJSONPasswordAllGrantsHttpCall.yaml
@@ -0,0 +1,29 @@
+document:
+ dsl: '1.0.0-alpha5'
+ namespace: examples
+ name: oauth2-authentication
+ version: '0.1.0'
+do:
+ - getPet:
+ call: http
+ with:
+ method: get
+ endpoint:
+ uri: http://localhost:8081/hello
+ authentication:
+ oauth2:
+ authority: http://localhost:8888/realms/test-realm
+ grant: password
+ request:
+ encoding: application/json
+ client:
+ id: ${ .clientId }
+ secret: ${ .clientSecret }
+ username: ${ .username }
+ password: ${ .password }
+ scopes:
+ - pets:read
+ - pets:write
+ - pets:delete
+ - pets:create
+ audiences: [ serverless-workflow, another-audience, third-audience ]
\ No newline at end of file
diff --git a/impl/http/src/test/resources/oAuthJSONPasswordAsArgHttpCall.yaml b/impl/http/src/test/resources/oAuthJSONPasswordAsArgHttpCall.yaml
new file mode 100644
index 00000000..c9864aa9
--- /dev/null
+++ b/impl/http/src/test/resources/oAuthJSONPasswordAsArgHttpCall.yaml
@@ -0,0 +1,27 @@
+document:
+ dsl: '1.0.0-alpha5'
+ namespace: examples
+ name: oauth2-authentication
+ version: '0.1.0'
+do:
+ - getPet:
+ call: http
+ with:
+ method: get
+ endpoint:
+ uri: http://localhost:8081/hello
+ authentication:
+ oauth2:
+ authority: http://localhost:8888/realms/test-realm
+ endpoints:
+ token: protocol/openid-connect/token
+ grant: password
+ request:
+ encoding: application/json
+ client:
+ id: ${ .clientId }
+ secret: ${ .clientSecret }
+ username: ${ .username }
+ password: ${ .password }
+ issuers:
+ - http://localhost:8888/realms/test-realm
diff --git a/impl/http/src/test/resources/oAuthJSONPasswordHttpCall.yaml b/impl/http/src/test/resources/oAuthJSONPasswordHttpCall.yaml
new file mode 100644
index 00000000..fa5d3edc
--- /dev/null
+++ b/impl/http/src/test/resources/oAuthJSONPasswordHttpCall.yaml
@@ -0,0 +1,27 @@
+document:
+ dsl: '1.0.0-alpha5'
+ namespace: examples
+ name: oauth2-authentication
+ version: '0.1.0'
+do:
+ - getPet:
+ call: http
+ with:
+ method: get
+ endpoint:
+ uri: http://localhost:8081/hello
+ authentication:
+ oauth2:
+ authority: http://localhost:8888/realms/test-realm
+ endpoints:
+ token: protocol/openid-connect/token
+ grant: password
+ request:
+ encoding: application/json
+ client:
+ id: serverless-workflow
+ secret: D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT
+ username: serverless-workflow-test
+ password: serverless-workflow-test
+ issuers:
+ - http://localhost:8888/realms/test-realm
diff --git a/impl/http/src/test/resources/oAuthJSONPasswordNoEndpointsHttpCall.yaml b/impl/http/src/test/resources/oAuthJSONPasswordNoEndpointsHttpCall.yaml
new file mode 100644
index 00000000..91da2890
--- /dev/null
+++ b/impl/http/src/test/resources/oAuthJSONPasswordNoEndpointsHttpCall.yaml
@@ -0,0 +1,23 @@
+document:
+ dsl: '1.0.0-alpha5'
+ namespace: examples
+ name: oauth2-authentication
+ version: '0.1.0'
+do:
+ - getPet:
+ call: http
+ with:
+ method: get
+ endpoint:
+ uri: http://localhost:8081/hello
+ authentication:
+ oauth2:
+ authority: http://localhost:8888/realms/test-realm
+ grant: password
+ request:
+ encoding: application/json
+ client:
+ id: ${ .clientId }
+ secret: ${ .clientSecret }
+ username: ${ .username }
+ password: ${ .password }
diff --git a/impl/jwt-impl/pom.xml b/impl/jwt-impl/pom.xml
new file mode 100644
index 00000000..64795e76
--- /dev/null
+++ b/impl/jwt-impl/pom.xml
@@ -0,0 +1,26 @@
+
+
+ 4.0.0
+
+ io.serverlessworkflow
+ serverlessworkflow-impl
+ 8.0.0-SNAPSHOT
+
+
+ serverlessworkflow-impl-jackson-jwt
+ Serverless Workflow :: Impl :: JWT
+
+
+
+ io.serverlessworkflow
+ serverlessworkflow-jwt
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+
\ No newline at end of file
diff --git a/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTConverter.java b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTConverter.java
new file mode 100644
index 00000000..850538ab
--- /dev/null
+++ b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTConverter.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.serverlessworkflow.impl.http.jwt;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.serverlessworkflow.http.jwt.JWT;
+import io.serverlessworkflow.http.jwt.JWTConverter;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+
+public class JacksonJWTConverter implements JWTConverter {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ @Override
+ public JWT fromToken(String token) throws IllegalArgumentException {
+ String[] parts = token.split("\\.");
+ if (parts.length < 2) {
+ throw new IllegalArgumentException("Invalid JWT token format");
+ }
+ try {
+ String payloadJson =
+ new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
+ return new JacksonJWTImpl(token, MAPPER.readValue(payloadJson, Map.class));
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("Failed to parse JWT token payload: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTImpl.java b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTImpl.java
new file mode 100644
index 00000000..9101646f
--- /dev/null
+++ b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTImpl.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.serverlessworkflow.impl.http.jwt;
+
+import io.serverlessworkflow.http.jwt.JWT;
+import java.util.Map;
+
+public class JacksonJWTImpl implements JWT {
+
+ private final Map claims;
+ private final String token;
+
+ JacksonJWTImpl(String token, Map claims) {
+ this.token = token;
+ this.claims = claims;
+ }
+
+ @Override
+ public String getToken() {
+ return token;
+ }
+
+ @Override
+ public Object getClaim(String name) {
+ if (claims == null || claims.isEmpty()) {
+ return null;
+ }
+ return claims.get(name);
+ }
+}
diff --git a/impl/jwt-impl/src/main/resources/META-INF/services/io.serverlessworkflow.http.jwt.JWTConverter b/impl/jwt-impl/src/main/resources/META-INF/services/io.serverlessworkflow.http.jwt.JWTConverter
new file mode 100644
index 00000000..1c898a8b
--- /dev/null
+++ b/impl/jwt-impl/src/main/resources/META-INF/services/io.serverlessworkflow.http.jwt.JWTConverter
@@ -0,0 +1 @@
+io.serverlessworkflow.impl.http.jwt.JacksonJWTConverter
\ No newline at end of file
diff --git a/impl/pom.xml b/impl/pom.xml
index 416e26ef..db10ad78 100644
--- a/impl/pom.xml
+++ b/impl/pom.xml
@@ -75,5 +75,6 @@
http
core
jackson
+ jwt-impl
\ No newline at end of file
diff --git a/jwt/pom.xml b/jwt/pom.xml
new file mode 100644
index 00000000..f3beea6c
--- /dev/null
+++ b/jwt/pom.xml
@@ -0,0 +1,15 @@
+
+
+ 4.0.0
+
+ io.serverlessworkflow
+ serverlessworkflow-parent
+ 8.0.0-SNAPSHOT
+
+
+ serverlessworkflow-jwt
+ Serverless Workflow :: JWT
+ jar
+
\ No newline at end of file
diff --git a/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWT.java b/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWT.java
new file mode 100644
index 00000000..8d2f19da
--- /dev/null
+++ b/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWT.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.serverlessworkflow.http.jwt;
+
+public interface JWT {
+
+ String getToken();
+
+ Object getClaim(String name);
+}
diff --git a/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWTConverter.java b/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWTConverter.java
new file mode 100644
index 00000000..fd3d759e
--- /dev/null
+++ b/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWTConverter.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.serverlessworkflow.http.jwt;
+
+public interface JWTConverter {
+
+ /**
+ * Converts a JWT token string into a JWT object.
+ *
+ * @param token the JWT token string
+ * @return a JWT object containing the token and its claims
+ */
+ JWT fromToken(String token) throws IllegalArgumentException;
+}
diff --git a/pom.xml b/pom.xml
index 75e32710..f0775854 100644
--- a/pom.xml
+++ b/pom.xml
@@ -46,6 +46,7 @@
examples
experimental
fluent
+ jwt
@@ -78,6 +79,7 @@
1.5.18
2.19.2
1.5.8
+ 4.12.0
3.1.1
1.5.2
3.27.4
@@ -166,6 +168,16 @@
serverlessworkflow-api
${project.version}
+
+ io.serverlessworkflow
+ serverlessworkflow-jwt
+ ${project.version}
+
+
+ io.serverlessworkflow
+ serverlessworkflow-impl-jackson-jwt
+ ${project.version}
+
com.networknt
json-schema-validator
@@ -223,6 +235,12 @@
${version.ch.qos.logback}
test
+
+ com.squareup.okhttp3
+ mockwebserver
+ ${version.com.squareup.okhttp3.mockwebserver}
+ test
+
org.assertj
assertj-core