Sitemap

ITNEXT

ITNEXT is a platform for IT developers & software engineers to share knowledge, connect, collaborate, learn and experience next-gen technologies.

Implementing and Securing a Spring Boot RSocket App using Keycloak for IAM

Step-by-step guide on building and securing Clock Server app with Spring Boot, RSocket and Keycloak for Identity and Access Management

10 min readJan 2, 2024

--

Press enter or click to view image in full size
Photo by Markus Winkler on Unsplash

In this article, we’ll implement a Spring Boot application called Clock Server. Unlike regular apps that use HTTP, our Clock Server will use RSocket.

RSocket is a binary protocol for system communication, designed for making reactive, real-time apps. With RSocket, we can use binary messages directly over TCP or WebSocket. It has modern features like multiplexing, back-pressure, resumption, and routing. Besides, it supports different messaging modes like request-response, request-stream, fire-and-forget, and channel.

The Clock Server will expose the following endpoints:

  • get.time: This endpoint is used for the request-response model, where the client can request the current time, and the server responds with the corresponding time;
  • stream.time: This endpoint is designed for the request-stream model, allowing the client to initiate a streaming of times, specifying the intervalInSeconds it wants to receive. The server responds with a continuous stream of times.

Later, we will secure the Clock Server with Keycloak.

Keycloak is an open-source identity and access management solution that provides authentication, authorization, and single sign-on capabilities for web and mobile applications.

Without any further ado, let’s get started.

Prerequisites

If you would like to follow along, you must have Java 17+ and Docker installed on your machine.

Creating Clock Server Spring Boot app

Let’s create a Spring-Boot application using Spring Initializr.

The application name will be clock-server and the dependencies needed are: Spring Reactive Web and RSocket.

We will use the Spring Boot version 3.2.4 and Java 17. Here is the link that contains all the setup mentioned previously.

Click the GENERATE button to download a zip file. Unzip the file to a preferred folder and then open the clock-server project in your IDE.

Create the controller package

In order to keep our code organized, let’s create the controller package inside the com.example.clockserver root package.

Create the DTO

In controller package, let’s create the ClockStreamRequest record with the following content:

package com.example.clockserver.controller;

public record ClockStreamRequest(int intervalInSeconds) {
}

Create the ClockController class

In controller package, let’s create the ClockController class with the content below:

package com.example.clockserver.controller;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Controller
public class ClockController {

@MessageMapping("get.time")
public Mono<String> getTime() {
return Mono.just(getNowAndConvert());
}

@MessageMapping("stream.time")
public Flux<String> streamTime(@Payload(required = false) ClockStreamRequest request) {
int interval = validateAndGetInterval(request);
return Flux.interval(Duration.ofSeconds(interval)).map(tick -> getNowAndConvert());
}

private int validateAndGetInterval(ClockStreamRequest request) {
return (request != null && request.intervalInSeconds() > 0) ?
request.intervalInSeconds() : DEFAULT_INTERVAL_SECONDS;
}

private String getNowAndConvert() {
return dtf.format(LocalDateTime.now());
}

private static final int DEFAULT_INTERVAL_SECONDS = 5;
private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("HH:mm:ss");
}

Update the application.properties

Let’s update the application.properties by adding the property highlighted in bold:

spring.application.name=clock-server

spring.rsocket.server.port=7000

The property spring.rsocket.server.port sets the RSocket server port.

Download RSocket Client CLI

We will use the RSocket Client CLI (RSC) to communicate with Clock Server. So, in a terminal, make sure you are in the clock-server root folder and run the command below to download it:

curl -o rsc.jar -LJO https://github.com/making/rsc/releases/download/0.9.1/rsc-0.9.1.jar

The RSC will be used later but, for now, let’s check if it’s working. For it, run the following command:

java -jar rsc.jar --help

You should see:

usage: rsc [options] uri

Non-option arguments:
[String: uri]

