From 543b83830cd7c68fa8de44898012cca16df2597c Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Wed, 6 Aug 2025 17:56:19 -0700 Subject: [PATCH 1/6] HttpCallTask implementation lack OAuth authentication support Signed-off-by: Dmitrii Tikhomirov --- .../io/serverlessworkflow/api/ApiTest.java | 17 ++ .../impl/WorkflowError.java | 8 +- impl/http/pom.xml | 4 + .../impl/executors/http/AuthProvider.java | 11 +- .../impl/executors/http/HttpExecutor.java | 5 +- .../executors/http/OAuth2AuthProvider.java | 37 ++++- .../http/oauth/AccessTokenProvider.java | 56 +++++++ .../http/oauth/ClientSecretBasic.java | 93 +++++++++++ .../http/oauth/ClientSecretPostStep.java | 85 ++++++++++ .../http/oauth/HttpRequestBuilder.java | 127 ++++++++++++++ .../impl/executors/http/oauth/JWT.java | 55 ++++++ .../http/oauth/OAuthRequestBuilder.java | 157 ++++++++++++++++++ .../http/oauth/TokenResponseHandler.java | 69 ++++++++ impl/pom.xml | 6 + 14 files changed, 722 insertions(+), 8 deletions(-) create mode 100644 impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/AccessTokenProvider.java create mode 100644 impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/ClientSecretBasic.java create mode 100644 impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/ClientSecretPostStep.java create mode 100644 impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/HttpRequestBuilder.java create mode 100644 impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/JWT.java create mode 100644 impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/OAuthRequestBuilder.java create mode 100644 impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/TokenResponseHandler.java diff --git a/api/src/test/java/io/serverlessworkflow/api/ApiTest.java b/api/src/test/java/io/serverlessworkflow/api/ApiTest.java index 36d6841e..c4aa2735 100644 --- a/api/src/test/java/io/serverlessworkflow/api/ApiTest.java +++ b/api/src/test/java/io/serverlessworkflow/api/ApiTest.java @@ -63,6 +63,23 @@ void testCallHTTPAPI() throws IOException { } } + @Test + void testCallHttpOauthAPI() throws IOException { + Workflow workflow = readWorkflowFromClasspath("features/authentication-oauth2.yaml"); + assertThat(workflow.getDo()).isNotEmpty(); + assertThat(workflow.getDo().get(0).getName()).isNotNull(); + assertThat(workflow.getDo().get(0).getTask()).isNotNull(); + Task task = workflow.getDo().get(0).getTask(); + if (task.get() instanceof CallTask) { + CallTask callTask = task.getCallTask(); + assertThat(callTask).isNotNull(); + assertThat(task.getDoTask()).isNull(); + CallHTTP httpCall = callTask.getCallHTTP(); + assertThat(httpCall).isNotNull(); + assertThat(httpCall.getWith().getMethod()).isEqualTo("get"); + } + } + @Test void testCallFunctionAPIWithoutArguments() throws IOException { Workflow workflow = readWorkflowFromClasspath("features/callFunction.yaml"); 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..e6fa4c76 100644 --- a/impl/http/pom.xml +++ b/impl/http/pom.xml @@ -24,6 +24,10 @@ org.glassfish.jersey.core jersey-client + + com.fasterxml.jackson.core + jackson-databind + io.serverlessworkflow serverlessworkflow-api 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..29776e90 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,55 @@ 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.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowApplication; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowModel; +import io.serverlessworkflow.impl.executors.http.oauth.JWT; +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 Oauth2 oauth2; + + private WorkflowApplication workflowApplication; + + 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) { + this.workflowApplication = application; + Oauth2 oauth2 = authPolicy.getOauth2(); + + if (oauth2.getOAuth2ConnectAuthenticationProperties() != null) { + this.oauth2 = 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 = + new OAuthRequestBuilder(workflowApplication, oauth2) + .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..08c2aea0 --- /dev/null +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/AccessTokenProvider.java @@ -0,0 +1,56 @@ +/* + * 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 com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.impl.TaskContext; +import java.net.http.HttpRequest; +import java.util.List; + +public class AccessTokenProvider { + + private final TokenResponseHandler tokenResponseHandler = new TokenResponseHandler(); + + private final TaskContext context; + private final List issuers; + private final HttpRequest requestBuilder; + + public AccessTokenProvider( + HttpRequest requestBuilder, TaskContext context, List issuers) { + this.requestBuilder = requestBuilder; + this.issuers = issuers; + this.context = context; + } + + public JWT validateAndGet() { + JsonNode token = tokenResponseHandler.apply(requestBuilder, context); + JWT jwt; + try { + jwt = JWT.fromString(token.get("access_token").asText()); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to parse JWT token: " + e.getMessage(), e); + } + if (!(issuers == null || issuers.isEmpty())) { + String tokenIssuer = (String) jwt.getClaim("iss"); + if (tokenIssuer == null || tokenIssuer.isEmpty() || !issuers.contains(tokenIssuer)) { + throw new RuntimeException("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..ad2bfd31 --- /dev/null +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/ClientSecretBasic.java @@ -0,0 +1,93 @@ +/* + * 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); + } else if (authenticationData.getGrant().equals(CLIENT_CREDENTIALS)) { + clientCredentials(requestBuilder); + } else { + throw new UnsupportedOperationException( + "Unsupported grant type: " + authenticationData.getGrant()); + } + } + + private void clientCredentials(HttpRequestBuilder requestBuilder) { + OAuth2AutenthicationData authenticationData = + oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); + 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) + .withMethod("POST") + .addQueryParam("grant_type", "client_credentials"); + } + + private void password(HttpRequestBuilder requestBuilder) { + OAuth2AutenthicationData authenticationData = + oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); + 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 + .withMethod("POST") + .addHeader("Authorization", "Basic " + encodedAuth) + .addQueryParam("grant_type", "password") + .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..59dbaeef --- /dev/null +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/ClientSecretPostStep.java @@ -0,0 +1,85 @@ +/* + * 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); + } else if (authenticationData.getGrant().equals(CLIENT_CREDENTIALS)) { + clientCredentials(requestBuilder); + } else { + throw new UnsupportedOperationException( + "Unsupported grant type: " + authenticationData.getGrant()); + } + } + + private void clientCredentials(HttpRequestBuilder requestBuilder) { + OAuth2AutenthicationData authenticationData = + oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); + 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 + .withMethod("POST") + .addQueryParam("grant_type", "client_credentials") + .addQueryParam("client_id", authenticationData.getClient().getId()) + .addQueryParam("client_secret", authenticationData.getClient().getSecret()); + } + + private void password(HttpRequestBuilder requestBuilder) { + OAuth2AutenthicationData authenticationData = + oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); + 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 + .withMethod("POST") + .addQueryParam("grant_type", "password") + .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..6bd9e447 --- /dev/null +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/HttpRequestBuilder.java @@ -0,0 +1,127 @@ +/* + * 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.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowModel; +import io.serverlessworkflow.impl.WorkflowUtils; +import io.serverlessworkflow.impl.WorkflowValueResolver; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpRequest; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class HttpRequestBuilder { + + private final Map> headers; + + private final Map> queryParams; + + private final WorkflowApplication app; + + private URI uri; + + private String method; + + public HttpRequestBuilder(WorkflowApplication app) { + this.app = app; + headers = new HashMap<>(); + queryParams = new HashMap<>(); + } + + public HttpRequestBuilder addHeader(String key, String token) { + headers.put(key, WorkflowUtils.buildStringFilter(app, token)); + return this; + } + + public HttpRequestBuilder addQueryParam(String key, String token) { + queryParams.put(key, WorkflowUtils.buildStringFilter(app, token)); + return this; + } + + public HttpRequestBuilder withUri(URI uri) { + this.uri = uri; + return this; + } + + public HttpRequestBuilder withMethod(String method) { + this.method = method; + return this; + } + + public HttpRequest build(WorkflowContext workflow, TaskContext task, WorkflowModel model) { + HttpRequest.Builder request = HttpRequest.newBuilder(); + + for (var entry : headers.entrySet()) { + String headerValue = entry.getValue().apply(workflow, task, model); + if (headerValue != null) { + request = request.header(entry.getKey(), headerValue); + } + } + + request.header("Accept", "application/json"); + + if (uri == null) { + throw new IllegalStateException("URI must be set before building the request"); + } + + String encoded = + queryParams.entrySet().stream() + .map( + e -> { + String v = e.getValue().apply(workflow, task, model); + if (v == null) return null; + return URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8) + + "=" + + URLEncoder.encode(v, StandardCharsets.UTF_8); + }) + .filter(Objects::nonNull) + .collect(Collectors.joining("&")); + + if (method != null) { + switch (method.toUpperCase()) { + case "GET" -> { + if (!encoded.isEmpty()) { + String sep = (uri.getQuery() == null || uri.getQuery().isEmpty()) ? "?" : "&"; + uri = URI.create(uri.toString() + sep + encoded); + } + request.uri(uri).GET(); + } + case "POST" -> { + request.uri(uri); + HttpRequest.BodyPublisher body = + encoded.isEmpty() + ? HttpRequest.BodyPublishers.noBody() + : HttpRequest.BodyPublishers.ofString(encoded); + request.POST(body); + } + default -> throw new IllegalArgumentException("Unsupported HTTP method: " + method); + } + } else { + throw new IllegalStateException("HTTP method must be set before building the request"); + } + request.timeout(Duration.ofSeconds(15)); + return request.build(); + } +} diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/JWT.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/JWT.java new file mode 100644 index 00000000..1aa92eb2 --- /dev/null +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/JWT.java @@ -0,0 +1,55 @@ +/* + * 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 com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; + +public class JWT { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final String token; + private final Map claims; + + private JWT(String token, Map claims) { + this.token = token; + this.claims = claims; + } + + public static JWT fromString(String token) throws JsonProcessingException { + String[] parts = token.split("\\."); + if (parts.length < 2) { + throw new IllegalArgumentException("Invalid JWT token format"); + } + + String payloadJson = + new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); + return new JWT(token, MAPPER.readValue(payloadJson, Map.class)); + } + + public String getToken() { + return token; + } + + public Object getClaim(String name) { + return claims.get(name); + } +} 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..5fe4f3a9 --- /dev/null +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/OAuthRequestBuilder.java @@ -0,0 +1,157 @@ +/* + * 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.*; + +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.List; +import java.util.Map; + +public class OAuthRequestBuilder { + + private final Oauth2 oauth2; + + 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.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() { + OAuth2AutenthicationData authenticationData = + oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); + 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) { + OAuth2AutenthicationData authenticationData = + oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); + if (authenticationData.getAudiences() != null && !authenticationData.getAudiences().isEmpty()) { + String audiences = String.join(" ", authenticationData.getAudiences()); + requestBuilder.addQueryParam("audience", audiences); + } + } + + private void scope(HttpRequestBuilder requestBuilder) { + OAuth2AutenthicationData authenticationData = + oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); + if (authenticationData.getScopes() != null && !authenticationData.getScopes().isEmpty()) { + String scopes = String.join(" ", authenticationData.getScopes()); + requestBuilder.addQueryParam("scope", scopes); + } + } + + 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) { + OAuth2AutenthicationData authenticationData = + oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); + + 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..3c5ee28c --- /dev/null +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/TokenResponseHandler.java @@ -0,0 +1,69 @@ +/* + * 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 com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowError; +import io.serverlessworkflow.impl.WorkflowException; +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.function.BiFunction; + +public class TokenResponseHandler implements BiFunction { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final HttpClient CLIENT = + HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + + @Override + public JsonNode apply(HttpRequest requestBuilder, TaskContext context) { + HttpResponse response; + try { + response = CLIENT.send(requestBuilder, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new WorkflowException( + WorkflowError.communication( + response.statusCode(), + context, + "Failed to obtain token: HTTP " + + response.statusCode() + + " — " + + response.body()) + .build()); + } + } catch (java.net.ConnectException e) { + throw new RuntimeException("Connection refused: " + e.getMessage(), e); + } catch (IOException e) { + throw new RuntimeException("Unable to send request: " + e.getMessage(), e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Unable to send request: " + e.getMessage(), e); + } + + try { + return MAPPER.readTree(response.body()); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to parse JSON response: " + e.getMessage(), e); + } + } +} diff --git a/impl/pom.xml b/impl/pom.xml index 416e26ef..ef2215de 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -14,6 +14,7 @@ 1.4.0 5.2.3 4.0.0 + 2.19.2 @@ -57,6 +58,11 @@ jakarta.ws.rs-api ${version.jakarta.ws.rs} + + com.fasterxml.jackson.core + jackson-databind + ${version.jackson-databind} + org.glassfish.jersey.core jersey-client From 240ac321488e72a5394c090e21ec1cb6425d7178 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Mon, 18 Aug 2025 17:50:50 -0700 Subject: [PATCH 2/6] tests Signed-off-by: Dmitrii Tikhomirov --- impl/http/pom.xml | 5 + .../executors/http/OAuth2AuthProvider.java | 4 +- .../http/oauth/ClientSecretBasic.java | 13 +- .../http/oauth/ClientSecretPostStep.java | 12 +- .../http/oauth/OAuthRequestBuilder.java | 13 +- .../impl/OAuthHTTPWorkflowDefinitionTest.java | 224 ++++++++++++++++++ ...ntSecretPostClientCredentialsHttpCall.yaml | 23 ++ ...oAuthClientSecretPostPasswordHttpCall.yaml | 25 ++ pom.xml | 7 + 9 files changed, 297 insertions(+), 29 deletions(-) create mode 100644 impl/http/src/test/java/io/serverlessworkflow/impl/OAuthHTTPWorkflowDefinitionTest.java create mode 100644 impl/http/src/test/resources/oAuthClientSecretPostClientCredentialsHttpCall.yaml create mode 100644 impl/http/src/test/resources/oAuthClientSecretPostPasswordHttpCall.yaml diff --git a/impl/http/pom.xml b/impl/http/pom.xml index e6fa4c76..8db94a93 100644 --- a/impl/http/pom.xml +++ b/impl/http/pom.xml @@ -59,5 +59,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/OAuth2AuthProvider.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/OAuth2AuthProvider.java index 29776e90..1cd2d69b 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 @@ -31,7 +31,7 @@ public class OAuth2AuthProvider implements AuthProvider { private Oauth2 oauth2; - private WorkflowApplication workflowApplication; + private final WorkflowApplication workflowApplication; private static final String BEARER_TOKEN = "%s %s"; @@ -39,7 +39,6 @@ public OAuth2AuthProvider( WorkflowApplication application, Workflow workflow, OAuth2AuthenticationPolicy authPolicy) { this.workflowApplication = application; Oauth2 oauth2 = authPolicy.getOauth2(); - if (oauth2.getOAuth2ConnectAuthenticationProperties() != null) { this.oauth2 = oauth2; } else if (oauth2.getOAuth2AuthenticationPolicySecret() != null) { @@ -62,7 +61,6 @@ public void preRequest( .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/ClientSecretBasic.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/ClientSecretBasic.java index ad2bfd31..2a9b3dd8 100644 --- 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 @@ -34,20 +34,17 @@ public ClientSecretBasic(Oauth2 oauth2) { public void execute(HttpRequestBuilder requestBuilder) { OAuth2AutenthicationData authenticationData = oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); - if (authenticationData.getGrant().equals(PASSWORD)) { - password(requestBuilder); + password(requestBuilder, authenticationData); } else if (authenticationData.getGrant().equals(CLIENT_CREDENTIALS)) { - clientCredentials(requestBuilder); + clientCredentials(requestBuilder, authenticationData); } else { throw new UnsupportedOperationException( "Unsupported grant type: " + authenticationData.getGrant()); } } - private void clientCredentials(HttpRequestBuilder requestBuilder) { - OAuth2AutenthicationData authenticationData = - oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); + private void clientCredentials(HttpRequestBuilder requestBuilder, OAuth2AutenthicationData authenticationData) { if (authenticationData.getClient() == null || authenticationData.getClient().getId() == null || authenticationData.getClient().getSecret() == null) { @@ -65,9 +62,7 @@ private void clientCredentials(HttpRequestBuilder requestBuilder) { .addQueryParam("grant_type", "client_credentials"); } - private void password(HttpRequestBuilder requestBuilder) { - OAuth2AutenthicationData authenticationData = - oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); + 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"); 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 index 59dbaeef..c799e8f8 100644 --- 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 @@ -34,18 +34,16 @@ public void execute(HttpRequestBuilder requestBuilder) { oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); if (authenticationData.getGrant().equals(PASSWORD)) { - password(requestBuilder); + password(requestBuilder, authenticationData); } else if (authenticationData.getGrant().equals(CLIENT_CREDENTIALS)) { - clientCredentials(requestBuilder); + clientCredentials(requestBuilder, authenticationData); } else { throw new UnsupportedOperationException( "Unsupported grant type: " + authenticationData.getGrant()); } } - private void clientCredentials(HttpRequestBuilder requestBuilder) { - OAuth2AutenthicationData authenticationData = - oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); + private void clientCredentials(HttpRequestBuilder requestBuilder, OAuth2AutenthicationData authenticationData) { if (authenticationData.getClient() == null || authenticationData.getClient().getId() == null || authenticationData.getClient().getSecret() == null) { @@ -60,9 +58,7 @@ private void clientCredentials(HttpRequestBuilder requestBuilder) { .addQueryParam("client_secret", authenticationData.getClient().getSecret()); } - private void password(HttpRequestBuilder requestBuilder) { - OAuth2AutenthicationData authenticationData = - oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); + 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"); 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 index 5fe4f3a9..16670fba 100644 --- 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 @@ -34,6 +34,8 @@ public class OAuthRequestBuilder { private final Oauth2 oauth2; + private final OAuth2AutenthicationData authenticationData; + private final WorkflowApplication application; private List issuers; @@ -46,6 +48,8 @@ public class OAuthRequestBuilder { public OAuthRequestBuilder(WorkflowApplication application, Oauth2 oauth2) { this.oauth2 = oauth2; + this.authenticationData = + oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); this.application = application; } @@ -85,8 +89,6 @@ private void clientSecretPost(HttpRequestBuilder requestBuilder) { } private OAuth2AutenthicationDataClient.ClientAuthentication getClientAuthentication() { - OAuth2AutenthicationData authenticationData = - oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); if (authenticationData.getClient() == null || authenticationData.getClient().getAuthentication() == null) { return CLIENT_SECRET_POST; @@ -103,8 +105,6 @@ private void issuers() { } public void audience(HttpRequestBuilder requestBuilder) { - OAuth2AutenthicationData authenticationData = - oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); if (authenticationData.getAudiences() != null && !authenticationData.getAudiences().isEmpty()) { String audiences = String.join(" ", authenticationData.getAudiences()); requestBuilder.addQueryParam("audience", audiences); @@ -112,8 +112,6 @@ public void audience(HttpRequestBuilder requestBuilder) { } private void scope(HttpRequestBuilder requestBuilder) { - OAuth2AutenthicationData authenticationData = - oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); if (authenticationData.getScopes() != null && !authenticationData.getScopes().isEmpty()) { String scopes = String.join(" ", authenticationData.getScopes()); requestBuilder.addQueryParam("scope", scopes); @@ -143,9 +141,6 @@ private void authenticationURI(HttpRequestBuilder requestBuilder) { } public void requestEncoding(HttpRequestBuilder requestBuilder) { - OAuth2AutenthicationData authenticationData = - oauth2.getOAuth2ConnectAuthenticationProperties().getOAuth2AutenthicationData(); - if (authenticationData.getRequest() != null && authenticationData.getRequest().getEncoding() != null) { requestBuilder.addHeader( 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..aac71a2a --- /dev/null +++ b/impl/http/src/test/java/io/serverlessworkflow/impl/OAuthHTTPWorkflowDefinitionTest.java @@ -0,0 +1,224 @@ +/* + * 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 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 = + """ + { + "access_token": "%s", + "token_type": "Bearer", + "expires_in": 3600, + "scope": "read write" + } + """ + .formatted(jwt); + + authServer.enqueue( + new MockResponse() + .setBody(tokenResponse) + .setHeader("Content-Type", "application/json") + .setResponseCode(200)); + + String response = + """ + { + "message": "Hello World" + } + """; + + 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; charset=UTF-8", 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 testOAuthClientSecretPostClientCredentialsWorkflowExecution() throws Exception { + String jwt = fakeAccessToken(); + + String tokenResponse = + """ + { + "access_token": "%s", + "token_type": "Bearer", + "expires_in": 3600, + "scope": "read write" + } + """ + .formatted(jwt); + + authServer.enqueue( + new MockResponse() + .setBody(tokenResponse) + .setHeader("Content-Type", "application/json") + .setResponseCode(200)); + + String response = + """ + { + "message": "Hello World" + } + """; + + 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; charset=UTF-8", 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")); + } + + 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/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/pom.xml b/pom.xml index 75e32710..57e7d896 100644 --- a/pom.xml +++ b/pom.xml @@ -78,6 +78,7 @@ 1.5.18 2.19.2 1.5.8 + 4.12.0 3.1.1 1.5.2 3.27.4 @@ -223,6 +224,12 @@ ${version.ch.qos.logback} test + + com.squareup.okhttp3 + mockwebserver + ${version.com.squareup.okhttp3.mockwebserver} + test + org.assertj assertj-core From 26c32d8adb8c2c0be23ec0eec88499493e54042a Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Tue, 19 Aug 2025 15:24:32 -0700 Subject: [PATCH 3/6] post review Signed-off-by: Dmitrii Tikhomirov --- .../io/serverlessworkflow/api/ApiTest.java | 17 --- impl/http/pom.xml | 13 +- .../executors/http/OAuth2AuthProvider.java | 2 +- .../http/oauth/AccessTokenProvider.java | 31 +++-- .../http/oauth/ClientSecretBasic.java | 14 +- .../http/oauth/ClientSecretPostStep.java | 14 +- .../http/oauth/HttpRequestBuilder.java | 125 ++++++++++-------- .../http/oauth/InvocationHolder.java | 42 ++++++ .../http/oauth/OAuthRequestBuilder.java | 2 +- .../http/oauth/TokenResponseHandler.java | 63 ++++----- .../impl/OAuthHTTPWorkflowDefinitionTest.java | 6 +- impl/jwt-impl/pom.xml | 26 ++++ .../impl/http/jwt/DefaultJWTConverter.java} | 36 ++--- .../impl/http/jwt/DefaultJWTImpl.java | 44 ++++++ ...o.serverlessworkflow.http.jwt.JWTConverter | 1 + impl/pom.xml | 6 +- jwt/pom.xml | 15 +++ .../io/serverlessworkflow/http/jwt/JWT.java | 24 ++++ .../http/jwt/JWTConverter.java | 28 ++++ pom.xml | 11 ++ 20 files changed, 357 insertions(+), 163 deletions(-) create mode 100644 impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/InvocationHolder.java create mode 100644 impl/jwt-impl/pom.xml rename impl/{http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/JWT.java => jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/DefaultJWTConverter.java} (61%) create mode 100644 impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/DefaultJWTImpl.java create mode 100644 impl/jwt-impl/src/main/resources/META-INF/services/io.serverlessworkflow.http.jwt.JWTConverter create mode 100644 jwt/pom.xml create mode 100644 jwt/src/main/java/io/serverlessworkflow/http/jwt/JWT.java create mode 100644 jwt/src/main/java/io/serverlessworkflow/http/jwt/JWTConverter.java diff --git a/api/src/test/java/io/serverlessworkflow/api/ApiTest.java b/api/src/test/java/io/serverlessworkflow/api/ApiTest.java index c4aa2735..36d6841e 100644 --- a/api/src/test/java/io/serverlessworkflow/api/ApiTest.java +++ b/api/src/test/java/io/serverlessworkflow/api/ApiTest.java @@ -63,23 +63,6 @@ void testCallHTTPAPI() throws IOException { } } - @Test - void testCallHttpOauthAPI() throws IOException { - Workflow workflow = readWorkflowFromClasspath("features/authentication-oauth2.yaml"); - assertThat(workflow.getDo()).isNotEmpty(); - assertThat(workflow.getDo().get(0).getName()).isNotNull(); - assertThat(workflow.getDo().get(0).getTask()).isNotNull(); - Task task = workflow.getDo().get(0).getTask(); - if (task.get() instanceof CallTask) { - CallTask callTask = task.getCallTask(); - assertThat(callTask).isNotNull(); - assertThat(task.getDoTask()).isNull(); - CallHTTP httpCall = callTask.getCallHTTP(); - assertThat(httpCall).isNotNull(); - assertThat(httpCall.getWith().getMethod()).isEqualTo("get"); - } - } - @Test void testCallFunctionAPIWithoutArguments() throws IOException { Workflow workflow = readWorkflowFromClasspath("features/callFunction.yaml"); diff --git a/impl/http/pom.xml b/impl/http/pom.xml index 8db94a93..4d85268b 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 @@ -24,10 +28,6 @@ org.glassfish.jersey.core jersey-client - - com.fasterxml.jackson.core - jackson-databind - io.serverlessworkflow serverlessworkflow-api @@ -38,6 +38,11 @@ serverlessworkflow-impl-jackson test + + io.serverlessworkflow + serverlessworkflow-impl-jwt + test + org.junit.jupiter junit-jupiter-api 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 1cd2d69b..ef7d21f6 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 @@ -18,11 +18,11 @@ 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.JWT; import io.serverlessworkflow.impl.executors.http.oauth.OAuthRequestBuilder; import jakarta.ws.rs.client.Invocation; import jakarta.ws.rs.client.Invocation.Builder; 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 index 08c2aea0..57983835 100644 --- 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 @@ -16,11 +16,12 @@ package io.serverlessworkflow.impl.executors.http.oauth; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.http.jwt.JWT; +import io.serverlessworkflow.http.jwt.JWTConverter; import io.serverlessworkflow.impl.TaskContext; -import java.net.http.HttpRequest; import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; public class AccessTokenProvider { @@ -28,21 +29,31 @@ public class AccessTokenProvider { private final TaskContext context; private final List issuers; - private final HttpRequest requestBuilder; + private final InvocationHolder invocation; - public AccessTokenProvider( - HttpRequest requestBuilder, TaskContext context, List issuers) { - this.requestBuilder = requestBuilder; + private final JWTConverter jwtConverter; + + AccessTokenProvider(InvocationHolder invocation, TaskContext context, List issuers) { + this.invocation = invocation; this.issuers = issuers; this.context = context; + + ServiceLoader jwtConverters = + ServiceLoader.load(JWTConverter.class, AccessTokenProvider.class.getClassLoader()); + + if (jwtConverters.iterator().hasNext()) { + this.jwtConverter = jwtConverters.iterator().next(); + } else { + throw new RuntimeException("No JWTConverter implementation found"); + } } public JWT validateAndGet() { - JsonNode token = tokenResponseHandler.apply(requestBuilder, context); + Map token = tokenResponseHandler.apply(invocation, context); JWT jwt; try { - jwt = JWT.fromString(token.get("access_token").asText()); - } catch (JsonProcessingException e) { + jwt = jwtConverter.fromToken((String) token.get("access_token")); + } catch (IllegalArgumentException e) { throw new RuntimeException("Failed to parse JWT token: " + e.getMessage(), e); } if (!(issuers == null || issuers.isEmpty())) { 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 index 2a9b3dd8..76fb3ea1 100644 --- 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 @@ -44,7 +44,8 @@ public void execute(HttpRequestBuilder requestBuilder) { } } - private void clientCredentials(HttpRequestBuilder requestBuilder, OAuth2AutenthicationData authenticationData) { + private void clientCredentials( + HttpRequestBuilder requestBuilder, OAuth2AutenthicationData authenticationData) { if (authenticationData.getClient() == null || authenticationData.getClient().getId() == null || authenticationData.getClient().getSecret() == null) { @@ -58,11 +59,12 @@ private void clientCredentials(HttpRequestBuilder requestBuilder, OAuth2Autenthi requestBuilder .addHeader("Authorization", "Basic " + encodedAuth) - .withMethod("POST") - .addQueryParam("grant_type", "client_credentials"); + .withRequestContentType(authenticationData.getRequest()) + .withGrantType(authenticationData.getGrant()); } - private void password(HttpRequestBuilder requestBuilder, OAuth2AutenthicationData authenticationData) { + 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"); @@ -79,9 +81,9 @@ private void password(HttpRequestBuilder requestBuilder, OAuth2AutenthicationDat String encodedAuth = Base64.getEncoder().encodeToString(idAndSecret.getBytes()); requestBuilder - .withMethod("POST") + .withGrantType(authenticationData.getGrant()) + .withRequestContentType(authenticationData.getRequest()) .addHeader("Authorization", "Basic " + encodedAuth) - .addQueryParam("grant_type", "password") .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 index c799e8f8..63af6f70 100644 --- 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 @@ -43,7 +43,8 @@ public void execute(HttpRequestBuilder requestBuilder) { } } - private void clientCredentials(HttpRequestBuilder requestBuilder, OAuth2AutenthicationData authenticationData) { + private void clientCredentials( + HttpRequestBuilder requestBuilder, OAuth2AutenthicationData authenticationData) { if (authenticationData.getClient() == null || authenticationData.getClient().getId() == null || authenticationData.getClient().getSecret() == null) { @@ -52,13 +53,14 @@ private void clientCredentials(HttpRequestBuilder requestBuilder, OAuth2Autenthi } requestBuilder - .withMethod("POST") - .addQueryParam("grant_type", "client_credentials") + .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) { + 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"); @@ -71,8 +73,8 @@ private void password(HttpRequestBuilder requestBuilder, OAuth2AutenthicationDat } requestBuilder - .withMethod("POST") - .addQueryParam("grant_type", "password") + .withGrantType(authenticationData.getGrant()) + .withRequestContentType(authenticationData.getRequest()) .addQueryParam("client_id", authenticationData.getClient().getId()) .addQueryParam("client_secret", authenticationData.getClient().getSecret()) .addQueryParam("username", authenticationData.getUsername()) 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 index 6bd9e447..f929c833 100644 --- 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 @@ -16,23 +16,31 @@ 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.net.URLEncoder; -import java.net.http.HttpRequest; import java.nio.charset.StandardCharsets; -import java.time.Duration; import java.util.HashMap; import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; -public class HttpRequestBuilder { +class HttpRequestBuilder { private final Map> headers; @@ -42,86 +50,99 @@ public class HttpRequestBuilder { private URI uri; - private String method; + private OAuth2AutenthicationData.OAuth2AutenthicationDataGrant grantType; + + private Oauth2TokenRequestEncoding requestContentType = APPLICATION_X_WWW_FORM_URLENCODED; - public HttpRequestBuilder(WorkflowApplication app) { + HttpRequestBuilder(WorkflowApplication app) { this.app = app; headers = new HashMap<>(); queryParams = new HashMap<>(); } - public HttpRequestBuilder addHeader(String key, String token) { + HttpRequestBuilder addHeader(String key, String token) { headers.put(key, WorkflowUtils.buildStringFilter(app, token)); return this; } - public HttpRequestBuilder addQueryParam(String key, String token) { + HttpRequestBuilder addQueryParam(String key, String token) { queryParams.put(key, WorkflowUtils.buildStringFilter(app, token)); return this; } - public HttpRequestBuilder withUri(URI uri) { + HttpRequestBuilder withUri(URI uri) { this.uri = uri; return this; } - public HttpRequestBuilder withMethod(String method) { - this.method = method; + HttpRequestBuilder withRequestContentType(OAuth2TokenRequest oAuth2TokenRequest) { + if (oAuth2TokenRequest != null) { + this.requestContentType = oAuth2TokenRequest.getEncoding(); + } + return this; + } + + HttpRequestBuilder withGrantType( + OAuth2AutenthicationData.OAuth2AutenthicationDataGrant grantType) { + this.grantType = grantType; return this; } - public HttpRequest build(WorkflowContext workflow, TaskContext task, WorkflowModel model) { - HttpRequest.Builder request = HttpRequest.newBuilder(); + 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) { - request = request.header(entry.getKey(), headerValue); + builder.header(entry.getKey(), headerValue); } } - request.header("Accept", "application/json"); + 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 v = value.apply(workflow, task, model); + String encodedKey = URLEncoder.encode(key, StandardCharsets.UTF_8); + String encodedValue = URLEncoder.encode(v, StandardCharsets.UTF_8); + form.param(encodedKey, encodedValue); + }); + entity = Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED); + } else { + Map jsonData = new HashMap<>(); + jsonData.put("grant_type", grantType.value()); + queryParams.forEach( + (key, value) -> { + String v = value.apply(workflow, task, model); + String encodedKey = URLEncoder.encode(key, StandardCharsets.UTF_8); + String encodedValue = URLEncoder.encode(v, StandardCharsets.UTF_8); + jsonData.put(encodedKey, encodedValue); + }); + entity = Entity.entity(jsonData, MediaType.APPLICATION_JSON); + } + return new InvocationHolder(client, () -> builder.post(entity)); + } + + private void validate() { if (uri == null) { throw new IllegalStateException("URI must be set before building the request"); } - String encoded = - queryParams.entrySet().stream() - .map( - e -> { - String v = e.getValue().apply(workflow, task, model); - if (v == null) return null; - return URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8) - + "=" - + URLEncoder.encode(v, StandardCharsets.UTF_8); - }) - .filter(Objects::nonNull) - .collect(Collectors.joining("&")); - - if (method != null) { - switch (method.toUpperCase()) { - case "GET" -> { - if (!encoded.isEmpty()) { - String sep = (uri.getQuery() == null || uri.getQuery().isEmpty()) ? "?" : "&"; - uri = URI.create(uri.toString() + sep + encoded); - } - request.uri(uri).GET(); - } - case "POST" -> { - request.uri(uri); - HttpRequest.BodyPublisher body = - encoded.isEmpty() - ? HttpRequest.BodyPublishers.noBody() - : HttpRequest.BodyPublishers.ofString(encoded); - request.POST(body); - } - default -> throw new IllegalArgumentException("Unsupported HTTP method: " + method); - } - } else { - throw new IllegalStateException("HTTP method must be set before building the request"); + if (grantType == null) { + throw new IllegalStateException("Grant type must be set before building the request"); } - request.timeout(Duration.ofSeconds(15)); - return request.build(); } } 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..42e9fd37 --- /dev/null +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/InvocationHolder.java @@ -0,0 +1,42 @@ +/* + * 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.util.function.Supplier; + +class InvocationHolder { + + private final Client client; + private final Supplier call; + + InvocationHolder(Client client, Supplier call) { + this.client = client; + this.call = call; + } + + Response invoke() { + return call.get(); + } + + 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 index 16670fba..8461ecd9 100644 --- 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 @@ -16,7 +16,7 @@ package io.serverlessworkflow.impl.executors.http.oauth; -import static io.serverlessworkflow.api.types.OAuth2AutenthicationDataClient.ClientAuthentication.*; +import static io.serverlessworkflow.api.types.OAuth2AutenthicationDataClient.ClientAuthentication.CLIENT_SECRET_POST; import io.serverlessworkflow.api.types.OAuth2AutenthicationData; import io.serverlessworkflow.api.types.OAuth2AutenthicationDataClient; 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 index 3c5ee28c..dbf0d771 100644 --- 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 @@ -16,54 +16,49 @@ package io.serverlessworkflow.impl.executors.http.oauth; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowError; import io.serverlessworkflow.impl.WorkflowException; -import java.io.IOException; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.client.ResponseProcessingException; +import jakarta.ws.rs.core.Response; +import java.util.Map; import java.util.function.BiFunction; -public class TokenResponseHandler implements BiFunction { - - private static final ObjectMapper MAPPER = new ObjectMapper(); - private static final HttpClient CLIENT = - HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); +public class TokenResponseHandler + implements BiFunction> { @Override - public JsonNode apply(HttpRequest requestBuilder, TaskContext context) { - HttpResponse response; - try { - response = CLIENT.send(requestBuilder, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() < 200 || response.statusCode() >= 300) { + public Map apply(InvocationHolder invocation, TaskContext context) { + try (Response response = invocation.invoke()) { + if (response.getStatus() < 200 || response.getStatus() >= 300) { throw new WorkflowException( WorkflowError.communication( - response.statusCode(), + response.getStatus(), context, "Failed to obtain token: HTTP " - + response.statusCode() + + response.getStatus() + " — " - + response.body()) + + response.getEntity()) .build()); } - } catch (java.net.ConnectException e) { - throw new RuntimeException("Connection refused: " + e.getMessage(), e); - } catch (IOException e) { - throw new RuntimeException("Unable to send request: " + e.getMessage(), e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Unable to send request: " + e.getMessage(), e); - } - - try { - return MAPPER.readTree(response.body()); - } catch (JsonProcessingException e) { - throw new RuntimeException("Failed to parse JSON response: " + e.getMessage(), e); + return (Map) response.readEntity(Map.class); + } 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 index aac71a2a..07ba342f 100644 --- a/impl/http/src/test/java/io/serverlessworkflow/impl/OAuthHTTPWorkflowDefinitionTest.java +++ b/impl/http/src/test/java/io/serverlessworkflow/impl/OAuthHTTPWorkflowDefinitionTest.java @@ -113,8 +113,7 @@ public void testOAuthClientSecretPostPasswordWorkflowExecution() throws Exceptio RecordedRequest tokenRequest = authServer.takeRequest(); assertEquals("POST", tokenRequest.getMethod()); assertEquals("/realms/test-realm/protocol/openid-connect/token", tokenRequest.getPath()); - assertEquals( - "application/x-www-form-urlencoded; charset=UTF-8", tokenRequest.getHeader("Content-Type")); + assertEquals("application/x-www-form-urlencoded", tokenRequest.getHeader("Content-Type")); String tokenRequestBody = tokenRequest.getBody().readUtf8(); assertTrue(tokenRequestBody.contains("grant_type=password")); @@ -177,8 +176,7 @@ public void testOAuthClientSecretPostClientCredentialsWorkflowExecution() throws RecordedRequest tokenRequest = authServer.takeRequest(); assertEquals("POST", tokenRequest.getMethod()); assertEquals("/realms/test-realm/protocol/openid-connect/token", tokenRequest.getPath()); - assertEquals( - "application/x-www-form-urlencoded; charset=UTF-8", tokenRequest.getHeader("Content-Type")); + assertEquals("application/x-www-form-urlencoded", tokenRequest.getHeader("Content-Type")); String tokenRequestBody = tokenRequest.getBody().readUtf8(); assertTrue(tokenRequestBody.contains("grant_type=client_credentials")); diff --git a/impl/jwt-impl/pom.xml b/impl/jwt-impl/pom.xml new file mode 100644 index 00000000..4db2de0f --- /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-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/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/JWT.java b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/DefaultJWTConverter.java similarity index 61% rename from impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/JWT.java rename to impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/DefaultJWTConverter.java index 1aa92eb2..0525d1b8 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/JWT.java +++ b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/DefaultJWTConverter.java @@ -14,42 +14,32 @@ * limitations under the License. */ -package io.serverlessworkflow.impl.executors.http.oauth; +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 JWT { +public class DefaultJWTConverter implements JWTConverter { private static final ObjectMapper MAPPER = new ObjectMapper(); - private final String token; - private final Map claims; - - private JWT(String token, Map claims) { - this.token = token; - this.claims = claims; - } - - public static JWT fromString(String token) throws JsonProcessingException { + @Override + public JWT fromToken(String token) throws IllegalArgumentException { String[] parts = token.split("\\."); if (parts.length < 2) { throw new IllegalArgumentException("Invalid JWT token format"); } - - String payloadJson = - new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); - return new JWT(token, MAPPER.readValue(payloadJson, Map.class)); - } - - public String getToken() { - return token; - } - - public Object getClaim(String name) { - return claims.get(name); + try { + String payloadJson = + new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); + return new DefaultJWTImpl(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/DefaultJWTImpl.java b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/DefaultJWTImpl.java new file mode 100644 index 00000000..df8188be --- /dev/null +++ b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/DefaultJWTImpl.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 io.serverlessworkflow.http.jwt.JWT; +import java.util.Map; + +public class DefaultJWTImpl implements JWT { + + private final Map claims; + private final String token; + + DefaultJWTImpl(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..d9b6dfbc --- /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.DefaultJWTConverter \ No newline at end of file diff --git a/impl/pom.xml b/impl/pom.xml index ef2215de..04948335 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -58,11 +58,6 @@ jakarta.ws.rs-api ${version.jakarta.ws.rs} - - com.fasterxml.jackson.core - jackson-databind - ${version.jackson-databind} - org.glassfish.jersey.core jersey-client @@ -81,5 +76,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..cebc7d59 --- /dev/null +++ b/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWT.java @@ -0,0 +1,24 @@ +/* + * 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..7f1f67e1 --- /dev/null +++ b/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWTConverter.java @@ -0,0 +1,28 @@ +/* + * 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 57e7d896..27d6853d 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,7 @@ examples experimental fluent + jwt @@ -167,6 +168,16 @@ serverlessworkflow-api ${project.version} + + io.serverlessworkflow + serverlessworkflow-jwt + ${project.version} + + + io.serverlessworkflow + serverlessworkflow-impl-jwt + ${project.version} + com.networknt json-schema-validator From 6f56995a6d480728151dad55285e980b0c9a151f Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Wed, 20 Aug 2025 11:19:12 -0700 Subject: [PATCH 4/6] post review Signed-off-by: Dmitrii Tikhomirov --- impl/http/pom.xml | 2 +- .../impl/executors/http/OAuth2AuthProvider.java | 13 +++---------- .../http/oauth/AccessTokenProvider.java | 16 ++++++---------- .../executors/http/oauth/HttpRequestBuilder.java | 10 +++------- .../executors/http/oauth/InvocationHolder.java | 8 +++++--- .../http/oauth/TokenResponseHandler.java | 2 +- impl/jwt-impl/pom.xml | 2 +- ...WTConverter.java => JacksonJWTConverter.java} | 4 ++-- .../{DefaultJWTImpl.java => JacksonJWTImpl.java} | 4 ++-- .../io.serverlessworkflow.http.jwt.JWTConverter | 2 +- impl/pom.xml | 1 - pom.xml | 2 +- 12 files changed, 26 insertions(+), 40 deletions(-) rename impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/{DefaultJWTConverter.java => JacksonJWTConverter.java} (92%) rename impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/{DefaultJWTImpl.java => JacksonJWTImpl.java} (91%) diff --git a/impl/http/pom.xml b/impl/http/pom.xml index 4d85268b..4b128a45 100644 --- a/impl/http/pom.xml +++ b/impl/http/pom.xml @@ -40,7 +40,7 @@ io.serverlessworkflow - serverlessworkflow-impl-jwt + serverlessworkflow-impl-jackson-jwt test 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 ef7d21f6..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 @@ -29,18 +29,15 @@ public class OAuth2AuthProvider implements AuthProvider { - private Oauth2 oauth2; - - private final WorkflowApplication workflowApplication; + private OAuthRequestBuilder requestBuilder; private static final String BEARER_TOKEN = "%s %s"; public OAuth2AuthProvider( WorkflowApplication application, Workflow workflow, OAuth2AuthenticationPolicy authPolicy) { - this.workflowApplication = application; Oauth2 oauth2 = authPolicy.getOauth2(); if (oauth2.getOAuth2ConnectAuthenticationProperties() != null) { - this.oauth2 = oauth2; + this.requestBuilder = new OAuthRequestBuilder(application, oauth2); } else if (oauth2.getOAuth2AuthenticationPolicySecret() != null) { throw new UnsupportedOperationException("Secrets are still not supported"); } @@ -55,11 +52,7 @@ public Builder build( @Override public void preRequest( Invocation.Builder builder, WorkflowContext workflow, TaskContext task, WorkflowModel model) { - JWT token = - new OAuthRequestBuilder(workflowApplication, oauth2) - .build(workflow, task, model) - .validateAndGet(); - + JWT token = requestBuilder.build(workflow, task, model).validateAndGet(); String tokenType = (String) token.getClaim("typ"); builder.header( AuthProviderFactory.AUTH_HEADER_NAME, 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 index 57983835..d2ca9286 100644 --- 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 @@ -38,14 +38,10 @@ public class AccessTokenProvider { this.issuers = issuers; this.context = context; - ServiceLoader jwtConverters = - ServiceLoader.load(JWTConverter.class, AccessTokenProvider.class.getClassLoader()); - - if (jwtConverters.iterator().hasNext()) { - this.jwtConverter = jwtConverters.iterator().next(); - } else { - throw new RuntimeException("No JWTConverter implementation found"); - } + this.jwtConverter = + ServiceLoader.load(JWTConverter.class) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No JWTConverter implementation found")); } public JWT validateAndGet() { @@ -54,12 +50,12 @@ public JWT validateAndGet() { try { jwt = jwtConverter.fromToken((String) token.get("access_token")); } catch (IllegalArgumentException e) { - throw new RuntimeException("Failed to parse JWT token: " + e.getMessage(), e); + throw new IllegalStateException("Failed to parse JWT token: " + e.getMessage(), e); } if (!(issuers == null || issuers.isEmpty())) { String tokenIssuer = (String) jwt.getClaim("iss"); if (tokenIssuer == null || tokenIssuer.isEmpty() || !issuers.contains(tokenIssuer)) { - throw new RuntimeException("Token issuer is not valid: " + 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/HttpRequestBuilder.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/HttpRequestBuilder.java index f929c833..d5d30cf5 100644 --- 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 @@ -39,6 +39,7 @@ import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import java.util.Objects; class HttpRequestBuilder { @@ -137,12 +138,7 @@ InvocationHolder build(WorkflowContext workflow, TaskContext task, WorkflowModel } private void validate() { - if (uri == null) { - throw new IllegalStateException("URI must be set before building the request"); - } - - if (grantType == null) { - throw new IllegalStateException("Grant type must be set before building the request"); - } + 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 index 42e9fd37..a529a429 100644 --- 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 @@ -18,9 +18,11 @@ 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 { +class InvocationHolder implements Callable, Closeable { private final Client client; private final Supplier call; @@ -30,11 +32,11 @@ class InvocationHolder { this.call = call; } - Response invoke() { + public Response call() { return call.get(); } - void close() { + public void close() { if (client != null) { client.close(); } 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 index dbf0d771..068fe6e7 100644 --- 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 @@ -30,7 +30,7 @@ public class TokenResponseHandler @Override public Map apply(InvocationHolder invocation, TaskContext context) { - try (Response response = invocation.invoke()) { + try (Response response = invocation.call()) { if (response.getStatus() < 200 || response.getStatus() >= 300) { throw new WorkflowException( WorkflowError.communication( diff --git a/impl/jwt-impl/pom.xml b/impl/jwt-impl/pom.xml index 4db2de0f..64795e76 100644 --- a/impl/jwt-impl/pom.xml +++ b/impl/jwt-impl/pom.xml @@ -9,7 +9,7 @@ 8.0.0-SNAPSHOT - serverlessworkflow-impl-jwt + serverlessworkflow-impl-jackson-jwt Serverless Workflow :: Impl :: JWT diff --git a/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/DefaultJWTConverter.java b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTConverter.java similarity index 92% rename from impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/DefaultJWTConverter.java rename to impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTConverter.java index 0525d1b8..54e4feb7 100644 --- a/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/DefaultJWTConverter.java +++ b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTConverter.java @@ -24,7 +24,7 @@ import java.util.Base64; import java.util.Map; -public class DefaultJWTConverter implements JWTConverter { +public class JacksonJWTConverter implements JWTConverter { private static final ObjectMapper MAPPER = new ObjectMapper(); @@ -37,7 +37,7 @@ public JWT fromToken(String token) throws IllegalArgumentException { try { String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); - return new DefaultJWTImpl(token, MAPPER.readValue(payloadJson, Map.class)); + 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/DefaultJWTImpl.java b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTImpl.java similarity index 91% rename from impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/DefaultJWTImpl.java rename to impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTImpl.java index df8188be..bb02ea8b 100644 --- a/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/DefaultJWTImpl.java +++ b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTImpl.java @@ -19,12 +19,12 @@ import io.serverlessworkflow.http.jwt.JWT; import java.util.Map; -public class DefaultJWTImpl implements JWT { +public class JacksonJWTImpl implements JWT { private final Map claims; private final String token; - DefaultJWTImpl(String token, Map claims) { + JacksonJWTImpl(String token, Map claims) { this.token = token; this.claims = claims; } 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 index d9b6dfbc..1c898a8b 100644 --- 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 @@ -1 +1 @@ -io.serverlessworkflow.impl.http.jwt.DefaultJWTConverter \ No newline at end of file +io.serverlessworkflow.impl.http.jwt.JacksonJWTConverter \ No newline at end of file diff --git a/impl/pom.xml b/impl/pom.xml index 04948335..db10ad78 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -14,7 +14,6 @@ 1.4.0 5.2.3 4.0.0 - 2.19.2 diff --git a/pom.xml b/pom.xml index 27d6853d..f0775854 100644 --- a/pom.xml +++ b/pom.xml @@ -175,7 +175,7 @@ io.serverlessworkflow - serverlessworkflow-impl-jwt + serverlessworkflow-impl-jackson-jwt ${project.version} From 0f358b63a4bc0138233d4102dce0823179ebc612 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Wed, 20 Aug 2025 16:39:27 -0700 Subject: [PATCH 5/6] tests and fixies Signed-off-by: Dmitrii Tikhomirov --- .../http/oauth/AccessTokenProvider.java | 1 - .../http/oauth/ClientSecretBasic.java | 1 - .../http/oauth/ClientSecretPostStep.java | 1 - .../http/oauth/HttpRequestBuilder.java | 15 +- .../http/oauth/InvocationHolder.java | 1 - .../http/oauth/OAuthRequestBuilder.java | 22 +- .../http/oauth/TokenResponseHandler.java | 1 - .../impl/OAuthHTTPWorkflowDefinitionTest.java | 754 +++++++++++++++++- ...etPostClientCredentialsParamsHttpCall.yaml | 25 + ...ntCredentialsParamsNoEndPointHttpCall.yaml | 21 + ...ntSecretPostPasswordAllGrantsHttpCall.yaml | 29 + ...ClientSecretPostPasswordAsArgHttpCall.yaml | 25 + ...SecretPostPasswordNoEndpointsHttpCall.yaml | 21 + .../oAuthJSONClientCredentialsHttpCall.yaml | 25 + ...thJSONClientCredentialsParamsHttpCall.yaml | 25 + ...ntCredentialsParamsNoEndPointHttpCall.yaml | 23 + .../oAuthJSONPasswordAllGrantsHttpCall.yaml | 29 + .../oAuthJSONPasswordAsArgHttpCall.yaml | 27 + .../resources/oAuthJSONPasswordHttpCall.yaml | 27 + .../oAuthJSONPasswordNoEndpointsHttpCall.yaml | 23 + .../impl/http/jwt/JacksonJWTConverter.java | 1 - .../impl/http/jwt/JacksonJWTImpl.java | 1 - .../io/serverlessworkflow/http/jwt/JWT.java | 1 - .../http/jwt/JWTConverter.java | 1 - 24 files changed, 1038 insertions(+), 62 deletions(-) create mode 100644 impl/http/src/test/resources/oAuthClientSecretPostClientCredentialsParamsHttpCall.yaml create mode 100644 impl/http/src/test/resources/oAuthClientSecretPostClientCredentialsParamsNoEndPointHttpCall.yaml create mode 100644 impl/http/src/test/resources/oAuthClientSecretPostPasswordAllGrantsHttpCall.yaml create mode 100644 impl/http/src/test/resources/oAuthClientSecretPostPasswordAsArgHttpCall.yaml create mode 100644 impl/http/src/test/resources/oAuthClientSecretPostPasswordNoEndpointsHttpCall.yaml create mode 100644 impl/http/src/test/resources/oAuthJSONClientCredentialsHttpCall.yaml create mode 100644 impl/http/src/test/resources/oAuthJSONClientCredentialsParamsHttpCall.yaml create mode 100644 impl/http/src/test/resources/oAuthJSONClientCredentialsParamsNoEndPointHttpCall.yaml create mode 100644 impl/http/src/test/resources/oAuthJSONPasswordAllGrantsHttpCall.yaml create mode 100644 impl/http/src/test/resources/oAuthJSONPasswordAsArgHttpCall.yaml create mode 100644 impl/http/src/test/resources/oAuthJSONPasswordHttpCall.yaml create mode 100644 impl/http/src/test/resources/oAuthJSONPasswordNoEndpointsHttpCall.yaml 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 index d2ca9286..d7290ab0 100644 --- 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 @@ -13,7 +13,6 @@ * 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; 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 index 76fb3ea1..6dfbe260 100644 --- 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 @@ -13,7 +13,6 @@ * 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; 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 index 63af6f70..e00ca28a 100644 --- 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 @@ -13,7 +13,6 @@ * 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; 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 index d5d30cf5..fff003d6 100644 --- 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 @@ -13,7 +13,6 @@ * 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; @@ -35,8 +34,6 @@ import jakarta.ws.rs.core.Form; import jakarta.ws.rs.core.MediaType; import java.net.URI; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -115,10 +112,8 @@ InvocationHolder build(WorkflowContext workflow, TaskContext task, WorkflowModel form.param("grant_type", grantType.value()); queryParams.forEach( (key, value) -> { - String v = value.apply(workflow, task, model); - String encodedKey = URLEncoder.encode(key, StandardCharsets.UTF_8); - String encodedValue = URLEncoder.encode(v, StandardCharsets.UTF_8); - form.param(encodedKey, encodedValue); + String resolved = value.apply(workflow, task, model); + form.param(key, resolved); }); entity = Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED); } else { @@ -126,10 +121,8 @@ InvocationHolder build(WorkflowContext workflow, TaskContext task, WorkflowModel jsonData.put("grant_type", grantType.value()); queryParams.forEach( (key, value) -> { - String v = value.apply(workflow, task, model); - String encodedKey = URLEncoder.encode(key, StandardCharsets.UTF_8); - String encodedValue = URLEncoder.encode(v, StandardCharsets.UTF_8); - jsonData.put(encodedKey, encodedValue); + String resolved = value.apply(workflow, task, model); + jsonData.put(key, resolved); }); entity = Entity.entity(jsonData, MediaType.APPLICATION_JSON); } 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 index a529a429..6655be8e 100644 --- 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 @@ -13,7 +13,6 @@ * 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; 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 index 8461ecd9..c78e16f2 100644 --- 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 @@ -13,7 +13,6 @@ * 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; @@ -27,8 +26,11 @@ 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 { @@ -112,9 +114,21 @@ public void audience(HttpRequestBuilder requestBuilder) { } private void scope(HttpRequestBuilder requestBuilder) { - if (authenticationData.getScopes() != null && !authenticationData.getScopes().isEmpty()) { - String scopes = String.join(" ", authenticationData.getScopes()); - requestBuilder.addQueryParam("scope", scopes); + 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); } } 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 index 068fe6e7..4529dbfe 100644 --- 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 @@ -13,7 +13,6 @@ * 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; diff --git a/impl/http/src/test/java/io/serverlessworkflow/impl/OAuthHTTPWorkflowDefinitionTest.java b/impl/http/src/test/java/io/serverlessworkflow/impl/OAuthHTTPWorkflowDefinitionTest.java index 07ba342f..aa9d5859 100644 --- a/impl/http/src/test/java/io/serverlessworkflow/impl/OAuthHTTPWorkflowDefinitionTest.java +++ b/impl/http/src/test/java/io/serverlessworkflow/impl/OAuthHTTPWorkflowDefinitionTest.java @@ -13,7 +13,6 @@ * 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; @@ -39,6 +38,23 @@ 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; @@ -67,17 +83,7 @@ void tearDown() throws IOException { @Test public void testOAuthClientSecretPostPasswordWorkflowExecution() throws Exception { String jwt = fakeAccessToken(); - - String tokenResponse = - """ - { - "access_token": "%s", - "token_type": "Bearer", - "expires_in": 3600, - "scope": "read write" - } - """ - .formatted(jwt); + String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt); authServer.enqueue( new MockResponse() @@ -85,16 +91,9 @@ public void testOAuthClientSecretPostPasswordWorkflowExecution() throws Exceptio .setHeader("Content-Type", "application/json") .setResponseCode(200)); - String response = - """ - { - "message": "Hello World" - } - """; - apiServer.enqueue( new MockResponse() - .setBody(response) + .setBody(RESPONSE) .setHeader("Content-Type", "application/json") .setResponseCode(200)); @@ -127,19 +126,62 @@ public void testOAuthClientSecretPostPasswordWorkflowExecution() throws Exceptio } @Test - public void testOAuthClientSecretPostClientCredentialsWorkflowExecution() throws Exception { + 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")); - String tokenResponse = - """ - { - "access_token": "%s", - "token_type": "Bearer", - "expires_in": 3600, - "scope": "read write" - } - """ - .formatted(jwt); + 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() @@ -147,16 +189,123 @@ public void testOAuthClientSecretPostClientCredentialsWorkflowExecution() throws .setHeader("Content-Type", "application/json") .setResponseCode(200)); - String response = - """ - { - "message": "Hello World" - } - """; + 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) + .setBody(RESPONSE) .setHeader("Content-Type", "application/json") .setResponseCode(200)); @@ -189,6 +338,535 @@ public void testOAuthClientSecretPostClientCredentialsWorkflowExecution() throws 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( 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/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/src/main/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTConverter.java b/impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTConverter.java index 54e4feb7..850538ab 100644 --- 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 @@ -13,7 +13,6 @@ * 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; 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 index bb02ea8b..9101646f 100644 --- 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 @@ -13,7 +13,6 @@ * 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; diff --git a/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWT.java b/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWT.java index cebc7d59..8d2f19da 100644 --- a/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWT.java +++ b/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWT.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.serverlessworkflow.http.jwt; public interface JWT { diff --git a/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWTConverter.java b/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWTConverter.java index 7f1f67e1..fd3d759e 100644 --- a/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWTConverter.java +++ b/jwt/src/main/java/io/serverlessworkflow/http/jwt/JWTConverter.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.serverlessworkflow.http.jwt; public interface JWTConverter { From f36711653a3a3b2acaf2b7c1a8c2c783a5cb99c2 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Thu, 21 Aug 2025 08:56:36 -0700 Subject: [PATCH 6/6] one more fix Signed-off-by: Dmitrii Tikhomirov --- .../impl/executors/http/oauth/AccessTokenProvider.java | 7 +------ .../impl/executors/http/oauth/TokenResponseHandler.java | 3 ++- 2 files changed, 3 insertions(+), 7 deletions(-) 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 index d7290ab0..0b4f6cce 100644 --- 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 @@ -45,12 +45,7 @@ public class AccessTokenProvider { public JWT validateAndGet() { Map token = tokenResponseHandler.apply(invocation, context); - JWT jwt; - try { - jwt = jwtConverter.fromToken((String) token.get("access_token")); - } catch (IllegalArgumentException e) { - throw new IllegalStateException("Failed to parse JWT token: " + e.getMessage(), e); - } + 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)) { 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 index 4529dbfe..a04998e8 100644 --- 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 @@ -20,6 +20,7 @@ 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; @@ -41,7 +42,7 @@ public Map apply(InvocationHolder invocation, TaskContext contex + response.getEntity()) .build()); } - return (Map) response.readEntity(Map.class); + return response.readEntity(new GenericType<>() {}); } catch (ResponseProcessingException e) { throw new WorkflowException( WorkflowError.communication(