Java Posts

How to Solve Keycloak Error, Uncaught server error: org.keycloak.authentication.AuthenticationFlowException

Had this error today when creating a custom authentication SPI for Keycloak

16:17:13,588 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (default task-1) Uncaught server error: 
org.keycloak.authentication.AuthenticationFlowException

        at org.keycloak.authentication.AuthenticationProcessor.authenticateOnly(AuthenticationProcessor.java:913)
        at org.keycloak.protocol.oidc.endpoints.TokenEndpoint.resourceOwnerPasswordCredentialsGrant(TokenEndpoint.java:554)
        at org.keycloak.protocol.oidc.endpoints.TokenEndpoint.processGrantRequest(TokenEndpoint.java:187)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:140)
        at org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:509)
        at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:399)
        at org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$0(ResourceMethodInvoker.java:363)
        at org.jboss.resteasy.core.interception.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:358)
        at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:365)
        at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:337)
        at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:137)
        at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:106)
        at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:132)
        at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:100)
        at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:443)
        at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:233)
        at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:139)
        at org.jboss.resteasy.core.interception.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:358)
        at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:142)
        at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:219)

This error happens because im not putting any userModel into Keycloak’s flowContext and adding a userModel solve this problem. Code can be seen here,

https://github.com/edwin/keycloak-password-encryptor/blob/master/src/main/java/com/edw/keycloak/spi/CustomKeycloakPasswordEncryptor.java#L93

How to Encrypt and Decrypt Password on Keycloak or Red Hat SSO

Previously i found one good question, "can we encrypt password at the user end before transmitting it to the server". I can see the purpose of this question is to prevent a plain text password being transmitted thru the network. And despite network is already on SSL, there are possibility that some SSL offloading or re-encryption happens along the way.

Okay so the concept is pretty much like this,

Solution is quite simple, an encryption is needed especially for sensitive data such as passwords in order to prevent those fields to be shown as a plain text.