Option Description
------ -----------
--ab, --authBearer [String] Enable Authentication Metadata
Extension (Bearer).
--authBasic [String] [DEPRECATED] Enable Authentication
Metadata Extension (Basic). This
Metadata exists only for the
backward compatibility with Spring
Security 5.2
--channel Shortcut of --im REQUEST_CHANNEL
...

Initial Demonstration

Starting Clock Server

In a terminal and inside the clock-server root folder, run the command below:

./mvnw clean spring-boot:run

Testing Endpoints

In another terminal, make sure you are in the clock-server root folder.

First, let’s submit a request-response request to get the time. The command is present below:

java -jar rsc.jar --request --route get.time tcp://localhost:7000

We got as response:

Press enter or click to view image in full size

When we run the command above with the --debug parameter, we will see some debug information explaining what happened during the request-response. It looks something like:

Press enter or click to view image in full size

Now, in two terminals, let’s submit two request-stream requests to initiate two streamings of times, one with one second interval and another with three seconds. The respective commands are:

java -jar rsc.jar --stream --route stream.time -d '{"intervalInSeconds":1}' tcp://localhost:7000
java -jar rsc.jar --stream --route stream.time -d '{"intervalInSeconds":3}' tcp://localhost:7000

To stop the streaming, press Ctrl+C

Here is a GIF showing the output:

Press enter or click to view image in full size

Shutdown Clock Server

In the terminal where the Clock Server is running, press Ctrl+C to stop the application.

Securing Clock Server With Keycloak

Modify the pom.xml

In the pom.xml file, let’s include the Spring Security and OAuth2 Resource Server dependencies by adding the following content (highlighted in bold):

<?xml version="1.0" encoding="UTF-8"?>
<project ...>
...
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-rsocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>


<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

</dependencies>
...
</project>

Create the security package

Let’s create the security package inside com.example.clockserver root package.

Create the RSocketSecurityConfig class

In security package, let’s create the RSocketSecurityConfig class with the content below:

package com.example.clockserver.security;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.rsocket.EnableRSocketSecurity;
import org.springframework.security.config.annotation.rsocket.RSocketSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Configuration
@EnableRSocketSecurity
public class RSocketSecurityConfig {

@Value("${jwt.auth.issuer-location}")
private String issuerLocation;

@Value("${jwt.auth.converter.resource-id}")
private String resourceId;

public static final String CLOCK_SERVER_USER = "CLOCK-SERVER-USER";

@Bean
public PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
rsocket.authorizePayload(authorize ->
authorize
.route("get.time").hasRole(CLOCK_SERVER_USER)
.route("stream.time").hasRole(CLOCK_SERVER_USER)
.anyRequest().authenticated()
.anyExchange().permitAll()
)
.jwt(jwtSpec -> jwtSpec.authenticationManager(jwtReactiveAuthenticationManager(jwtDecoder())));
return rsocket.build();
}

@Bean
public ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders.fromIssuerLocation(issuerLocation);
}

@Bean
public JwtReactiveAuthenticationManager jwtReactiveAuthenticationManager(ReactiveJwtDecoder decoder) {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
return Stream.concat(
jwtGrantedAuthoritiesConverter.convert(jwt).stream(),
extractRoles(jwt).stream()
).collect(Collectors.toSet());
});
JwtReactiveAuthenticationManager manager = new JwtReactiveAuthenticationManager(decoder);
manager.setJwtAuthenticationConverter(new ReactiveJwtAuthenticationConverterAdapter(converter));
return manager;
}

private Collection<? extends GrantedAuthority> extractRoles(Jwt jwt) {
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
Map<String, Object> resource;
Collection<String> resourceRoles;
if (resourceAccess == null
|| (resource = (Map<String, Object>) resourceAccess.get(resourceId)) == null
|| (resourceRoles = (Collection<String>) resource.get("roles")) == null) {
return Collections.emptySet();
}
return resourceRoles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toSet());
}
}

