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]