For this sample, im trying to simulate a simple login by using Keycloak rest api and see whether we can encrypt some fields there. Lets start with a pom file,

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.edw</groupId>
    <artifactId>KeycloakPasswordEncryptor</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <keycloak.version>4.8.3.Final</keycloak.version>

        <version.maven-bundle-plugin>2.3.7</version.maven-bundle-plugin>
        <version.maven-compiler-plugin>3.5.1</version.maven-compiler-plugin>
        <version.maven-resources-plugin>3.0.1</version.maven-resources-plugin>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-core</artifactId>
            <version>${keycloak.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-server-spi</artifactId>
            <version>${keycloak.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-server-spi-private</artifactId>
            <version>${keycloak.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-services</artifactId>
            <version>${keycloak.version}</version>
            <scope>provided</scope>
        </dependency>

        <!--        unit testing -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.1.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>


    <build>
        <defaultGoal>install</defaultGoal>
        <plugins>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.1.1</version>

                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>

                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

Next is creating a java class to do all the authentication logic, we can put our password decryption method here

package com.edw.keycloak.spi;

import com.edw.keycloak.spi.helper.EncryptionHelper;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordUserCredentialModel;

import javax.ws.rs.core.Response;
import java.util.List;

public class CustomKeycloakPasswordEncryptor implements Authenticator {

    public void authenticate(AuthenticationFlowContext authenticationFlowContext) {

        // not bringing username
        if(authenticationFlowContext.getHttpRequest().getFormParameters().get("username") == null
                || authenticationFlowContext.getHttpRequest().getFormParameters().get("username").isEmpty()) {

            Response challenge =  Response.status(400)
                    .entity("{\"error\":\"invalid_request\",\"error_description\":\"No Username\"}")
                    .header("Content-Type", "application/json")
                    .build();
            authenticationFlowContext.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
            return;
        }

        // not bringing password
        if(authenticationFlowContext.getHttpRequest().getFormParameters().get("password") == null
                || authenticationFlowContext.getHttpRequest().getFormParameters().get("password").isEmpty()) {

            Response challenge =  Response.status(400)
                    .entity("{\"error\":\"invalid_request\",\"error_description\":\"No Password\"}")
                    .header("Content-Type", "application/json")
                    .build();
            authenticationFlowContext.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
            return;
        }

        // capture username
        String username = authenticationFlowContext.getHttpRequest().getFormParameters().getFirst("username").trim();

        // search for corresponding user
        List<UserModel> userModels = authenticationFlowContext.getSession().users().searchForUser(username, authenticationFlowContext.getRealm());

        // user not exists
        if(userModels.isEmpty()) {
            Response challenge =  Response.status(400)
                    .entity("{\"error\":\"invalid_request\",\"error_description\":\"User Not Found\"}")
                    .header("Content-Type", "application/json")
                    .build();
            authenticationFlowContext.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
            return;
        }

        // capture usermodel, means user is exist
        UserModel userModel = userModels.get(0);

        // capture password and dont forget to html-decode the content (im using a string replacement for this example)
        String password = authenticationFlowContext.getHttpRequest().getFormParameters().getFirst("password").trim();
        password = password.replace("%3D", "=");

        // decrypt the password
        password = EncryptionHelper.decrypt(password);

        // password is incorrect
        PasswordUserCredentialModel credentialInput = UserCredentialModel.password(password);
        boolean valid = authenticationFlowContext.getSession().userCredentialManager().isValid(authenticationFlowContext.getRealm(),
                                                                                                userModel,
                                                                                                new PasswordUserCredentialModel[]{credentialInput} );
        if( !valid ) {
            Response challenge =  Response.status(400)
                    .entity("{\"error\":\"invalid_request\",\"error_description\":\"User Not Found\"}")
                    .header("Content-Type", "application/json")
                    .build();
            authenticationFlowContext.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
            return;
        }

        // set user
        authenticationFlowContext.setUser(userModel);

        // all validation success
        authenticationFlowContext.success();
    }

    public void action(AuthenticationFlowContext authenticationFlowContext) {
        authenticationFlowContext.success();
    }

    public boolean requiresUser() {
        return false;
    }

    public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {
        return false;
    }

    public void setRequiredActions(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {

    }

    public void close() {

    }
}

Next is create a factory class for this,

package com.edw.keycloak.spi;

import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.ConfigurableAuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;

import java.util.ArrayList;
import java.util.List;

public class CustomKeycloakPasswordEncryptorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory {

    public static final String PROVIDER_ID = "password-encryption";

    private static final CustomKeycloakPasswordEncryptor SINGLETON = new CustomKeycloakPasswordEncryptor();

    public String getDisplayType() {
        return "Simple Password Encryption";
    }

    public String getReferenceCategory() {
        return "Simple Password Encryption";
    }

    public boolean isConfigurable() {
        return false;
    }

    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
        return CustomKeycloakPasswordEncryptorFactory.REQUIREMENT_CHOICES;
    }

    public boolean isUserSetupAllowed() {
        return false;
    }

    public String getHelpText() {
        return "Simple Password Encryption";
    }

    private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
            AuthenticationExecutionModel.Requirement.REQUIRED,
            AuthenticationExecutionModel.Requirement.ALTERNATIVE,
            AuthenticationExecutionModel.Requirement.DISABLED
    };

    public List<ProviderConfigProperty> getConfigProperties() {
        return new ArrayList<ProviderConfigProperty>();
    }

    public Authenticator create(KeycloakSession keycloakSession) {
        return SINGLETON;
    }

    public void init(Config.Scope scope) {

    }

    public void postInit(KeycloakSessionFactory keycloakSessionFactory) {

    }

    public void close() {

    }

    public String getId() {
        return CustomKeycloakPasswordEncryptorFactory.PROVIDER_ID;
    }

    public int order() {
        return 0;
    }
}

Next is creating a text file with the name of "org.keycloak.authentication.AuthenticatorFactory", and put it under META-INF/services folder,

com.edw.keycloak.spi.CustomKeycloakPasswordEncryptorFactory

Run below command to build,

mvn clean package

And put the jar build result into Keycloak installation. In my case, i put it on keycloak-4.8.3.Final\standalone\deployments.

So basically, all our code part has been build and deployed and now lets focus on the Keycloak’s side. First we can start by creating a new Authentication

Once created, next is creating a new execution

Select the name of our Custom SPI, in my case it would be Simple Password Encryption.

And make it as Required,

Next is creating a client,

And the most important thing is, setting in the Authentication Flow Overrides.

Dont forget to create a user for this, in here im setting “1” as the password for this user.

Okay, once everything is already setup properly. Next we can do testing by doing a simple CURL to Keycloak’s API endpoint.

First we try with an un-encrypted password,

curl -L -X POST 'http://localhost:8080/auth/realms/whatever-realm/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=clientid-02' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_secret=4ddddab7-884f-4bb3-a403-660cf89ff5f2' \
--data-urlencode 'scope=openid' \
--data-urlencode 'username=edw' \
--data-urlencode 'password=1'

It will give below error response, due to combination of username and password is not found,

{
    "error": "invalid_request",
    "error_description": "User Not Found"
}

Next lets try with below CURL call, see the encrypted password field

curl -L -X POST 'http://localhost:8080/auth/realms/whatever-realm/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=clientid-02' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_secret=4ddddab7-884f-4bb3-a403-660cf89ff5f2' \
--data-urlencode 'scope=openid' \
--data-urlencode 'username=edw' \
--data-urlencode 'password=AA=='

It will gives a success result and show generated JWT token,

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIg.......IWhs_qth4B3ITCAkubmq4me2ftj6Fa2uaQMwydXaEn0cA",
    "expires_in": 600,
    "refresh_expires_in": 1800,
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI......w1OiD1n1mObX7WCs",
    "token_type": "bearer",
    "id_token": "eyJhbGciOiJSUzI1NiI.........XqqH49DaBeI1pHCjSw",
    "not-before-policy": 1601650090,
    "session_state": "553cc783-xxxx-xxxx-xxxx-a6a6ab1b6e0e",
    "scope": "openid email profile"
}

It shows that we are able to connect to Keycloak by using an encrypted password, and Keycloak is able to decypt it.

Anyway, code for this can be found on below link.

https://github.com/edwin/keycloak-password-encryptor

Have fun using Keycloak

Using ClientInterceptor to Modify WSDL String Responses on Spring Boot

Usually we are connecting to a middleware product which are able to provide us a prefect and working webservice request and response. But there are rare cases where response are not perfect, therefore breaking the whole application functionality.

In order to prevent that, sometimes we need to replace and sanitize all the response coming from middleware. But how to do it in Spring Boot?

Actually Spring Boot have an interceptor class for “intercepting” all message that comes, it is called a ClientInterceptor class. We could create a new custom class extending on the ClientInterceptor class to handle all WSDL responses, and do all the modification needed.

So first lets create a simple WSDL for this example,

<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions name="Employee"
                  targetNamespace="http://bestpay.payroll/employee" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
                  xmlns:tns="http://bestpay.payroll/employee" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
                  xmlns:xsd="http://www.w3.org/2001/XMLSchema">

    <wsdl:types>
        <xsd:schema>
            <xsd:import namespace="http://bestpay.payroll/employee"
                        schemaLocation="employee.xsd" />
        </xsd:schema>
    </wsdl:types>

    <wsdl:message name="employeeLookupRequest">
        <wsdl:part element="tns:EmployeeIdList" name="employeeIdList" />
    </wsdl:message>

    <wsdl:message name="employeeLookupResponse">
        <wsdl:part element="tns:EmployeeInfoList" name="employeeInfoList" />
    </wsdl:message>

    <wsdl:portType name="employeeLookupService">
        <wsdl:operation name="employeeLookup">
            <wsdl:input message="tns:employeeLookupRequest" />
            <wsdl:output message="tns:employeeLookupResponse" />
        </wsdl:operation>
    </wsdl:portType>

    <wsdl:binding name="employeeLookupBinding" type="tns:employeeLookupService">
        <soap:binding style="document"
                      transport="http://schemas.xmlsoap.org/soap/http" />
        <wsdl:operation name="employeeLookup">
            <soap:operation soapAction="http://bestpay.payroll/employee/employeeLookup" />
            <wsdl:input>
                <soap:body use="literal" />
            </wsdl:input>
            <wsdl:output>
                <soap:body use="literal" />
            </wsdl:output>
        </wsdl:operation>
    </wsdl:binding>

    <wsdl:service name="employeeLookupService">
        <wsdl:port binding="tns:employeeLookupBinding" name="employeeLookupPort">
            <soap:address location="http://localhost" />
        </wsdl:port>
    </wsdl:service>

</wsdl:definitions>

Next is creating a java project by using maven pom.xml,

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.3</version>
        <relativePath/>
    </parent>

    <groupId>com.edw</groupId>
    <artifactId>WSDLReplaceString</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
        <spring-boot-bom.version>2.1.6.Final-redhat-00004</spring-boot-bom.version>
        <start-class>com.edw.Main</start-class>
    </properties>

    <dependencyManagement>
        <dependencies>

            <dependency>
                <groupId>me.snowdrop</groupId>
                <artifactId>spring-boot-bom</artifactId>
                <version>${spring-boot-bom.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>junit</groupId>
                    <artifactId>junit</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web-services</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.jvnet.jaxb2.maven2</groupId>
                <artifactId>maven-jaxb2-plugin</artifactId>
                <version>0.14.0</version>
                <executions>
                    <execution>
                        <id>HelloWorld</id>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <configuration>
                            <schemaDirectory>${project.basedir}/src/main/resources/wsdl/</schemaDirectory>
                            <schemaIncludes>
                                <schemaInclude>*.wsdl</schemaInclude>
                            </schemaIncludes>
                            <generateDirectory>
                                ${project.build.directory}/generated-sources
                            </generateDirectory>
                            <generatePackage>com.edw.wsdl.bean.hello</generatePackage>
                            <extension>true</extension>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Based on above pom files, we could see that Im using JAXB2 maven plugin to convert WSDL file into Java Object, and using Spring WebService for handling all WS request and response.

Only four java classes involved here, and let me start with the first one. It’s going to be our main class.

package com.edw;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class);
    }
}

