This is an automated email from the ASF dual-hosted git repository.

fjtiradosarti pushed a commit to branch main
in repository 
https://gitbox.apache.org/repos/asf/incubator-kie-kogito-runtimes.git


The following commit(s) were added to refs/heads/main by this push:
     new cc88f1d92d apache/incubator-kie-issues#1894: introduce OAUTH2 token 
exchange (#3928)
cc88f1d92d is described below

commit cc88f1d92db0ebbfe3bb9e6f23789225a015ba6c
Author: gabriel-farache <[email protected]>
AuthorDate: Mon Aug 4 09:55:49 2025 +0000

    apache/incubator-kie-issues#1894: introduce OAUTH2 token exchange (#3928)
    
    * apache/incubator-kie-issues#1894: introduce OAUTH2 token exchange
    
    Signed-off-by: gabriel-farache <[email protected]>
    
    * Add IT
    
    * Add license header
    
    * Fix default access token: get it from header
    
    * Scope quarkus-oidc-client to provided
    
    Signed-off-by: gabriel-farache <[email protected]>
    
    * Fix Keycloak mock issue with body matching
    
    Signed-off-by: gabriel-farache <[email protected]>
    
    * Update 
quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow/src/main/java/org/kie/kogito/serverless/workflow/openapi/OpenApiCustomCredentialProvider.java
    
    Co-authored-by: Gonzalo Muñoz <[email protected]>
    
    * Apply feedback
    
    Signed-off-by: gabriel-farache <[email protected]>
    
    * Apply feedback for property naming
    
    Signed-off-by: gabriel-farache <[email protected]>
    
    * Apply suggestions from code review
    
    Co-authored-by: Francisco Javier Tirado Sarti 
<[email protected]>
    
    * fix suggested changes
    
    Signed-off-by: gabriel-farache <[email protected]>
    
    ---------
    
    Signed-off-by: gabriel-farache <[email protected]>
    Co-authored-by: Gonzalo Muñoz <[email protected]>
    Co-authored-by: Francisco Javier Tirado Sarti 
<[email protected]>
---
 .gitignore                                         |   4 +-
 .../pom.xml                                        |  10 ++
 .../src/main/resources/application.properties      |  46 ++++++++
 .../specs/token-exchange-external-service.yaml     |  95 +++++++++++++++++
 .../src/main/resources/token-exchange.sw.json      | 117 +++++++++++++++++++++
 .../quarkus/workflows/KeycloakServiceMock.java     |  68 +++++++++---
 .../TokenExchangeExternalServicesMock.java         |  87 +++++++++++++++
 .../kogito/quarkus/workflows/TokenExchangeIT.java  |  58 ++++++++++
 .../kogito-quarkus-serverless-workflow/pom.xml     |   9 ++
 .../openapi/OpenApiCustomCredentialProvider.java   | 114 ++++++++++++++++++++
 10 files changed, 591 insertions(+), 17 deletions(-)

diff --git a/.gitignore b/.gitignore
index fa15842c86..dbf0621888 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,4 +43,6 @@ target/
 *.log
 
 # Apache RAT check excludes file
-!.rat-excludes
\ No newline at end of file
+!.rat-excludes
+
+quarkus/integration-tests/integration-tests-quarkus-gradle/integration-tests-quarkus-gradle-project/**
\ No newline at end of file
diff --git 
a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/pom.xml
 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/pom.xml
index 3e3270f82e..adc6c1c9c9 100644
--- 
a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/pom.xml
+++ 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/pom.xml
@@ -118,6 +118,16 @@
       <groupId>io.quarkus</groupId>
       <artifactId>quarkus-grpc-deployment</artifactId>
     </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-oidc-client-deployment</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-oidc-common-deployment</artifactId>
+      <scope>provided</scope>
+    </dependency>
   </dependencies>
 
   <build>
diff --git 
a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/application.properties
 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/application.properties
index c581abbc87..979d8bc464 100644
--- 
a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/application.properties
+++ 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/application.properties
@@ -223,6 +223,51 @@ quarkus.oidc-client.service5_oauth2.grant.type=client
 quarkus.oidc-client.service5_oauth2.credentials.client-secret.method=basic
 quarkus.oidc-client.service5_oauth2.credentials.client-secret.value=secret
 
+# -------
+# Token propagation support test properties, relates to the TokenExchangeIT 
and the token-exchange.sw.json
+# -------
+# 1) Configure the desired packages for the code generation, this information 
is basically source
+quarkus.openapi-generator.codegen.spec.token_exchange_external_service_yaml.base-package=org.acme.externalexchangeservice
+
+# 2) Configure the access url for the service.
+quarkus.rest-client.token_exchange_external_service_yaml.url=${exchange-external-service-mock.url}
+sonataflow.security.auth.with_exchange_oauth2.exchange-token=true
+sonataflow.security.auth.with_exchange_and_propagation_oauth2.exchange-token=true
+
+# 3) Configure the different propagation alternatives.
+# default propagation for token_propagation_external_service1 invocation
+quarkus.openapi-generator.token_exchange_external_service_yaml.auth.with_exchange_and_propagation_oauth2.token-propagation=true
+
+# 4) Oidc clients for the services that has oauth2 security.
+# Oidc client used to test with exchange
+quarkus.oidc-client.with_exchange_oauth2.auth-server-url=${keycloak.mock.service.url}
+quarkus.oidc-client.with_exchange_oauth2.token-path=${keycloak.mock.exchange-service.token-path}
+quarkus.oidc-client.with_exchange_oauth2.discovery-enabled=false
+quarkus.oidc-client.with_exchange_oauth2.client-id=kogito-app
+quarkus.oidc-client.with_exchange_oauth2.grant.type=exchange
+quarkus.oidc-client.with_exchange_oauth2.credentials.client-secret.method=basic
+quarkus.oidc-client.with_exchange_oauth2.credentials.client-secret.value=secret
+
+# Oidc client used to test without exchange
+quarkus.oidc-client.without_exchange_oauth2.auth-server-url=${keycloak.mock.service.url}
+quarkus.oidc-client.without_exchange_oauth2.token-path=${keycloak.mock.service.token-path}
+quarkus.oidc-client.without_exchange_oauth2.discovery-enabled=false
+quarkus.oidc-client.without_exchange_oauth2.client-id=kogito-app
+quarkus.oidc-client.without_exchange_oauth2.grant.type=client
+quarkus.oidc-client.without_exchange_oauth2.credentials.client-secret.method=basic
+quarkus.oidc-client.without_exchange_oauth2.credentials.client-secret.value=secret
+
+# Oidc client used to test with exchange and with propagation
+quarkus.oidc-client.with_exchange_and_propagation_oauth2.auth-server-url=${keycloak.mock.service.url}
+quarkus.oidc-client.with_exchange_and_propagation_oauth2.token-path=${keycloak.mock.exchange-service.token-path}
+quarkus.oidc-client.with_exchange_and_propagation_oauth2.discovery-enabled=false
+quarkus.oidc-client.with_exchange_and_propagation_oauth2.client-id=kogito-app
+quarkus.oidc-client.with_exchange_and_propagation_oauth2.grant.type=exchange
+quarkus.oidc-client.with_exchange_and_propagation_oauth2.credentials.client-secret.method=basic
+quarkus.oidc-client.with_exchange_and_propagation_oauth2.credentials.client-secret.value=secret
+
+# --------- END TokenExchangeIT ------------
+
 mp.messaging.outgoing.kogito-processinstances-events.connector=smallrye-kafka
 
mp.messaging.outgoing.kogito-processinstances-events.topic=kogito-processinstances-events
 
mp.messaging.outgoing.kogito-processinstances-events.value.serializer=org.apache.kafka.common.serialization.StringSerializer
@@ -258,3 +303,4 @@ quarkus.http.auth.permission.default.policy=authenticated
 quarkus.security.users.embedded.enabled=true
 quarkus.security.users.embedded.plain-text=true
 quarkus.security.users.embedded.users.buddy=buddy
+quarkus.log.category."org.apache.http".level=INFO
\ No newline at end of file
diff --git 
a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/specs/token-exchange-external-service.yaml
 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/specs/token-exchange-external-service.yaml
new file mode 100644
index 0000000000..e666f8e2be
--- /dev/null
+++ 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/specs/token-exchange-external-service.yaml
@@ -0,0 +1,95 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+#
+
+---
+openapi: 3.0.3
+info:
+  title: external-service5 API
+  version: 2.0.0-SNAPSHOT
+paths:
+  /token-exchange-external-service/withExchange:
+    post:
+      operationId: withExchange
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/QueryRequest'
+      responses:
+        "200":
+          description: OK
+      security:
+        - with-exchange-oauth2: []
+  /token-exchange-external-service/withoutExchange:
+    post:
+      operationId: withoutExchange
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/QueryRequest'
+      responses:
+        "200":
+          description: OK
+      security:
+        - without-exchange-oauth2: [ ]
+  /token-exchange-external-service/withExchangeAndPropagation:
+    post:
+      operationId: withExchangeAndPropagation
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/QueryRequest'
+      responses:
+        "200":
+          description: OK
+      security:
+        - with-exchange-and-propagation-oauth2: [ ]
+components:
+  schemas:
+    QueryRequest:
+      type: object
+      properties:
+        processInstanceId:
+          type: string
+        query:
+          type: string
+  securitySchemes:
+    with-exchange-oauth2:
+        type: oauth2
+        flows:
+          clientCredentials:
+            authorizationUrl: https://example.com/oauth
+            tokenUrl: https://example.com/oauth/token
+            scopes: {}
+    without-exchange-oauth2:
+      type: oauth2
+      flows:
+        clientCredentials:
+          authorizationUrl: https://example.com/oauth
+          tokenUrl: https://example.com/oauth/token
+          scopes: { }
+    with-exchange-and-propagation-oauth2:
+      type: oauth2
+      flows:
+        clientCredentials:
+          authorizationUrl: https://example.com/oauth
+          tokenUrl: https://example.com/oauth/token
+          scopes: { }
\ No newline at end of file
diff --git 
a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/token-exchange.sw.json
 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/token-exchange.sw.json
new file mode 100644
index 0000000000..887bcdcad9
--- /dev/null
+++ 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/token-exchange.sw.json
@@ -0,0 +1,117 @@
+{
+  "id": "token_exchange",
+  "version": "1.0",
+  "name": "Token exchange SW",
+  "description": "Executes different endpoint using different security scheme",
+  "start": "WithExchange",
+  "errors": [
+    {
+      "name": "execution_error",
+      "code": "jakarta.ws.rs.ProcessingException"
+    }
+  ],
+  "functions": [
+    {
+      "name": "executeQueryWithExchange",
+      "type": "rest",
+      "operation": "specs/token-exchange-external-service.yaml#withExchange"
+    },
+    {
+      "name": "executeQueryWithoutExchange",
+      "type": "rest",
+      "operation": "specs/token-exchange-external-service.yaml#withoutExchange"
+    },
+    {
+      "name": "executeQueryWithExchangeAndWithPropagation",
+      "type": "rest",
+      "operation": 
"specs/token-exchange-external-service.yaml#withExchangeAndPropagation"
+    }
+  ],
+  "states": [
+    {
+      "name": "WithExchange",
+      "type": "operation",
+      "actions": [
+        {
+          "name": "executeQueryWithExchangeAction",
+          "functionRef": {
+            "refName": "executeQueryWithExchange",
+            "arguments": {
+              "processInstanceId": "$WORKFLOW.instanceId",
+              "query": ".query"
+            }
+          }
+        }
+      ],
+      "transition": "WithoutExchange",
+      "onErrors": [
+        {
+          "errorRef": "execution_error",
+          "transition": "EndWithError"
+        }
+      ]
+    },
+    {
+      "name": "WithoutExchange",
+      "type": "operation",
+      "actions": [
+        {
+          "name": "executeQueryWithoutExchangeAction",
+          "functionRef": {
+            "refName": "executeQueryWithoutExchange",
+            "arguments": {
+              "processInstanceId": "$WORKFLOW.instanceId",
+              "query": ".query"
+            }
+          }
+        }
+      ],
+      "transition": "WithExchangeAndWithPropagation",
+      "onErrors": [
+        {
+          "errorRef": "execution_error",
+          "transition": "EndWithError"
+        }
+      ]
+    },
+    {
+      "name": "WithExchangeAndWithPropagation",
+      "type": "operation",
+      "actions": [
+        {
+          "name": "executeQueryWithExchangeAndWithPropagationAction",
+          "functionRef": {
+            "refName": "executeQueryWithExchangeAndWithPropagation",
+            "arguments": {
+              "processInstanceId": "$WORKFLOW.instanceId",
+              "query": ".query"
+            }
+          }
+        }
+      ],
+      "transition": "End",
+      "onErrors": [
+        {
+          "errorRef": "execution_error",
+          "transition": "EndWithError"
+        }
+      ]
+    },
+    {
+      "name": "EndWithError",
+      "type": "inject",
+      "data": {
+        "executionStatus": "Service execution failed"
+      },
+      "transition": "End"
+    },
+    {
+      "name": "End",
+      "type": "inject",
+      "data": {
+        "executionStatus": "Service execution successful"
+      },
+      "end": true
+    }
+  ]
+}
\ No newline at end of file
diff --git 
a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/KeycloakServiceMock.java
 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/KeycloakServiceMock.java
index 67d3d66bcc..1ad94011ee 100644
--- 
a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/KeycloakServiceMock.java
+++ 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/KeycloakServiceMock.java
@@ -28,6 +28,7 @@ import 
io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
 import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
 import static com.github.tomakehurst.wiremock.client.WireMock.configureFor;
 import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.matching;
 import static com.github.tomakehurst.wiremock.client.WireMock.post;
 import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
 import static 
com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
@@ -41,18 +42,27 @@ import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
  */
 public class KeycloakServiceMock implements 
QuarkusTestResourceLifecycleManager {
 
-    public static final String KEY_CLOAK_SERVICE_URL = 
"keycloak.mock.service.url";
-    public static final String KEY_CLOAK_SERVICE_TOKEN_PATH = 
"keycloak.mock.service.token-path";
+    public static final String KEYCLOAK_SERVICE_URL = 
"keycloak.mock.service.url";
+    public static final String KEYCLOAK_SERVICE_TOKEN_PATH = 
"keycloak.mock.service.token-path";
+
+    public static final String KEYCLOAK_EXCHANGE_SERVICE_TOKEN_PATH = 
"keycloak.mock.exchange-service.token-path";
     public static final String REALM = "kogito-tests";
-    public static final String KEY_CLOAK_SERVICE_TOKEN_PATH_VALUE = "/realms/" 
+ REALM + "/protocol/openid-connect/token";
+    public static final String EXCHANGE_REALM = "kogito-exchange-tests";
+    public static final String KEYCLOAK_SERVICE_TOKEN_PATH_VALUE = "/realms/" 
+ REALM + "/protocol/openid-connect/token";
+    public static final String KEYCLOAK_EXCHANGE_SERVICE_TOKEN_PATH_VALUE = 
"/realms/" + EXCHANGE_REALM + "/protocol/openid-connect/token";
     public static final String CLIENT_ID = "kogito-app";
     public static final String SECRET = "secret";
     public static final String KEYCLOAK_ACCESS_TOKEN = "KEYCLOAK_ACCESS_TOKEN";
+
+    public static final String KEYCLOAK_EXCHANGED_ACCESS_TOKEN = 
"KEYCLOAK_EXCHANGED_ACCESS_TOKEN";
+
     public static final String KEYCLOAK_REFRESH_TOKEN = 
"KEYCLOAK_REFRESH_TOKEN";
     public static final String KEYCLOAK_SESSION_STATE = 
"KEYCLOAK_SESSION_STATE";
 
     public static final String AUTH_REQUEST_BODY = 
"grant_type=client_credentials";
 
+    public static final String EXCHANGE_AUTH_REQUEST_BODY = 
"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange.*";
+
     private WireMockServer wireMockServer;
 
     @Override
@@ -61,7 +71,7 @@ public class KeycloakServiceMock implements 
QuarkusTestResourceLifecycleManager
         wireMockServer.start();
         configureFor(wireMockServer.port());
 
-        stubFor(post(KEY_CLOAK_SERVICE_TOKEN_PATH_VALUE)
+        stubFor(post(KEYCLOAK_SERVICE_TOKEN_PATH_VALUE)
                 .withHeader(CONTENT_TYPE, equalTo(APPLICATION_FORM_URLENCODED))
                 .withBasicAuth(CLIENT_ID, SECRET)
                 .withRequestBody(equalTo(AUTH_REQUEST_BODY))
@@ -69,23 +79,49 @@ public class KeycloakServiceMock implements 
QuarkusTestResourceLifecycleManager
                         .withHeader(CONTENT_TYPE, APPLICATION_JSON)
                         .withBody(getTokenResult())));
 
+        stubFor(post(KEYCLOAK_EXCHANGE_SERVICE_TOKEN_PATH_VALUE)
+                .withHeader(CONTENT_TYPE, equalTo(APPLICATION_FORM_URLENCODED))
+                .withBasicAuth(CLIENT_ID, SECRET)
+                .withRequestBody(matching(EXCHANGE_AUTH_REQUEST_BODY))
+                .willReturn(aResponse()
+                        .withHeader(CONTENT_TYPE, APPLICATION_JSON)
+                        .withBody(exchangeTokenResult())));
+
         Map<String, String> properties = new HashMap<>();
-        properties.put(KEY_CLOAK_SERVICE_URL, wireMockServer.baseUrl());
-        properties.put(KEY_CLOAK_SERVICE_TOKEN_PATH, 
KEY_CLOAK_SERVICE_TOKEN_PATH_VALUE);
+        properties.put(KEYCLOAK_SERVICE_URL, wireMockServer.baseUrl());
+        properties.put(KEYCLOAK_SERVICE_TOKEN_PATH, 
KEYCLOAK_SERVICE_TOKEN_PATH_VALUE);
+        properties.put(KEYCLOAK_EXCHANGE_SERVICE_TOKEN_PATH, 
KEYCLOAK_EXCHANGE_SERVICE_TOKEN_PATH_VALUE);
         return properties;
     }
 
     private static String getTokenResult() {
-        return "{\n" +
-                "    \"access_token\": \"" + KEYCLOAK_ACCESS_TOKEN + "\",\n" +
-                "    \"expires_in\": 300,\n" +
-                "    \"refresh_expires_in\": 1800,\n" +
-                "    \"refresh_token\": \"" + KEYCLOAK_REFRESH_TOKEN + "\",\n" 
+
-                "    \"token_type\": \"bearer\",\n" +
-                "    \"not-before-policy\": 0,\n" +
-                "    \"session_state\": \"" + KEYCLOAK_SESSION_STATE + "\",\n" 
+
-                "    \"scope\": \"email profile\"\n" +
-                "}";
+        return """
+                {
+                    "access_token": "%s",
+                    "expires_in": 300,
+                    "refresh_expires_in": 1800,
+                    "refresh_token": "%s",
+                    "token_type": "bearer",
+                    "not-before-policy": 0,
+                    "session_state": "%s",
+                    "scope": "email profile"
+                }
+                """.formatted(KEYCLOAK_ACCESS_TOKEN, KEYCLOAK_REFRESH_TOKEN, 
KEYCLOAK_SESSION_STATE);
+    }
+
+    private static String exchangeTokenResult() {
+        return """
+                {
+                    "access_token": "%s",
+                    "expires_in": 300,
+                    "refresh_expires_in": 1800,
+                    "refresh_token": "%s",
+                    "token_type": "bearer",
+                    "not-before-policy": 0,
+                    "session_state": "%s",
+                    "scope": "email profile"
+                }
+                """.formatted(KEYCLOAK_EXCHANGED_ACCESS_TOKEN, 
KEYCLOAK_REFRESH_TOKEN, KEYCLOAK_SESSION_STATE);
     }
 
     @Override
diff --git 
a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/TokenExchangeExternalServicesMock.java
 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/TokenExchangeExternalServicesMock.java
new file mode 100644
index 0000000000..db623cdfc2
--- /dev/null
+++ 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/TokenExchangeExternalServicesMock.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.kie.kogito.quarkus.workflows;
+
+import java.util.Collections;
+import java.util.Map;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+
+import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
+
+import jakarta.ws.rs.core.HttpHeaders;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.configureFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static 
com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
+import static jakarta.ws.rs.core.HttpHeaders.CONTENT_TYPE;
+import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
+import static 
org.kie.kogito.quarkus.workflows.KeycloakServiceMock.KEYCLOAK_ACCESS_TOKEN;
+import static 
org.kie.kogito.quarkus.workflows.KeycloakServiceMock.KEYCLOAK_EXCHANGED_ACCESS_TOKEN;
+
+public class TokenExchangeExternalServicesMock implements 
QuarkusTestResourceLifecycleManager {
+
+    public static final String BASE_AND_PROPAGATED_AUTHORIZATION_TOKEN = 
"BASE_AND_PROPAGATED_AUTHORIZATION_TOKEN";
+
+    private static final String BEARER = "Bearer ";
+
+    public static final String TOKEN_PROPAGATION_EXTERNAL_SERVICE_MOCK_URL = 
"exchange-external-service-mock.url";
+
+    private WireMockServer wireMockServer;
+
+    @Override
+    public Map<String, String> start() {
+        wireMockServer = new WireMockServer(options().dynamicPort());
+        wireMockServer.start();
+        configureFor(wireMockServer.port());
+
+        // stub the /token-exchange-external-service/withExchange invocation 
with the expected token
+        
stubForExternalService("/token-exchange-external-service/withExchange", 
KEYCLOAK_EXCHANGED_ACCESS_TOKEN);
+
+        // stub the 
/token-exchange-external-service/withExchangeAndPropagation invocation with the 
expected token
+        
stubForExternalService("/token-exchange-external-service/withExchangeAndPropagation",
 BASE_AND_PROPAGATED_AUTHORIZATION_TOKEN);
+
+        // stub token-exchange-external-service/withoutExchange invocation 
with the expected token, no propagation nor
+        //  exchange are produced in this case but the service must receive 
the token provided by Keycloak since it has
+        // oauth2 security configured.
+        
stubForExternalService("/token-exchange-external-service/withoutExchange", 
KEYCLOAK_ACCESS_TOKEN);
+
+        return 
Collections.singletonMap(TOKEN_PROPAGATION_EXTERNAL_SERVICE_MOCK_URL, 
wireMockServer.baseUrl());
+    }
+
+    private static void stubForExternalService(String 
tokenPropagationExternalServiceUrl, String authorizationToken) {
+        stubFor(post(tokenPropagationExternalServiceUrl)
+                .withHeader(CONTENT_TYPE, equalTo(APPLICATION_JSON))
+                .withHeader(HttpHeaders.AUTHORIZATION, equalTo(BEARER + 
authorizationToken))
+                .willReturn(aResponse()
+                        .withHeader(CONTENT_TYPE, APPLICATION_JSON)
+                        .withBody("{}")));
+    }
+
+    @Override
+    public void stop() {
+        if (wireMockServer != null) {
+            wireMockServer.stop();
+        }
+    }
+
+}
diff --git 
a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/TokenExchangeIT.java
 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/TokenExchangeIT.java
new file mode 100644
index 0000000000..9dcf9947fd
--- /dev/null
+++ 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/TokenExchangeIT.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.kie.kogito.quarkus.workflows;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+import io.restassured.path.json.JsonPath;
+
+import jakarta.ws.rs.core.HttpHeaders;
+
+import static 
org.kie.kogito.quarkus.workflows.ExternalServiceMock.SUCCESSFUL_QUERY;
+import static 
org.kie.kogito.quarkus.workflows.TokenExchangeExternalServicesMock.BASE_AND_PROPAGATED_AUTHORIZATION_TOKEN;
+import static 
org.kie.kogito.test.utils.ProcessInstancesRESTTestUtils.newProcessInstance;
+
+@QuarkusTestResource(TokenExchangeExternalServicesMock.class)
+@QuarkusTestResource(KeycloakServiceMock.class)
+@QuarkusIntegrationTest
+class TokenExchangeIT {
+
+    @Test
+    void tokenExchange() {
+        // start a new process instance by sending the post query and collect 
the process instance id.
+        String processInput = buildProcessInput(SUCCESSFUL_QUERY);
+        Map<String, String> headers = new HashMap<>();
+        // prepare the headers to pass to the token_propagation SW.
+        // service token-propagation-external-service1 and 
token-propagation-external-service2 will receive the AUTHORIZATION_TOKEN 
+        headers.put(HttpHeaders.AUTHORIZATION, 
BASE_AND_PROPAGATED_AUTHORIZATION_TOKEN);
+
+        JsonPath jsonPath = newProcessInstance("/token_exchange", 
processInput, headers);
+        Assertions.assertThat(jsonPath.getString("id")).isNotBlank();
+    }
+
+    protected static String buildProcessInput(String query) {
+        return "{\"workflowdata\": {\"query\": \"" + query + "\"} }";
+    }
+}
diff --git 
a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow/pom.xml
 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow/pom.xml
index 6a2a4049ed..4dc732c701 100644
--- 
a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow/pom.xml
+++ 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow/pom.xml
@@ -128,6 +128,15 @@
       <groupId>io.quarkiverse.asyncapi</groupId>
       <artifactId>quarkus-asyncapi</artifactId>
     </dependency>
+    <dependency>
+        <groupId>io.quarkus</groupId>
+        <artifactId>quarkus-oidc-client</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkiverse.openapi.generator</groupId>
+      <artifactId>quarkus-openapi-generator</artifactId>
+    </dependency>
   </dependencies>
 
   <build>
diff --git 
a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow/src/main/java/org/kie/kogito/serverless/workflow/openapi/OpenApiCustomCredentialProvider.java
 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow/src/main/java/org/kie/kogito/serverless/workflow/openapi/OpenApiCustomCredentialProvider.java
new file mode 100644
index 0000000000..3991df9843
--- /dev/null
+++ 
b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow/src/main/java/org/kie/kogito/serverless/workflow/openapi/OpenApiCustomCredentialProvider.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.kie.kogito.serverless.workflow.openapi;
+
+import java.util.Collections;
+import java.util.Optional;
+
+import org.eclipse.microprofile.config.ConfigProvider;
+import org.kie.kogito.internal.utils.ConversionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.quarkiverse.openapi.generator.providers.ConfigCredentialsProvider;
+import io.quarkiverse.openapi.generator.providers.CredentialsContext;
+import io.quarkus.arc.Arc;
+import io.quarkus.oidc.client.OidcClient;
+import io.quarkus.oidc.client.OidcClientConfig;
+import io.quarkus.oidc.client.OidcClientException;
+import io.quarkus.oidc.client.OidcClients;
+import io.quarkus.oidc.client.Tokens;
+import io.quarkus.runtime.configuration.ConfigurationException;
+
+import jakarta.annotation.Priority;
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.enterprise.inject.Alternative;
+import jakarta.enterprise.inject.Specializes;
+import jakarta.ws.rs.core.HttpHeaders;
+
+import static 
io.quarkiverse.openapi.generator.providers.AbstractAuthProvider.getHeaderName;
+
+@RequestScoped
+@Alternative
+@Specializes
+@Priority(200)
+public class OpenApiCustomCredentialProvider extends ConfigCredentialsProvider 
{
+    private static final String CANONICAL_EXCHANGE_TOKEN_PROPERTY_NAME = 
"sonataflow.security.auth.%s.exchange-token";
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(OpenApiCustomCredentialProvider.class);
+
+    @Override
+    public Optional<String> getOauth2BearerToken(CredentialsContext input) {
+        LOGGER.debug("Calling 
OpenApiCustomCredentialProvider.getOauth2BearerToken for {}", 
input.getAuthName());
+        String authorizationHeaderName = 
Optional.ofNullable(getHeaderName(input.getOpenApiSpecId(), 
input.getAuthName())).orElse(HttpHeaders.AUTHORIZATION);
+        boolean exchangeToken = 
ConfigProvider.getConfig().getOptionalValue(getCanonicalExchangeTokenConfigPropertyName(input.getAuthName()),
 Boolean.class).orElse(false);
+        if (exchangeToken) {
+            String accessToken = 
input.getRequestContext().getHeaderString(authorizationHeaderName);
+
+            if (ConversionUtils.isEmpty(accessToken)) {
+                throw new ConfigurationException("An access token is required 
in the header %s (default is %s) but none was 
provided".formatted(authorizationHeaderName, HttpHeaders.AUTHORIZATION));
+            }
+
+            LOGGER.info("Oauth2 token exchange enabled for {}, will generate a 
tokens...", input.getAuthName());
+            OidcClients clients = 
Arc.container().instance(OidcClients.class).get();
+            if (clients == null) {
+                throw new ConfigurationException("No OIDC client was found. 
Hint: make sure the dependency io.quarkus:quarkus-oidc-client is provided 
and/or configure it in the properties.");
+            }
+
+            OidcClient exchangeTokenClient = 
clients.getClient(input.getAuthName());
+            if (exchangeTokenClient == null) {
+                throw new ConfigurationException("No OIDC client was found for 
%s. Hint: configure it in the properties.".formatted(input.getAuthName()));
+            }
+            return Optional.of(exchangeTokenIfNeeded(accessToken, 
exchangeTokenClient, input.getAuthName()));
+        }
+        return Optional.empty();
+    }
+
+    private String exchangeTokenIfNeeded(String token, OidcClient 
exchangeTokenClient, String authName) {
+        OidcClientConfig.Grant.Type exchangeTokenGrantType = 
ConfigProvider.getConfig().getValue("quarkus.oidc-client.%s.grant.type".formatted(authName),
 OidcClientConfig.Grant.Type.class);
+        try {
+            Tokens tokens = 
exchangeTokenClient.getTokens(Collections.singletonMap(getExchangeTokenProperty(exchangeTokenGrantType),
 token)).await().indefinitely();
+
+            //TODO store the refresh token in an expiring cache
+            //TODO store the access token in an expiring cache
+            //TODO cache should expire before access/refresh token expire so 
they can be refreshed before (need to decode the JWT claim)
+            return tokens.getAccessToken();
+        } catch (OidcClientException e) {
+            // TODO try to refresh the access token with the cached refresh 
token
+            LOGGER.error("Error while exchanging oauth2 token. The provided 
input token will be used without exchange.", e);
+        }
+
+        return token;
+    }
+
+    private static String getExchangeTokenProperty(OidcClientConfig.Grant.Type 
exchangeTokenGrantType) {
+        switch (exchangeTokenGrantType) {
+            case EXCHANGE:
+                return "subject_token";
+            case JWT:
+                return "assertion";
+        }
+        throw new ConfigurationException("Token exchange is required but OIDC 
client is configured to use the %s 
grantType".formatted(exchangeTokenGrantType.getGrantType()));
+
+    }
+
+    public static String getCanonicalExchangeTokenConfigPropertyName(String 
authName) {
+        return String.format(CANONICAL_EXCHANGE_TOKEN_PROPERTY_NAME, authName);
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to