the/experts. Blog

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

Posted on • Updated on

Keycloak - Configuration as Code Pt. 3

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.

Update of the Keycloak version

A CVE has been identified in the version of Keycloak we are currently using in this blog post series (20.0.3).
CVE-2021-3754
Hence, we will update it to the most recent version 20.0.5. We achieve this by updating the ./pom.xml:34

<keycloak.version>20.0.5</keycloak.version>
Enter fullscreen mode Exit fullscreen mode

and the ./Dockerfile:7

COPY --chown=keycloak:keycloak keycloak/target/keycloak-20.0.5  /opt/keycloak
Enter fullscreen mode Exit fullscreen mode

The Keycloak Java Admin Client

In this blog post (and the following), we will take a closer look at the Keycloak Java Admin Client and how it can help developers streamline their Keycloak management tasks. We will explore the features and capabilities of the Java Admin Client and provide examples of how to use it to configure Keycloak.

Adding the dependencies

In the previous post, we already added the following to the dependencyManagement section in ./pom.xml

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-admin-client</artifactId>
    <version>${keycloak.version}</version>
</dependency>
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-core</artifactId>
    <version>${keycloak.version}</version>
</dependency>
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-services</artifactId>
    <version>${keycloak.version}</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

We now add those dependencies to our java-configuration module's pom.xml:

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-admin-client</artifactId>
</dependency>
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-core</artifactId>
</dependency>
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-services</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Hint: The demo code makes use of lombok code generation. Feel free to add it or replace the used annotations in subsequent classes with actual code.

Creating the Configuration Application

We create nl/the_experts/keycloak/configuration/KeycloakConfigurationApplication.java:

public class KeycloakConfigurationApplication {
    /**
     * Main method to start Keycloak configuration.
     *
     * @param args Command line arguments passed to the application.
     */
    public static void main(String[] args) {
        try {
            // 1
            KeycloakConfigurationProperties configurationProperties = KeycloakConfigurationProperties.fromEnv();

            // 2
            KeycloakClientBuilder keycloakClientBuilder = KeycloakClientBuilder.create(
                    configurationProperties.get("KEYCLOAK_SERVER"),
                    configurationProperties.get("KEYCLOAK_USER"),
                    configurationProperties.get("KEYCLOAK_PASSWORD"),
                    configurationProperties.get("KEYCLOAK_REALM"));
            Keycloak keycloak = keycloakClientBuilder.getClient();

            // 3
            KeycloakConfiguration keycloakConfig = new KeycloakConfiguration(keycloak);
            keycloakConfig.configure();
        } catch (Exception all) {
            Logger.getLogger(KeycloakConfigurationApplication.class).error("Exception occurred.", all);
            throw all;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The main method is the entry point for the configuration application. It starts by obtaining the Keycloak configuration properties from the environment variables using the KeycloakConfigurationProperties.fromEnv() method. The KeycloakClientBuilder is then used to create a Keycloak client by passing the required configuration details such as the Keycloak server URL, user, password, and realm.

Once the Keycloak client is created, a new instance of KeycloakConfiguration is created with the Keycloak client and configuration properties, which contains the bundled realms configurations we will add later on. The configure() method is then called on the KeycloakConfiguration instance to trigger the desired configuration process.

1. KeycloakConfigurationProperties

KeycloakConfigurationProperties is a custom utility class that reads a combination of environment variables and Java system properties (the latter has higher precedence). We create nl/the_experts/keycloak/configuration/KeycloakConfigurationProperties.java:

@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class KeycloakConfigurationProperties {

    private final Map<String, String> configuration;

    public String get(String name) {
        return configuration.get(name);
    }

    public static KeycloakConfigurationProperties fromEnv() {
        Map<String, String> systemProperties = System.getProperties().entrySet().stream()
                .collect(Collectors.toMap(e -> convertKey((String) e.getKey()), e -> (String) e.getValue()));
        Map<String, String> configMap = new HashMap<>(System.getenv());
        configMap.putAll(systemProperties);

        return new KeycloakConfigurationProperties(configMap);
    }

    private static String convertKey(String key) {
        return key.replace('.', '_').toUpperCase(Locale.ROOT);
    }
}
Enter fullscreen mode Exit fullscreen mode

The fromEnv() static method is used to create a new instance of KeycloakConfigurationProperties based on before mentioned system environment variables and Java system properties. The method first collects the Java system properties, converting the property keys to the format expected by the configuration map (replacing '.' with '_' and converting the key to uppercase). It then creates a new configuration map, initially populated with the environment variables, and subsequently updates it with the converted Java system properties.

2. KeycloakClientBuilder

The KeycloakClientBuilder class is a simple helper class that provides you with a configured Keycloak Admin CLI instance via the static method getClient(). We create nl/the_experts/keycloak/configuration/KeycloakClientBuilder.java:

@AllArgsConstructor(staticName = "create")
public class KeycloakClientBuilder {

    private static final String ADMIN_CLI = "admin-cli";

    private final String server;
    private final String username;
    private final String password;
    private final String realm;

    public Keycloak getClient() {
        return KeycloakBuilder.builder()
                .serverUrl(server)
                .realm(realm)
                .username(username)
                .password(password)
                .clientId(ADMIN_CLI)
                .build();
    }
}
Enter fullscreen mode Exit fullscreen mode

3. KeycloakConfiguration

This is where the actual magic happens. In this class, we will centrally trigger all our realm configurations from within the configure() method.

@JBossLog
@AllArgsConstructor
public class KeycloakConfiguration {

    private final Keycloak keycloak;

    /**
     * Starts configuration of Keycloak.
     */
    public void configure() {
        log.info("-----------------------------------------------");
        log.info("Starting Java configuration");
        log.info("-----------------------------------------------");

        new ExampleConfiguration(keycloak, configurationProperties).configure();

        log.info("-----------------------------------------------");
        log.info("Finished Java configuration without errors.");
        log.info("-----------------------------------------------");
    }
}
Enter fullscreen mode Exit fullscreen mode

Creating a Realm

Without further ado, we will start our configuration as code by creating the most basic building block in Keycloak: a realm named example.
We create nl/the_experts/keycloak/configuration/example/ExampleConfiguration.java with the following content:

@JBossLog
@AllArgsConstructor
public class ExampleConfiguration {
    static final String REALM_NAME = "example";

    private final Keycloak keycloak;

    /**
     * Configures the example realm.
     */
    public void configure() {
        log.info("-----------------------------------------------");
        log.infof("Starting configuration of realm '%s'.", REALM_NAME);
        log.info("-----------------------------------------------");

        new RealmConfiguration(keycloak.realms(), REALM_NAME).configure();

        log.info("-----------------------------------------------");
        log.infof("Finished configuration of realm '%s'.%n", REALM_NAME);
        log.info("-----------------------------------------------");
    }
}
Enter fullscreen mode Exit fullscreen mode

The ExampleConfiguration class is responsible for the entire configuration process of our example realm. The realm configuration itself is obviously a subset of required configuration steps. Hence, to configure the realm, the method creates a new RealmConfiguration instance, passing the Keycloak realms and the REALM_NAME as parameters, and then calls the configure() method on the RealmConfiguration instance.

Consequently, we also create nl/the_experts/keycloak/configuration/example/RealmConfiguration.java with the following content:

@JBossLog
class RealmConfiguration {

    private static final String REALM_DISPLAY_NAME = "example";

    private final RealmsResource realmsResource;
    private final String realmName;

    RealmConfiguration(RealmsResource realmsResource, String realmName) {
        this.realmsResource = realmsResource;
        this.realmName = realmName;
    }

    /**
     * Configures the realm, first validates if the realm exists and if none exists, creates the realm.
     */
    public void configure() {
        List<RealmRepresentation> realms = realmsResource.findAll();
        if (realms.stream().noneMatch(realm -> realm.getId().equals(realmName))) {
            log.infof("Realm does not yet exist, creating for realm: %s", realmName);
            createRealm(realmName, REALM_DISPLAY_NAME, realmsResource);
        }
        updateRealm();
    }

    private void createRealm(String realmName, String displayName, RealmsResource realmsResource) {
        RealmRepresentation realmRepresentation = new RealmRepresentation();
        realmRepresentation.setDisplayName(displayName);
        realmRepresentation.setId(realmName);
        realmRepresentation.setRealm(realmName);
        realmRepresentation.setEnabled(false);

        realmsResource.create(realmRepresentation);
        log.infof("Created realm '%s'", realmName);
    }

    private void updateRealm() {
        RealmRepresentation realmRepresentation = new RealmRepresentation();
        realmRepresentation.setBruteForceProtected(true);
        realmRepresentation.setEnabled(true);

        RealmResource realmResource = realmsResource.realm(realmName);
        realmResource.update(realmRepresentation);
    }
}
Enter fullscreen mode Exit fullscreen mode

For now, the realm configuration is quite simple. We check if the given realm has been created in the past, if not, we create it by passing the desired realmRepresentation to the realmsResource.create(realmRepresentation) function. In the background, this will trigger the same API call you would see in the network traffic when creating a realm via the Admin UI.
Afterwards, we can update the realm even further with desired settings. For the sake of the example, we created the realm in a disabled state, then update it to be enabled and equally enable brute force detection on the realm. This is passed in an updated realmRepresentation to the realmResource.update(realmRepresentation) function.

You build it, you run it (and test it)

We finished our first configuration as code snippet. Now we need to automate the execution of that configuration and package it into our docker image. AND we need to harvest one of our greater benefits of configuration as code: we automate the testing.

This blog has become quite long, so stay tuned for Pt.4 of this series: You build it, you run it (and test it) in which we will update the build steps and packaging of our Keycloak image. Also, we will dive into the unit testing of our recently added code.

The code in this tutorial can be found here.

Discussion (0)