And a controller class,

package com.edw.controller;

import com.edw.service.ExternalRequestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class MainController {

    @Autowired
    private ExternalRequestService externalRequestService;

    @GetMapping("/")
    public Map index() {
        return new HashMap(){{
            put("response", externalRequestService.getName());
        }};
    }
}

A service class, this is where i put my WSDL endpoint. Basically it would return the employee’s firstname field from WSDL response,

package com.edw.service;

import com.edw.wsdl.bean.hello.EmployeeIdWrapper;
import com.edw.wsdl.bean.hello.EmployeeInfoWrapper;
import com.edw.wsdl.bean.hello.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.ws.client.core.WebServiceTemplate;
import org.springframework.ws.client.core.support.WebServiceGatewaySupport;

import javax.xml.bind.JAXBElement;

@Service
public class ExternalRequestService extends WebServiceGatewaySupport {

    @Autowired
    private WebServiceTemplate webServiceTemplate;

    public String getName() {

        EmployeeIdWrapper employeeIdWrapper = new EmployeeIdWrapper();
        employeeIdWrapper.getEid().add("001002");

        JAXBElement<EmployeeInfoWrapper> employeeInfoJaxB = (JAXBElement<EmployeeInfoWrapper>) webServiceTemplate
                .marshalSendAndReceive("http://localhost:8082",
                        new ObjectFactory().createEmployeeIdList(employeeIdWrapper));

        return employeeInfoJaxB.getValue()
                .getEmployeeInfo()
                .get(0)
                .getFirstName();
    }
}

