And Todays’s Quote Would Be..

Life begins at the end of your comfort zone.

Neale Donald Walsch

Simple Hello World App by Using Snowdrop BOM Dependency Management

According to its website (snowdrop.dev), snowdrop is a cloud native spring boot app which is a project within the Red Hat community. From syntax perspective, it’s pretty much the same as ordinary Spring Boot and we can see on below code as an example.

Let’s start by creating a new java project with pom.xml, the highlighted part here is the bom configuration provided by Snowdrop.

<?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>HelloWorldSnowdrop</artifactId>
    <version>1.0-SNAPSHOT</version>

    <repositories>
        <repository>
            <id>redhat-early-access</id>
            <name>Red Hat Early Access Repository</name>
            <url>https://maven.repository.redhat.com/earlyaccess/all/</url>
        </repository>
        <repository>
            <id>redhat-ga</id>
            <name>Red Hat GA Repository</name>
            <url>https://maven.repository.redhat.com/ga/</url>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>redhat-early-access</id>
            <name>Red Hat Early Access Repository</name>
            <url>https://maven.repository.redhat.com/earlyaccess/all/</url>
        </pluginRepository>
        <pluginRepository>
            <id>redhat-ga</id>
            <name>Red Hat GA Repository</name>
            <url>https://maven.repository.redhat.com/ga/</url>
        </pluginRepository>
    </pluginRepositories>

    <properties>
        <snowdrop-bom.version>2.3.6.Final-redhat-00001</snowdrop-bom.version>
        <spring-boot.version>2.1.4.RELEASE-redhat-00001</spring-boot.version>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>dev.snowdrop</groupId>
                <artifactId>snowdrop-dependencies</artifactId>
                <version>${snowdrop-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>
        </dependency>
    </dependencies>


    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <version>${spring-boot.version}</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

</project>

Next is creating an ordinary rest api, like a regular spring boot.

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, args);
    }
}
package com.edw.controller;

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 HelloWorldController {

    @GetMapping("/")
    public Map index() {
        return new HashMap() {{
            put("hello", "world");
        }};
    }
}

Build, and run it thru IDE. And can test by using a curl command,

curl -kv http://localhost:8080/

Code can be accesses on below github link,

https://github.com/edwin/hello-world-snowdrop

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

Create a Protected JBoss EAP UDP Cluster with Authentication

This is the second part of my previous article about how to run multiple containerized Keycloak and make them able to communicate one and another thru UDP protocol. But this approach have a problem, what if an authorized JBoss EAP suddenly joining the cluster and do malicious thing such as intercepting message or even deleting clustered caches. To prevent this, JBoss EAP have a mechanism called AUTH protocol. Which means only instances of JBoss EAP which have a specific credentials can join in the cluster group.

So lets try to simulate this in one JBoss EAP instance on a containerized Keycloak,

docker run -p 8081:8080 -e PROXY_ADDRESS_FORWARDING=true \
 -e DB_VENDOR="mysql" -e DB_ADDR="192.168.56.1" -e DB_USER="keycloak" \
 -e DB_PASSWORD="password" -e DB_PORT="3306" -e DB_DATABASE="keycloak_db" \
 --add-host=HOST:192.168.56.1 jboss/keycloak

Check running image’s containerId by using docker ps command, and copy standalone-ha.xml file to our host folder. For this example, our containerId would be 3cdab1375336.

docker cp 3cdab1375336:/opt/jboss/keycloak/standalone/configuration/standalone-ha.xml .

Edit our standalone-ha.xml and adding this part. Im using password123 as cluster’s password, which means a JBoss EAP instance can only join a cluster when they have the same password.

<stack name="udp">
	<transport type="UDP" socket-binding="jgroups-udp"/>
	<protocol type="PING"/>
	<protocol type="MERGE3"/>
	<socket-protocol type="FD_SOCK" socket-binding="jgroups-udp-fd"/>
	<protocol type="FD_ALL"/>
	<protocol type="VERIFY_SUSPECT"/>
	<protocol type="pbcast.NAKACK2"/>
	<protocol type="UNICAST3"/>
	<protocol type="pbcast.STABLE"/>
	<protocol type="AUTH">
		<property name="auth_class">org.jgroups.auth.MD5Token</property>
		<property name="auth_value">password123</property>
		<property name="token_hash">SHA</property>
	</protocol>
	<protocol type="pbcast.GMS"/>
	<protocol type="UFC"/>
	<protocol type="MFC"/>
	<protocol type="FRAG3"/>
</stack>

Create a Dockerfile,

FROM jboss/keycloak
COPY standalone-ha.xml /opt/jboss/keycloak/standalone/configuration/

Re-build the image,

docker build -t mykeycloak .

And run the new modified image,

docker run -p 8081:8080 -e PROXY_ADDRESS_FORWARDING=true  \
 -e DB_VENDOR="mysql" -e DB_ADDR="192.168.56.1" -e DB_USER="keycloak"  \
 -e DB_PASSWORD="password" -e DB_PORT="3306" -e DB_DATABASE="keycloak_db"  \
 --add-host=HOST:192.168.56.1 mykeycloak

If we try run the original image which are not having any AUTH password at all, an error would occur showing that the corresponding JBoss EAP is unable to join the cluster.

07:23:11,866 WARN  [org.jgroups.protocols.UNICAST3] (thread-7,ejb,0f9a0d05f1eb) 
JGRP000039: 0f9a0d05f1eb: failed to deliver OOB message [ebf69c82cffd to 0f9a0d05f1eb, 0 bytes, flags=OOB|INTERNAL]: 
java.lang.IllegalStateException: found GmsHeader[JOIN_REQ]: mbr=ebf69c82cffd from ebf69c82cffd but no AUTH header

The sample code for this article can be seen at below github page,

https://github.com/edwin/jboss-eap-clustered-with-auth

Have fun using JBoss EAP 🙂