the/experts. Blog

Cover image for Keycloak - Configuration as Code Pt. 4
Maik Kingma
Maik Kingma

Posted on • Updated on

Keycloak - Configuration as Code Pt. 4

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:

  1. 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.
  2. 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.
  3. Keycloak - Configuration as Code Pt. 2 - In this blog post, we add the Keycloak distribution to our project and containerize it with Docker.
  4. 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

from your terminal in the root directory of the project.
After having done so, we can simply run

docker compose up --build
Enter fullscreen mode Exit fullscreen mode



This command will build our docker images and launch the containers, as defined in
./docker.compose.yml`.

Discussion (0)