Orchestration of third-party services using OAuth 2.0 authentication in SonataFlow

This document describes the example of how you can implement and configure a workflow that orchestrates the interaction with an OAuth 2.0 secured REST service.

For more information about orchestrating and configuring OpenAPI services, see Orchestrating the OpenAPI services, Configuring the OpenAPI services endpoints, and Authentication for OpenAPI services in SonataFlow.

Example of OAuth 2.0 orchestration in a workflow

To understand the example of OAuth 2.0 orchestration in a workflow, you can use the serverless-workflow-oauth2-orchestration-quarkus example application. This example application implements a workflow related to currency exchange calculations, which orchestrates an OAuth 2.0 secured REST service provided by Acme Financial Services.

Suppose you have a set of applications that must resolve the currency exchange calculations as their regular operations, and to resolve the currency exchange calculations, you need to get the accurate exchange rates. For this purpose, you can use the Acme Financial Services.

When you use the Acme Financial Services, you can query the exchange rates using their OAuth 2.0 secured services, which you can access using the granted credentials. However, you do not want to expose the services provided by Acme to the applications. In this case, you can implement a workflow that resolves the following aspects:

  • Orchestration with services provided by Acme and currency exchange calculations.

  • Authentication requirements to access the services provided by Acme.

  • Potential vendor lock-in problems, in case you want to change the provider in future.

  • Domain-specific validations and optimizations.

The further sections describes how an end-to-end solution is created in the serverless-workflow-oauth2-orchestration-quarkus example application. To see the source code of serverless-workflow-oauth2-orchestration-quarkus example application, you can clone the kogito-examples repository in GitHub and select the serverless-workflow-examples/serverless-workflow-oauth2-orchestration-quarkus directory.

The serverless-workflow-oauth2-orchestration-quarkus example application contains the following services to compose the solution:

  • currency-exchange-workflow: Workflow that implements the currency exchange calculations.

  • acme-financial-service: REST service that provides the exchange rates.

  • acme-oauth2-server: Keycloak server that secures the REST services from Acme Financial Services.

The following figure describes the architecture of the solution in serverless-workflow-oauth2-orchestration-quarkus example application:

Architecture diagram
Figure 1. Example architecture diagram of the solution
  1. The application creates a workflow instance to calculate the currency exchange.

  2. The workflow executes an OpenAPI operation to get the exchange rates information.

  3. Authorizations are produced to validate the access.

  4. The workflow receives the exchange rates information and executes the calculations.

  5. The execution of workflow is finalized by sending the result to the application.

The workflow automatically manages the interactions with the OAuth 2.0 server. Also, you must configure a Quarkus OpenId Connect Client (OIDC). For more information, see Configuration in serverless-workflow-oauth2-orchestration-quarkus example application.

currency-exchange-workflow

The currency-exchange-workflow in serverless-workflow-oauth2-orchestration-quarkus example application is a workflow that implements the currency exchange calculations.

The following figure describes the workflow in currency-exchange-workflow:

currency exchange workflow diagram
Figure 2. Example currency-exchange-workflow
  1. First, validate the input data.

  2. Check the validation results:

    1. If validation is successful, then transition to (3).

    2. If validation is unsuccessful, then no transition and finalize the workflow with the error execution status.

  3. Send REST request to acme-financial-service to retrieve the exchange rates:

    1. If the request is successful, then transition to (4).

    2. If the request is unsuccessful, then transition to (6).

  4. Calculate the currency exchange and transition to (5).

  5. Set the successful execution status and finalize the currency-exchange-workflow execution.

  6. Set the error execution status and finalize the currency-exchange-workflow execution.

The following currency-exchange-workflow.sw.json file shows the specification of the currency-exchange-workflow:

currency-exchange-workflow.sw.json file
{
  "id": "currency_exchange_workflow",
  "version": "1.0",
  "name": "Currency Exchange SW",
  "dataInputSchema": "currency-exchange-workflow-schema.json",
  "start": "ValidateInputs",
  "functions": [
    {
      "name": "validateInputs",
      "type": "custom",
      "operation": "service:org.kie.kogito.examples.ExchangeWorkflowHelper::validateInputs"
    },
    {
      "name": "getExchangeRate",
      "type": "rest",
      "operation": "specs/acme-financial-service.yml#exchangeRate"
    },
    {
      "name": "calculateExchange",
      "type": "expression",
      "operation": "${ { calculateExchangeResult: .amount * .exchangeRate } }"
    }
  ],
  "errors": [
    {
      "name": "service_error",
      "code": "java.lang.Exception"
    }
  ],
  "states": [
    {
      "name": "ValidateInputs", (1)
      "type": "operation",
      "actions": [
        {
          "name": "validateInputsAction",
          "functionRef": {
            "refName": "validateInputs",
            "arguments": {
              "currencyFrom": "${ .currencyFrom }",
              "currencyTo": "${ .currencyTo }",
              "amount": "${ .amount }",
              "exchangeDate": "${ .exchangeDate }"
            }
          }
        }
      ],
      "transition": "CheckValidation"
    },
    {
      "name": "CheckValidation", (2)
      "type": "switch",
      "dataConditions": [
        {
          "condition": "${ .executionStatus == \"ERROR\" }",
          "end": true
        }
      ],
      "defaultCondition": {
        "transition": "GetExchangeRate"
      }
    },
    {
      "name": "GetExchangeRate", (3)
      "type": "operation",
      "actions": [
        {
          "name": "getExchangeRateAction",
          "functionRef": {
            "refName": "getExchangeRate",
            "arguments": {
              "currencyFrom": "${ .currencyFrom }",
              "currencyTo": "${ .currencyTo }",
              "exchangeDate": "${ .exchangeDate }"
            }
          },
          "actionDataFilter": {
            "results": "${ {exchangeRate: .rate} }"
          }
        }
      ],
      "transition": "CalculateExchange",
      "onErrors": [
        {
          "errorRef": "service_error",
          "transition": "EndWithError"
        }
      ]
    },
    {
      "name": "CalculateExchange", (4)
      "type": "operation",
      "actions": [
        {
          "name": "calculateExchangeAction",
          "functionRef": {
            "refName": "calculateExchange"
          },
          "actionDataFilter": {
            "results": "${ {result: .calculateExchangeResult} }"
          }
        }
      ],
      "transition": "EndSuccessful"
    },
    {
      "name": "EndWithError", (5)
      "type": "inject",
      "data": {
        "executionStatus": "ERROR",
        "executionStatusMessage": "Execution failed: The acme-financial-service invocation has failed, check that the service is running and that you have configured the OAuth2 client properly"
      },
      "end": true
    },
    {
      "name": "EndSuccessful", (6)
      "type": "inject",
      "data": {
        "executionStatus": "OK",
        "executionStatusMessage": "Execution successful"
      },
      "end": true
    }
  ]
}
1 ValidateInputs state executes the validateInputs function to validate the input data.
2 CheckValidation state determines the next state to go by evaluating the validation results.
3 GetExchangeRate state executes the getExchangeRate function to retrieve the exchange rate from the remote server.
4 CalculateExchange state executes the calculateExchange function to calculate the currency exchange.
5 EndWithError state finalizes the workflow with an ERROR.
6 EndSuccessful state finalizes the workflow with successful OK status.

The validateInputs function is used to execute the custom Java processing as part of the workflow. For more information about custom functions, see Serverless Workflow specification.

The following is an example of validateInputs function definition:

Example validateInputs function definition
{
  "name": "validateInputs", (1)
  "type": "custom", (2)
  "operation": "service:org.kie.kogito.examples.ExchangeWorkflowHelper::validateInputs" (3)
}
1 validateInputs function declaration.
2 custom type that enables you to use your own Java class to implement a function.
3 Specifies that the function is implemented by the method validateIntpus in the org.kie.kogito.examples.ExchangeWorkflowHelper Java class.

To implement a custom function, you must create a Java class such as ExchangeWorkflowHelper in your project:

Example ExchangeWorkflowHelper.java file
package org.kie.kogito.examples;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class ExchangeWorkflowHelper {

    public ValidationResult validateInputs(String currencyFrom,
                                           String currencyTo,
                                           double amount,
                                           String exchangeDate) {
        // Implement your custom Java processing here and return
        // a Java POJO to the Serverless Workflow.
        if (!good) {
            return new ValidationResult("ERROR", "Not good!");
        }
        return new ValidationResult();
    }

    public static class ValidationResult {
        private String executionStatus;
        private String executionStatusMessage;
        // getters, setters, etc.
    }
}

To access the acme-financial-service REST service in currency-exchange-workflow, a workflow function such as getExchangeRate is used. For more information about using functions for REST services, see Serverless Workflow specification.

Following is the function definition of getExchangeRate:

Example getExchangeRate function definition
{
  "name": "getExchangeRate", (1)
  "type": "rest",
  "operation": "specs/acme-financial-service.yml#exchangeRate" (2)
}
1 getExchangeRate function declaration.
2 Specifies that the function is implemented by the exchangeRate operation in the acme-financial-service.yml file.

For the previous configuration, the acme-financial-service.yml file must be located in the src/main/resources/specs directory of the project.

In order to filter the information, which must be returned to the currency-exchange-workflow, an actionDataFilter is used:

Example actionDataFilter to pass the getExchangeRate results
  "actionDataFilter": {
    "results": "${ {exchangeRate: .rate} }" (1)
  }
1 Merge the value of the rate property to the exchangeRate workflow data property. The value of the rate property is retrieved from the acme-financial-service invocation result.

For more information about action data filters, see Action data filters in Serverless Workflow specification.

To calculate the currency exchange rates in currency-exchange-workflow, a function named calculateExchange is used:

Example calculateExchange function definition
{
  "name": "calculateExchange", (1)
  "type": "expression", (2)
  "operation": "${ { calculateExchangeResult: .amount * .exchangeRate } }" (3)
}
1 calculateExchange function declaration.
2 expression type that enables you to use an expression to implement a function.
3 Specifies that the function returns a JSON object with a calculateExchangeResult property, containing the calculation.

For more information about using functions for expression evaluation, see Serverless Workflow specification.

Similar to getExchangeRate to filter the information, which must be returned to the currency-exchange-workflow, an actionDataFilter is used:

Example actionDataFilter to pass the calculateExchange results:
  "actionDataFilter": {
    "results": "${ {result: .calculateExchangeResult} }" (1)
  }
1 Merge the value of the calculateExchangeResult property to the result workflow data property. The value of the calculateExchangeResult property is retrieved from the expression result.
acme-financial-service

The acme-financial-service in serverless-workflow-oauth2-orchestration-quarkus example application is a REST service that provides the exchange rates. Following is the OpenAPI specification that defines the acme-financial-service:

Example acme-financial-service.yml OpenAPI specification
---
openapi: 3.0.3
info:
  title: Acme Financial Service API
  version: 1.0.1
paths:
  /financial-service/exchange-rate: (1)
    get:
      tags:
        - Acme Financial Resource
      operationId: exchangeRate
      parameters: (2)
        - name: currencyFrom
          in: query
          schema:
            type: string
        - name: currencyTo
          in: query
          schema:
            type: string
        - name: exchangeDate
          in: query
          schema:
            type: string
      responses: (3)
        "200":
          description: OK
          content: (4)
            application/json:
              schema:
                $ref: '#/components/schemas/ExchangeRateResult'
      security:
        - acme-financial-oauth: [ ] (5)
components:
  schemas:
    ExchangeRateResult: (6)
      type: object
      properties:
        rate:
          format: double
          type: number
  securitySchemes:
    acme-financial-oauth: (7)
      type: oauth2 (8)
      flows:
        clientCredentials: (9)
          authorizationUrl: http://localhost:8281/auth/realms/kogito/protocol/openid-connect/auth
          tokenUrl: http://localhost:8281/auth/realms/kogito/protocol/openid-connect/token
          scopes: { }
1 REST path to access the exchangeRate operation in the remote server.
2 Parameter of the exchangeRate operation.
3 Responses of the exchangeRate operation.
4 Response type and data exchange format.
5 Specifies that the exchangeRate operation is secured using the acme-financial-oauth security scheme.
6 Response type specification.
7 Specification of the acme-financial-oauth security scheme.
8 Security scheme type. The security scheme type indicates that you must configure a Quarkus OpenId Connect Client (OIDC) using acme_financial_oauth name to execute the operation.
9 Authentication flow and related information.

