Keycloak is an open-source identity and access management solution that provides features such as Single Sign-On, social login, and user federation. It is widely used by organizations to secure their web applications and APIs.
One of the main advantages of Keycloak is its Java Admin Client, which allows developers to automate the management of Keycloak through Java code. The Java Admin Client provides a rich set of APIs that allow developers to perform a wide range of administrative tasks, such as creating and managing realms, users, roles, groups, and clients. In this hands-on blog series, we will work towards a fully automated configuration, exclusively using such Java code configurations. Whether you are a Keycloak beginner or an experienced developer, this blog series will provide valuable insights into the configuration of Keycloak using Java code and how it can make your life easier.
What we have seen so far:
- Identity and Access Management with Keycloak - In this blog post, we get to know some basic building blocks that we have at our disposal in Keycloak.
- Keycloak - Configuration as Code Pt. 1 - In this blog post, we cover how to create the basic project setup for our 'Keycloak - Configuration as Code' endeavour.
- Keycloak - Configuration as Code Pt. 2 - In this blog post, we add the Keycloak distribution to our project and containerize it with Docker.
- Keycloak - Configuration as Code Pt. 3 - In this blog post, we kicked off the configuration as code by adding a first example realm to our Keycloak instance.
Version updates
A lot of our dependencies release updates quite regularly, so here are some recommended version updates we need to apply to our root pom.xml file:
<quarkus.version>2.16.7.Final</quarkus.version>
<quarkus.resteasy.version>2.16.7.Final</quarkus.resteasy.version>
You build it, you run it (and test it)!
We continue where we left things in part 3 of this blog series. We are at the stage where we added configuration code to our code base, that creates a custom example realm on our Keycloak instance.
Test it
In Keycloak - Configuration as Code Pt. 1 it was mentioned that one of the why's for using this configuration as code approach is that our code becomes (unit) testable.
For that purpoose, let us add a few more dependencies that will add JUnit support to our java-configuration module.
- Firstly, in the
<properties>
element in./pom.xml
, we add:
<!-- Testing -->
<junit.jupiter.version>5.9.2</junit.jupiter.version>
<assertj-core.version>3.23.1</assertj-core.version>
<mockito.version>5.3.1</mockito.version>
<junit.pioneer.version>2.0.1</junit.pioneer.version>
- Secondly, in the
<dependencyManagement>
element in./pom.xml
, we add:
<!--Test-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj-core.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<version>${junit.pioneer.version}</version>
<scope>test</scope>
</dependency>
We will use these test dependencies in our java-configuration module, hence we need to add them in .java-configuration/pom.xml
to the element:
<!--Test-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
</dependency>
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
</dependency>
The stage is set and we can now write our first unit test java-configuration/src/test/java/nl/the_experts/keycloak/configuration/KeycloakConfigurationApplicationTest.java
that will test that the entry point of our cofig as code application configures the Keycloak client as expected with the given environment variables:
@ExtendWith(MockitoExtension.class)
class KeycloakConfigurationApplicationTest {
@Test
@SetSystemProperty(key = "KEYCLOAK_SERVER", value = "server_test")
@SetSystemProperty(key = "KEYCLOAK_USER", value = "user_test")
@SetSystemProperty(key = "KEYCLOAK_PASSWORD", value = "password_test")
@SetSystemProperty(key = "KEYCLOAK_REALM", value = "realm_test")
void main_shouldCreateKeycloakClient() {
MockedStatic<KeycloakConfigurationProperties> keycloakConfigPropertiesMockedStatic =
mockStatic(KeycloakConfigurationProperties.class, CALLS_REAL_METHODS);
MockedStatic<KeycloakClientBuilder> keycloakClientBuilderMockedStatic =
mockStatic(KeycloakClientBuilder.class, CALLS_REAL_METHODS);
MockedConstruction<KeycloakConfiguration> keycloakConfigurationMockedConstruction =
mockConstruction(KeycloakConfiguration.class);
// when
KeycloakConfigurationApplication.main(Arrays.array("test"));
// then
keycloakConfigPropertiesMockedStatic.verify(KeycloakConfigurationProperties::fromEnv, times(1));
keycloakClientBuilderMockedStatic.verify(() ->
KeycloakClientBuilder.create(
"server_test",
"user_test",
"password_test",
"realm_test"
), times(1));
KeycloakConfiguration keycloakConfiguration = keycloakConfigurationMockedConstruction.constructed().get(0);
verify(keycloakConfiguration, times(1)).configure();
keycloakConfigPropertiesMockedStatic.close();
keycloakClientBuilderMockedStatic.close();
keycloakConfigurationMockedConstruction.close();
}
}
In a similar fashion, we add unit tests for KeycloakConfiguration.java
:
@ExtendWith(MockitoExtension.class)
class KeycloakConfigurationTest {
@InjectMocks
private KeycloakConfiguration keycloakConfiguration;
@Test
void configure_shouldCallsConfigureForExampleRealms() {
// given
MockedConstruction<ExampleConfiguration> exampleConfigurationMockedConstruction =
mockConstruction(ExampleConfiguration.class);
// when
keycloakConfiguration.configure();
// then
ExampleConfiguration exampleConfiguration = exampleConfigurationMockedConstruction.constructed().get(0);
verify(exampleConfiguration, times(1)).configure();
// finally
exampleConfigurationMockedConstruction.close();
}
}
And ExampleConfiguration.java
:
@ExtendWith(MockitoExtension.class)
class ExampleConfigurationTest {
@Mock
Keycloak keycloak;
@InjectMocks
private ExampleConfiguration exampleConfiguration;
@Test
void configure_shouldCallAllRequiredMethods() {
// given
MockedConstruction<RealmConfiguration> realmConfigConstructorMock = mockConstruction(RealmConfiguration.class);
// when
exampleConfiguration.configure();
// then
RealmConfiguration mockRealm = realmConfigConstructorMock.constructed().get(0);
verify(mockRealm, times(1)).configure(ExampleConfiguration.REALM_NAME, ExampleConfiguration.REALM_DISPLAY_NAME);
realmConfigConstructorMock.close();
}
}
All of the above unit tests are testing quite basic behaviour, they merely verify that certain functions are being called on our configuration class instances. If you have further intereset in the use of that particular mocking pattern, feel free to read my blog Mastering Mockito's MockedConstruction feature.
Another test, that shows the power of unit testing our configuration nicely, is the RealmConfigurationTest.java
;
@ExtendWith(MockitoExtension.class)
class RealmConfigurationTest {
@Mock
private RealmResource realmResource;
@Mock
private RealmsResource realmsResource;
private RealmConfiguration realmConfiguration;
@BeforeEach
void beforeEach() {
realmConfiguration = new RealmConfiguration(realmsResource);
}
@Test
void configure_givenRealmNotPresent_shouldCreateNewRealm() {
// given
when(realmsResource.findAll()).thenReturn(List.of());
when(realmsResource.realm("example")).thenReturn(realmResource);
// when
realmConfiguration.configure(ExampleConfiguration.REALM_NAME, ExampleConfiguration.REALM_DISPLAY_NAME);
//then
verify(realmsResource).create(assertArg(result -> assertThat(result)
.returns(false, RealmRepresentation::isEnabled)
.returns("example", RealmRepresentation::getRealm)));
}
@Test
void configure_givenRealmPresent_shouldUpdateRealm() {
// given
when(realmsResource.findAll()).thenReturn(List.of(createRealmRepresentation("example", "example")));
when(realmsResource.realm("example")).thenReturn(realmResource);
// when
realmConfiguration.configure(ExampleConfiguration.REALM_NAME, ExampleConfiguration.REALM_DISPLAY_NAME);
//then
verify(realmResource).update(assertArg(result -> assertThat(result)
.returns(true, RealmRepresentation::isBruteForceProtected)
.returns(true, RealmRepresentation::isEnabled)));
verify(realmsResource, times(0)).create(any());
}
private RealmRepresentation createRealmRepresentation(String id, String realm) {
RealmRepresentation realmRepresentation = new RealmRepresentation();
realmRepresentation.setId(id);
realmRepresentation.setRealm(realm);
realmRepresentation.setEnabled(true);
return realmRepresentation;
}
}
In this unit test, we can actually verify that our code will produce the expected realm representations that are passed via the Keycloak Java Admin Client to our Keycloak instance. Remember that we enabled brute force detection on our example realm? The unit test configure_givenRealmPresent_shouldUpdateRealm
verifies that after the updating of our realm the brute force detection and the realm itself is actually enabled.
These configuration steps are only an example for now and we will add more specific configurations as we go along. For now, the added unit tests cover our added configuration code nicely with roughly 95% line coverage.
Build it
Maven update
We add the following build section to our java-configuration/pom.xml
:
<build>
<plugins>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>${maven.jar.plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>${maven.shade.plugin.version}</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>nl.the_experts.keycloak.configuration.KeycloakConfigurationApplication</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/services/javax.ws.rs.ext.Providers</resource>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<outputFile>${project.build.directory}/${project.artifactId}.jar</outputFile>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Local docker compose setup
We add the following lines at the end of our docker-compose.yml
file:
keycloak-java-configuration:
image: openjdk:17
volumes:
- ./java-configuration/target/java-configuration.jar:/jars/java-configuration.jar
environment:
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=admin
- KEYCLOAK_CONFIG_SERVER_URI=http://localhost:8080
- KEYCLOAK_CONFIG_REALM=master
working_dir: /jars
command: java -Dkeycloak.server=${KEYCLOAK_CONFIG_SERVER_URI} -Dkeycloak.realm=${KEYCLOAK_CONFIG_REALM} -Dkeycloak.user=${KEYCLOAK_ADMIN} -Dkeycloak.password=${KEYCLOAK_ADMIN_PASSWORD} -jar java-configuration.jar
depends_on:
keycloak1:
condition: service_healthy
The config script
We need a small shell script that will trigger our configuration jar file once our docker image is started. For that purpose, we add java-configuration/src/main/resources/scripts/start-configuration.sh
:
#!/bin/bash
echo Starting Keycloak configuration
if [[ -z "$KEYCLOAK_SERVER_URI" ]]; then
KEYCLOAK_SERVER_URI=http://keycloak-server/auth
fi
if [[ -z "$KEYCLOAK_REALM" ]]; then
KEYCLOAK_REALM=master
fi
JAR=java-configuration.jar
if [[ ! -f $JAR ]];then
JAR=/opt/keycloak-config/$JAR
fi
# Start Java based configuration.
java -Dkeycloak.server=${KEYCLOAK_CONFIG_SERVER_URI} -Dkeycloak.realm=${KEYCLOAK_CONFIG_REALM} \
-Dkeycloak.user=${KEYCLOAK_ADMIN} -Dkeycloak.password="${KEYCLOAK_ADMIN_PASSWORD}" \
-jar $JAR "$@"
if [ $? -eq 0 ]
then
echo "Keycloak configuration finished."
exit 0
else
echo "Keycloak configuration failed." >&2
exit 1
fi
The docker image
In order to add our Java configuration jar to our docker image and automatically execute it, we add the following lines to our Dockerfile
after line 23:
RUN mkdir -p /opt/keycloak-config && chown 1000:0 /opt/keycloak-config
COPY --chown=1000:0 java-configuration/target/java-configuration.jar /opt/keycloak-config
COPY --chown=1000:0 java-configuration/target/classes/scripts/start-configuration.sh /opt/keycloak-config
Adding our config jar to the docker image is important, because we will be needing it later on when deploying Keycloak to the cloud.
Run it
If you havent't done so please run
mvn clean package
from your terminal in the root directory of the project.
After having done so, we can simply run
docker compose up --build
./docker.compose.yml`.
This command will build our docker images and launch the containers, as defined in
Discussion (0)