Keycloak ist ein Identitätsmanagementsystem, welches einfach an die eigenen Erfordernisse angepasst werden kann. Diese Anpassungen können gänzlich neue Funktionen oder Erweiterungen schon bestehender Funktionen sein.
Als Software Engineer steht man in einem solchen Fall vor der Herausforderung, wie man die Erweiterungen testen kann. Ein Deployment auf die Produktionsumgebung, um dort den Test auszuführen, ist in den seltensten Fällen eine gute Idee. Aus diesem Grund ist es hilfreich, Keycloak lokal auf dem eigenen Rechner zu installieren und dort die Erweiterungen zu testen. Natürlich ist dies nicht der endgültige Test, denn auch Keycloak-Erweiterungen sollten mit Continuous Delivery über diverse Stages qualitätsgesichert deployed werden.

Dies gelingt uns in 3 kurzen Schritten:

  1. Das Keycloak-Dockerfile erstellen
  2. Die Konfiguration des Keycloak lokal testen
  3. Das Docker-Image packen und Keycloak lokal starten

Wie kann ich Keycloak überhaupt erweitern?

Keycloak bietet eine Reihe von Schnittstellen, welche die Erweiterung der Software ermöglichen. Diese Schnittstellen werden Service Provider Interface (SPI) genannt. Sie ermöglichen unter anderem das Hinzufügen von Authentifizierungsmechanismen (Authentication SPI) oder von Möglichkeiten zur Benutzerdatenhaltung (User Storage SPI). Beispiele für Erweiterungen sind z.B. die Emailnotifications und die Verwendung eigener LDAP-Strukturen.

Schritt 1: Das Keycloak-Dockerfile

Übrigens gibt es das hier erläuterte Beispiel auch in unserem GitHub-Repository.

Am einfachsten gestaltet sich die Verwendung des jboss/keycloak Docker Images, welches uns bereits eine funktionsfähige Keycloak-Instanz zur Verfügung stellt. Damit wir aber auch in der Lage sind, unsere Testdaten bei jeder Benutzung des Containers zu verwenden, erweitern wir dieses Image mit einem eigenen Dockerfile:

FROM jboss/keycloak@sha256:cc624aa8a797046a826bd70b1a2ff041beeb70a15ae5fa4ae4f832e4366f23f1

ENV KEYCLOAK_LOG_LEVEL=DEBUG
EXPOSE 8080/tcp
COPY ./keycloak/ /tmp/import

USER root
RUN chgrp -R 0 $JBOSS_HOME &&
chmod -R g+rw $JBOSS_HOME &&
chmod -R 777 /tmp/import
RUN sed -i -e 's/keycloak;AUTO_SERVER=TRUE/&;MVCC=TRUE;LOCK_TIMEOUT=10000/' $JBOSS_HOME/standalone/configuration/standalone.xml

CMD ["-b", "0.0.0.0", "-Dkeycloak.migration.action=import", "-Dkeycloak.migration.provider=dir", "-Dkeycloak.migration.dir=/tmp/import"]
USER jboss

Mit dieser Konfiguration sind wir in der Lage, ein auf Keycloak 11 basierendes Image zu starten, welches als standalone Variante eine H2 In-Memory-Datenbank verwendet. Das bedeutet aber auch, dass Änderungen an der Konfiguration nach einem Neustart verloren gehen.

Im Ordner /tmp/import des Containers können exportierte Konfigurationen untergebracht werden, sodass sie bei jedem Start importiert werden. Die so bereitgestellte Keycloak Instanz können wir unter http://localhost:8080 aufrufen.

Aber wie testen wir, dass unsere Testuser, die wir für diese Instanz angelegt haben, auch wirklich ein Token bekommen, um sich an unserem Service anzumelden? Hier kommt uns die Wahl eines Buildsystems zu Gute.

Schritt 2: Die Konfiguration lokal testen

Das Projekt nutzt Maven als Buildsystem. Sprich, wir haben das Dockerfile in ein Maven Projekt verpackt. Um die Keycloak Konfiguration mit Hilfe eines Integrationstests zu testen nutzen wir mvn verify. Für die Integrationstests verwenden wir das Testframework Cucumber:

Ability: Check Configuration of Keycloak Docker Image

  Scenario Template: Check if user can login and gets access token
    Given I am the user <username> and my password is <password>
    When I try to log in to the client <clientId> with secret <clientSecret> on realm <realm>
    Then I get an access token

    Examples:
      | realm                 | clientId            | clientSecret                         | username | password |
      | test-realm            | test-client         | 78a10012-188a-45e7-9d58-cc557f237087 | test     | test     |

Hinweis

In einem Cucumber-Feature haben produktive Daten nichts verloren! Allein Testdaten sollten verwendet werden, die nur lokale Services für Testzwecke absichern.

Wie aber wird der Keycloak tatsächlich aufgerufen? Das erfahren wir aus dem entsprechenden Step des Integrationstests:

public class LoginStepDefs {

  @Inject
  UserWorld userWorld;

  @Inject
  TokenWorld tokenWorld;

  @When("^I try to log in to the client (.+) with secret (.+) on realm (.+)$")
  public void whenITryToLogIn(String clientId, String clientSecret, String realm) {
    AccessTokenSupplier accessTokenSupplier = new AccessTokenSupplier(CucumberProperties.getBaseUri(),
        new LoginInfo(userWorld.getCurrentUser(), realm, clientId, clientSecret));
    tokenWorld.setCurrentAccessToken(accessTokenSupplier.get());
  }
 ...
}
final class AccessTokenSupplier implements Supplier<String> {

  private final LoginInfo loginInfo;
  private final String baseUri;

  AccessTokenSupplier(final String baseUri, LoginInfo loginInfo) {
    this.baseUri = baseUri;
    this.loginInfo = loginInfo;
  }

  @Override
  public String get() {
    Map<String, String> params = new HashMap<>();
    params.put("grant_type", loginInfo.getGrantType().getValue());
    params.put("client_id", loginInfo.getClientId());
    params.put("client_secret", loginInfo.getClientSecret());
    if (loginInfo.getGrantType() == GrantType.PASSWORD) {
      params.put("username", loginInfo.getUsername());
      params.put("password", loginInfo.getPassword());
    }

    String tokenResponse = given()
        .baseUri(baseUri)
        .params(params)
        .log().ifValidationFails(LogDetail.ALL, true)
        .when()
        .post("/realms/" + loginInfo.getRealm() + "/protocol/openid-connect/token")
        .then().statusCode(200)
        .extract().response().asString();
    JsonPath jsonPath = new JsonPath(tokenResponse);
    return jsonPath.getString("access_token");
  }
}

Mit dem Cucumberfeature haben wir nun auch sichergestellt, dass unser Keycloak tatsächlich die darin gefordeten Benutzer/Clients mit einem Token versorgt.

Schritt 3: Das Docker-Image packen und Keycloak lokal starten

Mit mvn install können wir das Image von Maven bauen lassen. Damit landet es direkt in unserer lokalen Docker Registry.

Wir können nun das Docker-Image mit docker run conciso-keycloak:latest starten. Auch können wir dieses Image nun in anderen Projekten verwenden, um uns einen Test-Keycloak automatisiert für Integrationstests zu starten.

Damit haben wir die 3 wichtigsten Schritte gemeistert. Nun können wir anfangen unsere Applikation damit zu testen! Alle Details finden Sie in unserem GitHub-Repository.

Und wie geht es jetzt weiter?

In diesem Beispiel habe ich nur die Grundlagen gelegt, um eine lokale Instanz eines Keycloaks deployen und testen zu können. Keycloak bietet eine große Anzahl an Erweiterungsmöglichkeiten, die so unabhängig von Infrastrukturen lokal ausprobiert und weiter entwickelt werden können.

Übrigens hat mein Kollege Sven-Torben Janus kürzlich bei der JUG Dortmund einen Talk über die Erweiterungsmöglichkeiten von Keycloak gehalten: