Programming

basic programming

Using Infinispan to Store Spring Boot’s HTTP Session

There are multiple ways of externalizing http session in Spring Boot, we can use a regular SQL database, or even a no-sql approach such as using Infinispan. For this sample, we are trying to integrate Spring Boot with Spring Security and externalizing its session to Infinispan.

So lets start with running an Infinispan instances,

$ docker pull infinispan/server:latest

$ docker run -p 11222:11222 -e USER=admin -e PASS=password infinispan/server

And create a new cache with the name of “app-session”, with a lifespan of one day, and and idle time of 5 minutes.

<?xml version="1.0"?>
<distributed-cache name="app-session" owners="1" mode="SYNC" statistics="true">
	<encoding>
		<key media-type="application/x-protostream"/>
		<value media-type="application/x-protostream"/>
	</encoding>
	<locking isolation="REPEATABLE_READ"/>
	<expiration lifespan="86400000" max-idle="300000"/>
</distributed-cache>

After that, we can focus on creating a new Java apps. We can start with a new pom.xml 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>org.example</groupId>
    <artifactId>spring-infinispan-session</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>
        <infinispan.version>14.0.1.Final</infinispan.version>
        <spring-session.version>2.7.0</spring-session.version>
        <spring-boot.version>2.7.0</spring-boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.infinispan</groupId>
                <artifactId>infinispan-bom</artifactId>
                <version>${infinispan.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

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

        <!-- spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- storing session in external storage -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-core</artifactId>
            <version>${spring-session.version}</version>
        </dependency>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-spring-boot-starter-remote</artifactId>
            <version>${infinispan.version}</version>
        </dependency>

    </dependencies>
</project>

And application.properties,

# spring boot
server.port=8080

# infinispan
infinispan.remote.server-list=127.0.0.1:11222
infinispan.remote.auth-username=admin
infinispan.remote.auth-password=password

# serialization
infinispan.remote.java-serial-whitelist=java.lang.*

And we can start with to code our Java files,

package com.edw;

import org.infinispan.spring.remote.session.configuration.EnableInfinispanRemoteHttpSession;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
@EnableInfinispanRemoteHttpSession(cacheName = "app-session")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
package com.edw.controller;

import org.infinispan.spring.remote.provider.SpringRemoteCacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;

@RestController
public class IndexController {

    @Autowired
    SpringRemoteCacheManager cacheManager;

    @GetMapping(path = "/")
    public HashMap index() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return new HashMap(){{
            put("hello", auth.getName());
        }};
    }
}
package com.edw.config;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("admin")
                .password("{noop}password")
                .roles("ADMIN")
            .and()
                .withUser("user")
                .password("{noop}password")
                .roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception{
        super.configure(http);
        http
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
            .and()
                .csrf()
                .disable();
    }
}
package com.edw.config;

import org.infinispan.commons.marshall.ProtoStreamMarshaller;
import org.infinispan.spring.starter.remote.InfinispanRemoteCacheCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

@Configuration
public class InfinispanConfiguration {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public InfinispanRemoteCacheCustomizer remoteCacheCustomizer() {
        return b -> {
            b.remoteCache("app-session").marshaller(ProtoStreamMarshaller.class);
        };
    }
}

If some NullPointerException happens, make sure that your cache is created first before we start our Java apps.

We can run the code and see our Spring Security default login page,

User admin as username, and password as its password to login, and we can see the login result,

And we can see the number of entries in increased on our app-session cache,

Code for this application can be found in below repository,

https://github.com/edwin/spring-boot-and-infinispan-http-session

Blacklist a Specific Application URL on Openshift using Route

Sometimes we want to hide a sensitive URLs such as our prometheus or even Spring Boot’s actuator from external world, but we still want those URL to be accesible within internal cluster. Basically there are multiple ways of doing that, such as blocking it from Firewall, rewrite from Reverse Proxy, or even doing blacklisting from application level.

One thing that i want to try is to do blacklisting from Openshift Route level, which is something doable by the DevOps team since it is still within platform level.

So for this example, i want to expose all my API to external world except for actuator endpoint which can only be consume within internal network. So lets start with a sample Kubernetes Service Yaml,

kind: Service
apiVersion: v1
metadata:
  name: catalogue-service
  namespace: edwin-ns
  labels:
    app: catalogue-service
    app.kubernetes.io/component: catalogue-service
    app.kubernetes.io/instance: catalogue-service
    app.kubernetes.io/name: catalogue-service
    app.kubernetes.io/part-of: sample-app
    app.openshift.io/runtime-version: latest
  annotations:
    openshift.io/generated-by: OpenShiftWebConsole
spec:
  ports:
    - name: 8080-tcp
      protocol: TCP
      port: 8080
      targetPort: 8080
  internalTrafficPolicy: Cluster
  type: ClusterIP
  ipFamilyPolicy: SingleStack
  sessionAffinity: None
  selector:
    app: catalogue-service
    deploymentconfig: catalogue-service

And i want to expose above Service into a specific URL by using Route,

kind: Route
apiVersion: route.openshift.io/v1
metadata:
  name: catalogue-service
  namespace: edwin-ns
  labels:
    app: catalogue-service
    app.kubernetes.io/component: catalogue-service
    app.kubernetes.io/instance: catalogue-service
    app.kubernetes.io/name: catalogue-service
    app.kubernetes.io/part-of: sample-app
    app.openshift.io/runtime-version: latest
  annotations:
    openshift.io/host.generated: 'true'
