edwin

Importing a Custom SPI into Keycloak Operator in Openshift

Keycloak Operator provide a convenient method for uploading a custom SPI into Keycloak instances, and that is by using an extensions inside Keycloak YAML operator.

apiVersion: keycloak.org/v1alpha1
kind: Keycloak
metadata: 
  namespace: my-redhat-sso
  labels:
    app: sso
spec:
  extensions:
    - >-
      https://url/custom-sso-spi-1.0.0.jar
  externalAccess:
    enabled: true
  externalDatabase:
    enabled: true
  instances: 1

Rollout your Keycloak pod, and you can see that Keycloak instance is now having a custom SPI embedded within it.

Directly Accessing Keycloak’s Registration Page

We can directly accessing Keycloak’s Registration Page without have to go to the Login Page first, and it is quite simple. Here is the URL required to have that condition,

http://localhost:8080/realms/PowerRanger/protocol/openid-connect/registrations?
       client_id=my-client-id&
       redirect_uri=redhat.com&
       response_type=code&
       scope=openid

This are achieve by using Keycloak version 17.

Quarkus, SmallRye, and Retry Mechanism

Quarkus provide a convenient library when connecting to an unreliable third party external system, and that is SmallRye Fault Tolerance. In this sample, we are trying to simulate a connection to an external website, which is reqres.in, while creating a random IOException.

Based on above scenario, we will try to retry the connection when we arent able to connect to our backend services, but retries will happen at most 4times and only on specific defined exceptions.