For more information about the acme-financial-service implementation, see acme-financial-service in serverless-workflow-oauth2-orchestration-quarkus example application.

Configuration in serverless-workflow-oauth2-orchestration-quarkus example application

The configuration for the workflow used in serverless-workflow-oauth2-orchestration-quarkus example application includes defining required properties in application.properties file as shown in the following example:

Example application.properties file configuration
quarkus.openapi-generator.codegen.spec.acme_financial_service_yml.base-package=com.acme (1)

quarkus.rest-client.acme_financial_service_yml.url=http://localhost:8483 (2)

quarkus.oidc-client.acme_financial_oauth.discovery-enabled=false (3)
quarkus.oidc-client.acme_financial_oauth.auth-server-url=http://localhost:8281/auth/realms/kogito/protocol/openid-connect/auth (4)
quarkus.oidc-client.acme_financial_oauth.token-path=http://localhost:8281/auth/realms/kogito/protocol/openid-connect/token (5)
quarkus.oidc-client.acme_financial_oauth.client-id=kogito-app (6)
quarkus.oidc-client.acme_financial_oauth.grant.type=client
quarkus.oidc-client.acme_financial_oauth.credentials.client-secret.method=basic (7)
quarkus.oidc-client.acme_financial_oauth.credentials.client-secret.value=secret (8)
1 Package name for automatically generated classes that implement the access to all operations defined in the acme-financial-service.yml file.
2 Root URL to access all operations defined in the acme-financial-service.yml file. For the exchangeRate operation, a URL such as http://localhost:8483/financial-service/exchange-rate is automatically generated.
3 Disables the OAuth 2.0 server endpoints discovery as the endpoints provided in the acme-financial-service-yml file are used instead.
4 Authentication URL of the OAuth 2.0 server.
5 Relative path or URL of the OAuth 2.0 token endpoint, which allocates access and refreshes tokens.
6 Client ID to identify the workflow against the authorization service, such as kogito-app. This identifier must be provided by Acme.
7 Method that is used at the time of sending the client-secret for the authentications when the client grant type is used.
8 client-secret to authenticate the workflow against the authorization service when the client grant type is used. This secret must be provided by Acme.

To configure the Quarkus OpenId Connect Client (OIDC) for acme_financial_service_oauth service, you must follow the rules described in Example of OAuth 2.0 authentication.

Also, the particular attributes depend on the OAuth 2.0 server and authorization flow to use. You can get the information about the OAuth 2.0 server and authorization flow from the acme-financial-service.yml file.

Also, you can use the alternatives defined in the Quarkus configuration reference guide to configure the properties in application.properties file. You can define environment variables to set the authentication secrets, and you can use the Quarkus Credentials Provider framework.

Running the example application

Once you clone the serverless-workflow-oauth2-orchestration-quarkus example application from GitHub repository, you can run the example application.

Prerequisites
  • Java 11+ is installed.

  • Maven 3.8.6 or later is installed.

  • Docker 20.10.7 or later is installed.

  • (Optional) Docker compose 1.27.2 or later is installed.

Procedure
  1. In a command terminal, clone the kogito-examples repository and navigate to the cloned directory:

    Clone kogito-examples repository and navigate to the directory
    git clone https://github.com/kiegroup/kogito-examples.git
    
    cd kogito-examples/serverless-workflow-examples/serverless-workflow-oauth2-orchestration-quarkus
  2. Run the following command to build the example application using Apache Maven:

    Build the example application
    mvn clean install
  3. In a separate command terminal window, start the Keycloak server:

    Start the Keycloak server
    cd kogito-examples/serverless-workflow-examples/serverless-workflow-oauth2-orchestration-quarkus/scripts
    
    ./startKeycloak.sh

    Alternatively, you can start the Docker Compose using the following command:

    Start Docker Compose
    cd kogito-examples/serverless-workflow-examples/serverless-workflow-oauth2-orchestration-quarkus/docker-compose
    
    docker-compose up
  4. In a separate command terminal window, navigate to the acme-financial-service directory and start the Quarkus application of Acme Financial Service:

    Start Acme Financial Service
    cd kogito-examples/serverless-workflow-examples/serverless-workflow-oauth2-orchestration-quarkus/acme-financial-service
    
    java -jar target/quarkus-app/quarkus-run.jar
  5. In a separate command terminal window, navigate to the currency-exchange-workflow directory and start the Quarkus application of currency exchange workflow:

    Start currency exchange workflow
    cd kogito-examples/serverless-workflow-examples/serverless-workflow-oauth2-orchestration-quarkus/currency-exchange-workflow
    
    java -jar target/quarkus-app/quarkus-run.jar
  6. When all the services are running, use the following curl commands to run the currency-exchange-workflow:

    The following is an example of successful execution when calculating the currency exchange from EUR to USD:

    • Example request

    • Example response

    curl -X 'POST' \
      'http://localhost:8080/currency_exchange_workflow' \
      -H 'accept: */*' \
      -H 'Content-Type: application/json' \
      -d '{
           "currencyFrom": "EUR",
           "currencyTo": "USD",
           "exchangeDate": "2022-06-10",
           "amount": 2.0
        }'
    {
      "id": "399ce304-037c-486d-b4bf-1564baf907a1",
      "workflowdata": {
        "currencyFrom": "EUR",
        "currencyTo": "USD",
        "exchangeDate": "2022-06-10",
        "amount": 2.0,
        "executionStatus": "OK",
        "executionStatusMessage": "Execution successful",
        "exchangeRate": 1.0578,
        "result": 2.1156
      }
    }

    The following is an example of an unsupported currency error when calculating the currency exchange from EUR to MXN:

    • Example request

    • Example response

    curl -X 'POST' \
      'http://localhost:8080/currency_exchange_workflow' \
      -H 'accept: */*' \
      -H 'Content-Type: application/json' \
      -d '{
        "currencyFrom": "EUR",
        "currencyTo": "MXN",
        "exchangeDate": "2022-06-10",
        "amount": 2.0
    }'
    {
      "id": "e0e7708d-c82c-47d7-9354-09ccd1e972bb",
      "workflowdata": {
        "currencyFrom": "EUR",
        "currencyTo": "MXN",
        "exchangeDate": "2022-06-10",
        "amount": 2,
        "executionStatus": "ERROR",
        "executionStatusMessage": "Invalid currencyTo: MXN, only the following currencies are supported [EUR, USD, JPY, GBP, CAD, BRL, AUD]",
        "exchangeRate": null
      }
    }

    In the previous examples, the currencies supported by the currency-exchange-workflow include EUR, USD, JPY, GBP, CAD, BRL, and AUD. However, the acme-financial-service REST service can resolve any type of currency exchange. This is an example of a workflow implementing the intermediate data filtering, transforming, and validations.

    The following is an example of the occurrence of unexpected errors when accessing acme-financial-service:

    Before you launch the command, you must go to the terminal window where you started the acme-financial-service and stop the service using CTRL+C.

    • Example request

    • Example response

    curl -X 'POST' \
      'http://localhost:8080/currency_exchange_workflow' \
      -H 'accept: */*' \
      -H 'Content-Type: application/json' \
      -d '{
        "currencyFrom": "EUR",
        "currencyTo": "USD",
        "exchangeDate": "2022-06-10",
        "amount": 2.0
    }'
    {
      "id": "0044ffa0-7b2b-4fdc-af60-cd98c6bd3ade",
      "workflowdata": {
        "currencyFrom": "EUR",
        "currencyTo": "USD",
        "exchangeDate": "2022-06-10",
        "amount": 2.0,
        "executionStatus": "ERROR",
        "executionStatusMessage": "Execution failed: The acme-financial-service invocation has failed, check that the service is running and that you have configured the OAuth2 client properly",
        "exchangeRate": null
      }
    }

    In this example the error indicates that it was not possible to contact the acme-financial-service.

Additional resources

Found an issue?

If you find an issue or any misleading information, please feel free to report it here. We really appreciate it!