The RSocketSecurityConfig class is marked with the @Configuration and @EnableRSocketSecurity annotations, signaling its role as a configuration class for RSocket security.

The rsocketInterceptor method orchestrates payload authorization for some routes (message destinations):

  • Specific routes, such as get.time and stream.time demand the “CLOCK-SERVER-USER” role for access.
  • Authentication is mandatory for other routes, while any additional exchanges are universally permitted.

The jwtDecoder method creates a reactive JWT decoder using the provided issuer location in the application properties. In parallel, the jwtReactiveAuthenticationManager method sets up a reactive authentication manager designed for JWT. This manager employs a JwtAuthenticationConverter to extract roles from JWT claims, converting them into authorities. Roles are obtained from the “resource_access” claim in the JWT and prefixed with “ROLE_” before being designated as authorities.

The extractRoles method extracts roles from the “resource_access” claim in the JWT. It checks for the presence of the “resource_access” claim, fetches roles associated with the specified resource (identified by the resource ID), and converts them into authorities.

Update the application.properties

Let’s update the application.properties file with the properties highlighted in bold:

spring.application.name=clock-server

spring.rsocket.server.port=7000

jwt.auth.issuer-location=http://localhost:9080/realms/my-realm
jwt.auth.converter.resource-id=clock-server

Just a brief explanation of the new properties added:

  • jwt.auth.issuer-location: Specifies the issuer URI for validating JWT tokens;
  • jwt.auth.converter.resource-id: Defines the resource ID used by JwtReactiveAuthenticationManager to extract the roles.

Starting Keycloak

In a terminal, run the following command to start the Keycloak Docker container:

docker run -d --rm \
--name keycloak \
-p 9080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:24.0.2 start-dev

Wait for Keycloak to start. It takes some seconds.

Configuring Keycloak

Sign in

  1. Open a browser and access Keycloak Web Console at http://localhost:9080/admin;
  2. On the Sign in screen, type admin for both “Username” and “Password” fields;
  3. Click Sign in.

Create a new Realm

  1. On the left menu, click the dropdown button that contains “master”;
  2. Click Create Realm button;
  3. Set my-realm to “Realm name” field;
  4. Click Create button.

Disable the Required Action Verify Profile

  1. On the left menu, select “Authentication”;
  2. In the “Authentication” screen, click “Required actions” tab;
  3. Disable the “Verify Profile” required action.

Create a new Client

  1. On the left-hand menu, select “Clients”;
  2. In the “Clients” screen, click Create client;
  3. In the “General Settings” tab, set clock-server as the “Client ID” and click Next;
  4. In the “Capability config” tab, enable the “Client authentication” toggle switch and click Next;
  5. In the “Login Settings” tab, keep the default values and click the Save button.
  6. The Client secret generated for the clock-server client can be found in the “Credentials” tab. Make a note of this value, as it will be needed later.
  7. Finally, let’s create a new role for the clock-server client users.
  • Navigate to the “Roles” tab and click Create Role;
  • Type CLOCK-SERVER-USER as the Role Name;
  • Click Save to complete the process.

Create a new Group

  1. In the left-hand menu, select “Groups”;
  2. In the “Groups” screen, click Create group;
  3. Enter ClockServerUsers for the “Name” and click Create;
  4. Click on the name of the ClockServerUsers group to select it;
  5. Select the “Role mapping” tab and click Assign role;
  6. Choose “Filter by clients” and type CLOCK in the “Search by role name” field. The CLOCK-SERVER-USER role of the clock-server client will appear. Select it, and click Assign.

Create a new User

  1. In the left-hand menu, select “Users”;
  2. In the “Uses” screen, click the Add user button;
  3. In the “Create user” screen form:
  • Set user.test for the “Username” field;
  • Enable the “Email verified” toggle switch;
  • Click Join Groups button. Then, select ClockServerUsers and click Join ;
  • Finalize the Admin creation by clicking Create.