So lets start with a regular maven 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>quarkus-retry</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <skipITs>true</skipITs>
        <surefire-plugin.version>3.0.0-M7</surefire-plugin.version>
        <compiler-plugin.version>3.10.1</compiler-plugin.version>

        <!-- quarkus -->
        <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
        <quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
        <quarkus.platform.version>2.16.6.Final</quarkus.platform.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>${quarkus.platform.group-id}</groupId>
                <artifactId>${quarkus.platform.artifact-id}</artifactId>
                <version>${quarkus.platform.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-arc</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-resteasy-jackson</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-resteasy</artifactId>
        </dependency>

        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-smallrye-fault-tolerance</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-rest-client</artifactId>
        </dependency>

        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-junit5</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>${quarkus.platform.group-id}</groupId>
                <artifactId>quarkus-maven-plugin</artifactId>
                <version>${quarkus.platform.version}</version>
                <extensions>true</extensions>
                <executions>
                    <execution>
                        <goals>
                            <goal>build</goal>
                            <goal>generate-code</goal>
                            <goal>generate-code-tests</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${compiler-plugin.version}</version>
                <configuration>
                    <compilerArgs>
                        <arg>-parameters</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>${surefire-plugin.version}</version>
                <configuration>
                    <systemPropertyVariables>
                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                        <maven.home>${maven.home}</maven.home>
                    </systemPropertyVariables>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>${surefire-plugin.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                        <configuration>
                            <systemPropertyVariables>
                                <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                                <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                                <maven.home>${maven.home}</maven.home>
                            </systemPropertyVariables>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <profiles>
        <profile>
            <id>native</id>
            <activation>
                <property>
                    <name>native</name>
                </property>
            </activation>
            <properties>
                <skipITs>false</skipITs>
                <quarkus.package.type>native</quarkus.package.type>
            </properties>
        </profile>
    </profiles>

</project>

And create application.properties to put all apps configuration there

# default
quarkus.http.port=8080
quarkus.log.level=INFO
quarkus.log.category."com.edw".level=DEBUG

# rest client
com.edw.client.UserRestClient/mp-rest/url=https://reqres.in/

Next is to create a RestClient that will do a rest api call to an external 3rd party

@ApplicationScoped
@RegisterRestClient
@Path("/api")
public interface UserRestClient {
    @GET
    @Path("/users/{id}")
    Users get(@PathParam("id") Integer id);
}

And the create a Service class that will call the RestClient. In this class we are simulating an Exception randomly.

@ApplicationScoped
public class UserService {

    private Logger logger = LoggerFactory.getLogger(this.getClass().getName());

    @Inject
    @RestClient
    UserRestClient userRestClient;

    public Users getUser(Integer id) throws IOException {
        Random random = new Random();
        if(random.nextBoolean()) {
            logger.debug("==== simulate random exception ====");
            throw new IOException();
        }

        return userRestClient.get(id);
    }
}

And last, is our Controller class,

@Path("/")
public class HelloWorldController {

    private Logger logger = LoggerFactory.getLogger(this.getClass().getName());
    @Inject
    UserService userService;

    @GET
    @Path("/")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response index() {
        return Response
                .status(200)
                .entity(new Hello("world"))
                .build();
    }

    @GET
    @Path("/user/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    @Retry(maxRetries = 1, retryOn= IOException.class)
    @Fallback(fallbackMethod = "getEmptyUser")
    public Response getUser(@PathParam("id") Integer id) throws IOException {
        Users users = userService.getUser(id);
        return Response
                .status(200)
                .entity(users)
                .build();
    }

    public Response getEmptyUser(Integer id) throws IOException {
        logger.debug("==== giving default response ====");
        return Response
                .status(200)
                .entity(new Users())
                .build();
    }
}

We can run our Quarkus project by using below command,

$ mvn quarkus:dev

And do some rest api call to it, a successful response would looks like this

$ curl -kv http://localhost:8080/user/2
*   Trying ::1:8080...
* TCP_NODELAY set
*   Trying 127.0.0.1:8080...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /user/2 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.65.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json
< content-length: 280
<
* Connection #0 to host localhost left intact
{"data":{"id":2,"email":"janet.weaver@reqres.in","first_name":"Janet","last_name":"Weaver","avatar":"https://reqres.in/img/faces/2-image.jpg"},"support":{"url":"https://reqres.in/#support-heading","text":"To keep ReqRes free, contributions towards server costs are appreciated!"}}

while a failed one will give below response,

$ curl -kv http://localhost:8080/user/2
*   Trying ::1:8080...
* TCP_NODELAY set
*   Trying 127.0.0.1:8080...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /user/2 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.65.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json
< content-length: 28
<
* Connection #0 to host localhost left intact
{"data":null,"support":null} 

Code for this post can be found on below link,

https://github.com/edwin/quarkus-smallrye-retry

Java, SecureRandom, and Openshift

Had one unique case where generating a SecureRandom in Java is very very slow, the thing is that this never happens on a regular VM deployment, and only happens in a Pod deployment in Openshift 4.

This is original sample code that is slow,

    @GetMapping(path = "/secure-random")
    public HashMap secureRandom() {
        SecureRandom secureRandom = new SecureRandom();
        secureRandom.setSeed(secureRandom.generateSeed(24));
        return new HashMap(){{
            put("random-value", String.format("%06d", secureRandom.nextInt(1000000)));
        }};
    }

The reason why it is slow is because SecureRandom is relying on the OS random generator which is relying on noise. And it seems that for our case, we are lacking of noise to make a good entropy therefore we arent able to generate a SecureRandom at all.

Our workaround for this is by using a Pseudo Random Number Generator (PRNG) from Java, and not relying on the OS at all. For this example, im using “SHA1PRNG”.

    @GetMapping(path = "/secure-random-new")
    public HashMap secureRandomNew() throws Exception {
        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
        secureRandom.setSeed(secureRandom.generateSeed(24));
        return new HashMap(){{
            put("random-value", String.format("%06d", secureRandom.nextInt(1000000)));
        }};
    }

Hope it helps.

“No name matching” and “No subject alternative names present” when Connecting to a Secure Broker on Red Hat AMQ Broker

I had this error when using Artemis to connect to a secure broker on Openshift 4.10. AMQ Broker version is 7.10 and being installed by using Operator.

Caused by: java.security.cert.CertificateException: No name matching enterprise-rhamq-ss-0.enterprise-rhamq-hdls-svc.enterprise-rhamq.svc.cluster.local found
        at java.base/sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:234) [java.base:]
        at java.base/sun.security.util.HostnameChecker.match(HostnameChecker.java:103) [java.base:]
        at java.base/sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:458) [java.base:]
        at java.base/sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:418) [java.base:]
        at java.base/sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:292) [java.base:]
        at java.base/sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:144) [java.base:]
        at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1335) [java.base:]
        ... 28 more

And when accessing with a direct IP, it would give below error

Caused by: java.security.cert.CertificateException: No subject alternative names present
        at java.base/sun.security.util.HostnameChecker.matchIP(HostnameChecker.java:142) [java.base:]
        at java.base/sun.security.util.HostnameChecker.match(HostnameChecker.java:101) [java.base:]
        at java.base/sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:458) [java.base:]
        at java.base/sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:432) [java.base:]
        at java.base/sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:292) [java.base:]
        at java.base/sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:144) [java.base:]
        at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1335) [java.base:]
        ... 28 more

Apparently it is due to Java certificate validation. Workaround is quite easy, just adding below configuration on Artemis URL and it should works,

verifyHost=false