spec:
  host: catalogue-service-edwin-ns.apps.openshift.com
  to:
    kind: Service
    name: catalogue-service
    weight: 100
  port:
    targetPort: 8080-tcp
  tls:
    termination: edge
    insecureEdgeTerminationPolicy: Redirect
  wildcardPolicy: None

Above configuration means that everytime external users accessing catalogue-service-edwin-ns.apps.openshift.com, they are able to access catalogue-service application APIs thru its Kubernetes Service. If we want to block a specific URL, we need to create another Route yaml specifically for blocking it based on Path variable,

kind: Route
apiVersion: route.openshift.io/v1
metadata:
  name: catalogue-service-blocking-actuator
  namespace: edwin-ns
  labels:
    app: catalogue-service
    app.kubernetes.io/component: catalogue-service
    app.kubernetes.io/instance: catalogue-service
    app.kubernetes.io/name: catalogue-service
    app.kubernetes.io/part-of: sample-app
    app.openshift.io/runtime-version: latest
  annotations:
    haproxy.router.openshift.io/rewrite-target: /go-to-some-404-url
    openshift.io/host.generated: 'true'
spec:
  host: catalogue-service-edwin-ns.apps.openshift.com
  path: /actuator
  to:
    kind: Service
    name: catalogue-service
    weight: 100
  port:
    targetPort: 8080-tcp
  tls:
    termination: edge
    insecureEdgeTerminationPolicy: Redirect
  wildcardPolicy: None

Having those 2 YAML all together making sure that we are expose all APIs that are needed, excluding the actuator URL which we “rewrite” into some 404 url.

How to Generate User Statistics Queries using Keycloak

Sometimes we want to see how many users has registered to our Keycloak, how many login per-hours, how many failed logins, and other statistical data for multiple purposes.

We can a use sample queries below for generating those reports. But first we need to enable events for that corresponding realm,

Once we turn Keycloak events on, we can run below queries to populate the required results

## get total number of successful login
select count(1) from EVENT_ENTITY where TYPE='LOGIN';

## get total number of failed login
select count(1) from EVENT_ENTITY where TYPE='LOGIN_ERROR';

## get user's all activity
select USER_ENTITY.USERNAME, EVENT_ENTITY.* 
from USER_ENTITY, EVENT_ENTITY where EVENT_ENTITY.USER_ID = USER_ENTITY.ID
order by USER_ENTITY.USERNAME, EVENT_TIME;

Pretty simple right 🙂

Keycloak redirect_uri is Not HTTPS when Spring Boot is Behind Reverse Proxy

Recently i have a regular Keycloak deployment with the high level concept like below image,

But during implementation, i had this weird condition when Keycloak, behind a reverse proxy for SSL offloader, is redirecting to my Spring Boot application. But Keycloak is not detecting my Spring Boot application as https.

https://keycloak/auth/realms/realm/protocol/openid-connect/auth?
response_type=code&client_id=client-id&redirect_uri=http%3A%2F%2Fspring-boot-app%2Fsso&state=123&
login=true&scope=openid

As we can see, redirect_uri is having http as its protocol, instead of https. Despite my Spring Boot application is being deployed behind a reverse proxy with an SSL offloader.

The workaround is actually quite simple, first thing is that we need to forward request from users into downstream apps, which is Keycloak and Spring Boot. This is primarily being done on reverse proxy or Load Balancer such as F5 or Nginx

X-Forwarded-For: 10.20.81.131
X-Forwarded-Proto: https
X-Forwarded-Host: my.apps.com

But sometimes even after above headers being forwarded, Spring Boot still unaware that it is being accessed as HTTPS. Therefore we need to add one more configuration line in our Spring Boot’s application.properties configuration.

server.forward-headers-strategy=NATIVE

This should be sufficient enough.

How to Fix “java.lang.IllegalArgumentException: Invalid characters (CR/LF) in header message”

Recently i met a weird exception when using a Rest API endpoint provided by Red Hat Fuse (or Apache Camel) and deployed on top of Spring Boot,

javax.servlet.ServletException: java.lang.IllegalArgumentException: 
Invalid characters (CR/LF) in header message
	at org.apache.camel.http.common.CamelServlet.doService(CamelServlet.java:235) 
~[camel-http-common-2.21.0.fuse-770013-redhat-00001.jar!/:2.21.0.fuse-770013-redhat-00001]
	at org.apache.camel.http.common.CamelServlet.service(CamelServlet.java:80) 
~[camel-http-common-2.21.0.fuse-770013-redhat-00001.jar!/:2.21.0.fuse-770013-redhat-00001]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:791) 
~[jboss-servlet-api_4.0_spec-1.0.0.Final.jar!/:1.0.0.Final]
	at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74) 
~[undertow-servlet-2.0.30.SP1-redhat-00001.jar!/:2.0.30.SP1-redhat-00001]
	at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129) 
~[undertow-servlet-2.0.30.SP1-redhat-00001.jar!/:2.0.30.SP1-redhat-00001]

It happens everytime im using below CURL command,

curl -kv -L -X POST https://url/api/  -H 'Authorization: Basic Yxxxx'  -H 'Content-Type: application/json'  
--data-raw '{
        "id": "123",
        "birthDate": "19900429"
    }'

The funny thing is, i think culprit is because i have a newline within my json body request. After i change my command into below CURL, it seems that everything is working well now.

curl -kv -L -X POST https://url/api/  -H 'Authorization: Basic Yxxxx'  -H 'Content-Type: application/json'  
--data-raw '{"id": "123","birthDate": "19900429"}'

Weird eh