4. Let’s set a password for user.test:

  • Navigate to the “Credentials” tab;
  • Click Set password button.
  • Provide a strong password for user.test, 123 😝.
  • Disable the “Temporary” switch toggle.
  • Click the Save and confirm the password setting by clicking the Save password.

Demonstration With Security

Starting Clock Server

In a terminal, make sure you are inside the clock-server root folder. Then, run the following command:

./mvnw clean spring-boot:run

Testing Endpoints

In another terminal, and inside the clock-server root folder, let’s submit a request-response request to get the time. We are not providing the access token. The command is present below:

java -jar rsc.jar --request --route get.time tcp://localhost:7000

We got as response:

Press enter or click to view image in full size

We need an access token to retrieve the current time. In order to obtain it, we must ask Keycloak for it using the client secret generated when setting up of the clock-server client.

In order to retrieve the clock-server client secret, go to Keycloak Web Console > Clients > clock-server > Credentials.

Press enter or click to view image in full size

Once we have the client secret, let’s return to the terminal. We will create an environment variable called CLOCK_SERVER_CLIENT_SECRET and set the clock-server client secret value to it.

CLOCK_SERVER_CLIENT_SECRET=<client-secret-provided-by-keycloak>

Then, execute the following cURL command:

curl -X POST \
"http://localhost:9080/realms/my-realm/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=user.test" \
-d "password=123" \
-d "grant_type=password" \
-d "client_secret=$CLOCK_SERVER_CLIENT_SECRET" \
-d "client_id=clock-server"

We should get something like:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSl...",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSl...",
"token_type": "Bearer",
"not-before-policy": 0,
"session_state": "029f4f2c-946d-4ea4-83c9-ecccaf0c09f2",
"scope": "email profile"
}

What we need is the access_token. So, let’s copy and set it to another environment variable called USER_TEST_ACCESS_TOKEN:

USER_TEST_ACCESS_TOKEN=...

Let’s re-submit a request-response request to get the current time, now providing the access token. The command is:

java -jar rsc.jar --request --route get.time --authBearer $USER_TEST_ACCESS_TOKEN tcp://localhost:7000

We should receive as response:

Press enter or click to view image in full size

Let’s submit a request-stream request to initiate the streaming of times with 1 second interval. We are not providing the access token. The command is:

java -jar rsc.jar --stream --route stream.time -d '{"intervalInSeconds":1}' tcp://localhost:7000

As expected, we got access denied:

Press enter or click to view image in full size

Now, providing the access token, let’s re-submit a request-stream request. Here is updated command:

java -jar rsc.jar --stream --route stream.time -d '{"intervalInSeconds":1}' --authBearer $USER_TEST_ACCESS_TOKEN tcp://localhost:7000

To stop the streaming, press Ctrl+C

Here is a GIF showing the output:

Press enter or click to view image in full size

Shutdown Clock Server and Keycloak

In the terminal where the Clock Server is running, press Ctrl+C to stop the application.

To stop the Keycloak Docker container, run the following command in a terminal:

docker rm -fv keycloak

Conclusion

In this tutorial, we created a Spring Boot application named Clock Server, distinguishing it from regular apps by using RSocket instead of HTTP. Additionally, we secured the application with Keycloak. At the end, we initiated the application and tested its endpoints, ensuring they were correctly secured.

Additional Readings

Keycloak

18 stories

RSocket Crypto Server Tutorial

8 stories

Support and Engagement

If you enjoyed this article and would like to show your support, please consider taking the following actions:

  • 👏 Engage by clapping, highlighting, and replying to my story. I’ll be happy to answer any of your questions;
  • 🌐 Share my story on Social Media;
  • 🔔 Follow me on: Medium | LinkedIn | X | GitHub;
  • ✉️ Subscribe to my newsletter, so you don’t miss out on my latest posts.

--

--

ITNEXT
ITNEXT
Ivan Franchin
Ivan Franchin

No responses yet