But all of those code wont be running without a configuration class,

package com.edw.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.ws.client.core.WebServiceTemplate;
import org.springframework.ws.client.support.interceptor.ClientInterceptor;

@Configuration
public class SOAPConfig {
    @Bean
    public Jaxb2Marshaller marshaller() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setPackagesToScan("com.edw.wsdl.bean.hello");
        return marshaller;
    }

    @Bean
    public WebServiceTemplate webServiceTemplate() {
        WebServiceTemplate webServiceTemplate = new WebServiceTemplate();
        webServiceTemplate.setMarshaller(marshaller());
        webServiceTemplate.setUnmarshaller(marshaller());

        return webServiceTemplate;
    }
}

Im using SoapUI for simulating a middleware product which provide wsdl response,

And try do some rest api call with curl

$ curl -kv http://localhost:8081/
*   Trying ::1:8081...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET / HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.65.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Connection: keep-alive
< Transfer-Encoding: chunked
< Content-Type: application/json
< Date: Wed, 03 Mar 2021 09:53:51 GMT
<
* Connection #0 to host localhost left intact
{"response":"FOO"}                    

As we can see, the response is FOO. Now here comes the scenario where i need to change on-the-fly the response into something else, for this example it would be BAR.

First we need to create a java class to handle all the interception,

package com.edw.config;

import org.springframework.ws.client.WebServiceClientException;
import org.springframework.ws.client.support.interceptor.ClientInterceptor;
import org.springframework.ws.context.MessageContext;
import org.springframework.ws.soap.SoapVersion;
import org.springframework.ws.soap.saaj.SaajSoapMessage;
import org.springframework.ws.soap.saaj.SaajSoapMessageFactory;

import javax.xml.soap.MessageFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

public class SOAPClientInterceptor implements ClientInterceptor {

    @Override
    public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException {
        return true;
    }

    @Override
    public boolean handleResponse(MessageContext messageContext) throws WebServiceClientException {
        return true;
    }

    @Override
    public boolean handleFault(MessageContext messageContext) throws WebServiceClientException {
        return false;
    }

    @Override
    public void afterCompletion(MessageContext messageContext, Exception e) throws WebServiceClientException {
        OutputStream s = null;
        InputStream is1 = null;
        try {
            SaajSoapMessage message = (SaajSoapMessage) messageContext.getResponse();
            s = new ByteArrayOutputStream();
            message.writeTo(s);

            String response = s.toString();
            System.out.println("SOAP RESPONSE: " + response);

            // do all string replacement here
            response = response.replace("FOO", "BAR");

            is1 = new ByteArrayInputStream(response.getBytes());

            SaajSoapMessageFactory saaj = new SaajSoapMessageFactory();
            saaj.setSoapVersion(SoapVersion.SOAP_12);
            saaj.setMessageFactory(MessageFactory.newInstance());

            messageContext.clearResponse();
            messageContext.setResponse(saaj.createWebServiceMessage(is1));

            System.out.println("New Response has been SET");
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            if(s != null)
                try {
                    s.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            if(is1 != null)
                try {
                    is1.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
        }
    }
}

And register this newly created class in our SOAPConfig java class,

    @Bean
    public WebServiceTemplate webServiceTemplate() {
        WebServiceTemplate webServiceTemplate = new WebServiceTemplate();
        webServiceTemplate.setMarshaller(marshaller());
        webServiceTemplate.setUnmarshaller(marshaller());

        // register our interceptor here
        webServiceTemplate.setInterceptors(new ClientInterceptor[]{new SOAPClientInterceptor()});

        return webServiceTemplate;
    }

And we can try to do a rest api call with curl to see whether response has changed or not,

$ curl -kv http://localhost:8081/
*   Trying ::1:8081...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET / HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.65.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Connection: keep-alive
< Transfer-Encoding: chunked
< Content-Type: application/json
< Date: Wed, 03 Mar 2021 10:15:49 GMT
<
* Connection #0 to host localhost left intact
{"response":"BAR"}                                      

As we can see, our previous FOO has been changed into BAR which shows that finally we can replace WS response on-the-fly.

Code can be found here,

https://github.com/edwin/spring-boot-ws-response-modification

How to Handle CORS in Red Hat Process Automation Manager

One feature on Red Hat Process Automation Manager (RHPAM) is the ability to provide an API endpoint which can be accessed from multiple applications. But when we call them directly from javascript within a browser, sometimes it would shows a CORS error.

Workaround is quite easy, either we add a CORS header on RHPAM API response or change call method from a browser call into server-to-server call. For this article i would go with the first approach, and that is adding a CORS header on API that RHPAM provides.

Luckily our RHPAM deployment is on top of Openshift, and by adding below key-value parameters to RHPAM kie-server’s Deployment Config is good enough to solve CORS issue.

         - name: FILTERS
           value: "AC_ALLOW_ORIGIN,AC_ALLOW_METHODS,AC_ALLOW_HEADERS,AC_ALLOW_CREDENTIALS,AC_MAX_AGE"
         - name: AC_ALLOW_ORIGIN_FILTER_RESPONSE_HEADER_NAME
           value: "Access-Control-Allow-Origin"
         - name: AC_ALLOW_ORIGIN_FILTER_RESPONSE_HEADER_VALUE
           value: "*"
         - name: AC_ALLOW_METHODS_FILTER_RESPONSE_HEADER_NAME
           value: "Access-Control-Allow-Methods"
         - name: AC_ALLOW_METHODS_FILTER_RESPONSE_HEADER_VALUE
           value: "POST,GET,OPTIONS,PUT"
         - name: AC_ALLOW_HEADERS_FILTER_RESPONSE_HEADER_NAME
           value: "Access-Control-Allow-Headers"
         - name: AC_ALLOW_HEADERS_FILTER_RESPONSE_HEADER_VALUE
           value: "*"
         - name: AC_ALLOW_CREDENTIALS_FILTER_RESPONSE_HEADER_NAME
           value: "Access-Control-Allow-Credentials"
         - name: AC_ALLOW_CREDENTIALS_FILTER_RESPONSE_HEADER_VALUE
           value: "true"
         - name: AC_MAX_AGE_FILTER_RESPONSE_HEADER_NAME
           value: "Access-Control-Max-Age"
         - name: AC_MAX_AGE_FILTER_RESPONSE_HEADER_VALUE
           value: "86400"

Integrating DMN and Business Process on Red Hat Process Automation Manager

DMN stands for Decision Model and Notation. According to Wikipedia it is a standard approach for describing and modeling repeatable decisions within organizations to ensure that decision models are interchangeable across organizations. It is another approach of creating a “decision” on RHPAM (Red Hat Process Automation Manager), other than Decision Table and DRL.

On RHPAM, DMN file can be deployed as a standalone dpeloyment, or as an embedded within a Business Process. On this writing, im trying to do both and we’ll see what are the benefit and weakness of each approach.

Lets try to create a simple DMN to calculate how much loan should one get based on his age and salary. Create “age” and “salary” as DMN Input Data, and two DMN Decisions. “Loan_limit” with Decision Table, and “result” with Context.

Save, Build and Deploy, and we can test it by using rest api. But first we need to check on DMN Namespace and Model Name which is highlighted on the first screenshot and then put is as json parameter.

curl -L -X POST 'http://localhost:8080/kie-server/services/rest/server/containers/loan_validation_1.0.0-SNAPSHOT/dmn' \
-H 'Authorization: Basic cGFtQWRtaW46cGFzc3dvcmQ=' \
-H 'Content-Type: application/json' \
--data-raw '{
  "model-namespace":"https://kiegroup.org/dmn/_43FF885A-C2FB-49A4-BFB4-0F007A2C1C4F",
  "model-name":"Validation",
  "dmn-context": {
    "age":50,
    "salary":1200
  }
}'

The next step is put this DMN into a workflow. We can start by crating a simple workflow, dont forget to add DMN Namespace and Model Name on Business Rule Task.

And run this curl ommand to create a new instance,

curl -L -X POST 'http://localhost:8080/kie-server/services/rest/server/containers/loan_validation_1.0.0-SNAPSHOT/processes/loan_workflow/instances/' \
-H 'Authorization: Basic cGFtQWRtaW46cGFzc3dvcmQ=' \
-H 'Content-Type: application/json' \
--data-raw '{
    "age":50,
    "salary":1200
}'

And we can see the result on log,

12:58:08,486 INFO  [stdout] (default task-17) ================
12:58:08,486 INFO  [stdout] (default task-17) you are eligible for 20000
12:58:08,486 INFO  [stdout] (default task-17) ================

Code sample can be downloaded on below github repository,

https://github.com/edwin/rhpam-loan-validation-sample-with-dmn