Keycloak SPIs implementieren – Schritt für Schritt

Foto des Autors
Sven-Torben Janus

Die Open-Source Identity- und Accessmanagement-Lösung Keycloak kann leicht erweitert werden. Dieser Beitrag zeigt, wie man die Service Provider Interface (SPI) nutzen kann, um bestehende Funktionalität von Keycloak zu erweitern oder gänzlich neue Funktionen bereitzustellen.

Die IAM-Lösung Keycloak erfreut sich in den letzten Jahren weiter Verbreitung. Sie bietet einen vielfältigen Funktionsumfang, in der Praxis sind jedoch Anpassungen immer wieder unabdingbar. Hierzu stehen SPIs als Erweiterungspunkte bereit. Dieser Artikel zeigt am konkreten Beispiel, wie diese für genutzt werden können. Eine „Schritt für Schritt“-Anleitung.

Keycloak ist eine open-source „Identity- und Accessmanagement-Lösung“, die sich mittlerweile weiter Verbreitung und Nutzung erfreut. Auch wenn viele Funktionen im Standard vorhanden sind, kommt es in der praktischen Anwendung immer wieder vor, dass eigene Erweiterungen notwendig werden. Glücklicherweise stehen in Keycloak eine Reihe von Erweiterungspunkten, sogenannte Service Provider Interfaces oder kurz SPIs, zur Verfügung, um vorhandene Funktionalität auszutauschen oder an die eigenen Bedürfnisse anzupassen.

Dieser Artikel zeigt, wie eine solche Erweiterung vorgenommen werden kann. Von der Bereitstellung eines eigenen Moduls, über die Registrierung eines Providers bis hin zu Aspekten wie Logging und Konfiguration werden die Grundlagen Schritt für Schritt erläutert.

Service Provider Interfaces in Keycloak

Keycloak bietet eine ganze Reihe an Service Provider Interfaces (SPIs), die als Erweiterungs- bzw. Anpassungspunkte dienen können. Da die Dokumentation längst nicht alle vorhandenen SPIs auflistet, empfiehlt sich für eine Gesamtübersicht ein Blick auf Keycloaks Server Info Page.

Hierzu reicht es aus, eine Keycloak-Instanz mittels Docker zu starten.

docker run -d --name keycloak -p 8080:8080 -e KEYCLOAK_USER=test -e KEYCLOAK_PASSWORD=test jboss/keycloak:11.0.2

Anschließend steht die Server Info Page mit einer Liste aller SPIs und verfügbarer Implementierung in der Administrationskonsole unter http://localhost:8080/auth zur Verfügung. Der Login erfolgt mit Benutzername test und Passwort test.

Server Infopage

Im Rahmen dieses Artikels soll die SPI emailSender als Beispiel dienen. Wie die Server Info Page zeigt, gibt es dazu in Keycloak genau eine Implementierung (Provider) namens default.
Die Implementierung dient zum Versand von E-Mails und nutzt die SMTP-Einstellungen, die in der Konsole unter Realm Settings im Untermenü Email konfiguriert werden können.
Als exemplarische Anforderung für eine Erweiterung soll es möglich sein, die Betreffzeilen aller E-Mails mit einem konfigurierbaren Präfix zu versehen. Beispielsweise soll ein Präfix „KEYCLOAK: “ konfiguriert werden können, so dass eine E-Mail mit dem eigentlichen Betreff „Ihr Passwort wurde geändert“ nun den Betreff „KEYCLOAK: Ihr Passwort wurde geändert“ trägt.

Ein Grundgerüst für die Implementierung

Die Implementierung einer SPI wird in der Regel als JAR-Datei ausgeliefert und später deployed.
Zur Generierung eines JARs kann ein einfaches Maven-Projekt genutzt werden.

Das Beispiel für diesen Artikel steht auf Github bereit. Nachfolgend werden daher nur Ausschnitte gezeigt.

Folgende Maven Dependencies sind für eine Implementierung der SPI notwendig und dem Projekt hinzuzufügen:

<project>
    <!-- ... -->
    <dependencies>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-core</artifactId>
            <version>${version.keycloak}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-server-spi</artifactId>
            <version>${version.keycloak}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-server-spi-private</artifactId>
            <version>${version.keycloak}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <!-- ... -->
</project>
Code-Sprache: HTML, XML (xml)

Die emailSender-SPI ist durch das Interface EmailSenderProvider definiert. Die eigene Implementierung erfolgt einfach über die Implementierung dieses Interfaces wie nachfolgend dargestellt.

public class EmailPrefixSenderProvider implements EmailSenderProvider {
  EmailPrefixSenderProvider() {
  }
  public void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
    // ... hier folgt die Implementierung
  }
  public void close() {
  }
}
Code-Sprache: JavaScript (javascript)

Zusätzlich benötigt diese Provider-Implementierung eine zugehörigen Factory im Sinne des Factory-Patterns. Die Implementierung erfolgt über das entsprechende Interface EmailSenderProviderFactory, in etwa wie folgt:

public class EmailPrefixSenderProviderFactory implements EmailSenderProviderFactory {
    public EmailSenderProvider create(KeycloakSession session) {
        return new EmailPrefixSenderProvider();
    }
    public void init(Config.Scope config) {
    }
    public void postInit(KeycloakSessionFactory factory) {
    }
    public void close() {
    }
    public String getId() {
        return "email-prefix";
    }
}
Code-Sprache: PHP (php)

Dabei wird in der Methode create eine Instanz des Providers erzeugt und in der Methode getId ein Name für die SPI-Implementierung definiert. Dieser Name ist äquivalent zum Namen default, der für die Standardimplementierung auf der Server Info Page zu sehen war.

Damit ist das Grundgerüst für eine Implementierung prinzipiell bereits erstellt. Es fehlt lediglich eine Datei, um den Provider zu registrieren, wie sie für Java Service Provider Interfaces üblich ist. Die Datei muss den Namen org.keycloak.email.EmailSenderProviderFactory haben und im Verzeichnis src/main/resources/META-INF/services liegen. Als Inhalt hat sie nur eine Zeile mit dem vollständigen Namen der Factory-Klasse:

io.github.conciso.keycloak.email.EmailPrefixSenderProviderFactory
Code-Sprache: CSS (css)
Dateistruktur des Projektes

Durch den Maven Build wird aus diesen Code-Artefakten eine JAR-Datei erzeugt.

mvn clean install

Deployment in einen Docker Container

Die JAR-Datei muss in ein Wildfly Modul eingepackt werden, um sie nach Keycloak deployen zu können. Das Wildfly Modul besteht im Prinzip aus einer Descriptor-Datei im XML-Format und der JAR-Datei selbst. Die Descriptor-Datei sieht dabei wie folgt aus und trägt den Namen module.xml. Sie liegt im Verzeichnis src/main/docker.

<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.3" name="keycloak-spi-example.provider">
    <resources>
        <resource-root path="${project.build.finalName}.jar"/>
    </resources>
    <dependencies>
        <module name="org.keycloak.keycloak-core"/>
        <module name="org.keycloak.keycloak-server-spi"/>
        <module name="org.keycloak.keycloak-server-spi-private"/>
    </dependencies>
</module>
Code-Sprache: HTML, XML (xml)

Sie definiert die Abhängigkeiten auf andere Keycloak-spezifische Wildfly Module. Diese Wildfly Module entsprechen den zuvor genannten Maven Dependencies. Darüber hinaus definiert das Modul aus welchen JAR-Dateien es besteht. In diesem Fall wird die Maven Property project.build.finalName genutzt, um den Namen des JAR-Files anzugeben, das durch den Maven Build erzeugt wird.
Dazu ist es notwendig in Maven das Resource-Filtering zu aktivieren:

<project>
    <!-- ... -->
    <build>
        <resources>
            <resource>
                <directory>src/main/docker</directory>
                <filtering>true</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
        <!-- ... -->
    </build>
        <!-- ... -->
</project>
Code-Sprache: HTML, XML (xml)

Deployment des Providers

Das Deployment erfolgt durch Kopieren der beiden Dateien in das modules-Verzeichnis des Keycloak- bzw. Wildfly-Servers. Nutzt man Keycloak auf Basis der offiziellen Docker Images, kann das im Rahmen der Erstellung eines Dockerfile wie folgt geschehen:

FROM jboss/keycloak:${version.keycloak}
COPY target/*.jar /opt/jboss/keycloak/modules/keycloak-spi-example/provider/main/
COPY target/classes/module.xml /opt/jboss/keycloak/modules/keycloak-spi-example/provider/main/  

Das Image kann nun mittels Docker gebaut werden. Es geht aber auch direkt innerhalb des Maven Builds wie folgt:

<project>
    <!-- ... -->
    <build>
        <!-- ... -->
        <plugins>
            <!-- ... -->
            <plugin>
                <groupId>io.fabric8</groupId>
                <artifactId>docker-maven-plugin</artifactId>
                <version>0.33.0</version>
                <configuration>
                    <images>
                        <image>
                            <name>${project.artifactId}:${project.version}</name>
                            <build>
                                <dockerFileDir>${project.basedir}</dockerFileDir>
                                <noCache>true</noCache>
                                <tags>
                                    <tag>latest</tag>
                                </tags>
                            </build>
                        </image>
                    </images>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>build</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
        <!-- ... -->
    <build>
    <!-- ... -->
<project>
Code-Sprache: HTML, XML (xml)

Der Provider ist damit in Keycloak deployed. Damit er jedoch genutzt wird, muss er noch registriert bzw. geladen werden. Dazu erstelle Wildfly Modul im Submodul keycloak-server als Provider ergänzt werden.
Dies erfolgt über ein „Command Line Interface (CLI)“-Skript beim Starten des Containers.

/subsystem=keycloak-server/:list-add(name=providers,value="module:keycloak-spi-example.provider")
Code-Sprache: PHP (php)

Das Skript registriert das Wildfly Modul als Provider. Keycloak wird daraufhin beim Starten das Modul nach möglichen SPI-Implementierungen durchsuchen und diese verfügbar machen.

Für die automatisierte Ausführung des CLI-Skripts, stellt das offizielle Docker Image einen Mechanismus bereit. Das Skript muss lediglich im Verzeichnis /opt/jboss/startup-skript/ hinterlegt werden. Das kann durch Mounten eines Volumes oder durch Kopieren des Skriptes in das Image erfolgen.
Im Rahmen des Maven Builds kann das Skript im Verzeichnis src/main/docker/startup-scripts liegen und mittels des Dockerfiles wie folgt in das Image kopiert werden.

COPY target/classes/startup-scripts/* /opt/jboss/startup-scripts/

Nach einem erneuten Build ist das Docker Image einsatzbereit.
Dazu zunächst den noch laufenden Docker Container stoppen, mit Maven erneut den Build starten und anschließend den Container mit dem neuen Image wieder startet.

docker stop keycloak && docker rm keycloak
mvn clean install
docker run --name keycloak -d -p 8080:8080 -e KEYCLOAK_USER=test -e KEYCLOAK_PASSWORD=test keycloak-spi-example

Auf der Server Info Page sollte nun neben dem default-Provider zusätzlich der neue Provider mit Namen example-email erscheinen.

Aktivierung des Providers

Der Keycloak Server verfügt nun über zwei Implementierungen der gleichen SPI. Es stellt sich daher die Frage, welche Implementierung herangezogen wird, wenn die SPI genutzt werden soll.

Hierzu stellt Keycloak die Möglichkeit zu Aktivierung einzelner Provider im Submodul keycloak-server bereit. Dazu wird das CLI-Skript einfach wie folgt erweitert.

/subsystem=keycloak-server/spi=emailSender:add
/subsystem=keycloak-server/spi=emailSender/provider=email-prefix:add(enabled=true)
/subsystem=keycloak-server/spi=emailSender:write-attribute(name=default-provider, value=email-prefix)
Code-Sprache: JavaScript (javascript)

Im ersten Schritt wird die SPI emailSender hinzugefügt. Anschließend wird der neue Provider für die SPI ergänzt und aktiviert (enabled=true). Zu guter letzt setzt das Skript den Standard-Provider auf den neuen Provider. Das sorgt dafür, dass der Provider immer herangezoge wird, wenn aus Keycloak heraus eine E-Mail versandt werden soll.

Logging

Beim Starten des Containers ist der Provider nun aktiviert und wird standardmäßig verwendet. Zur Prüfung wird zunächst einmal Logging in den Provider eingebaut.

Keycloak nutzt dazu JBoss-Logging. Daher muss die entsprechende Dependency in der pom.xml für den Maven Build ergänzt werden.

<project>
    <!-- ... -->
    <dependencies>
        <!-- ... -->
        <dependency>
            <groupId>org.jboss.logging</groupId>
            <artifactId>jboss-logging</artifactId>
            <version>3.4.1.Final</version>
            <scope>provided</scope>
        </dependency>
        <!-- ... -->
    </dependencies>
    <!-- ... -->
</project>
Code-Sprache: HTML, XML (xml)

Zusätzlich ist es notwendig, dass entsprechende Wildfly Modul in der module.xml zu referenzieren.

<module>
    <!-- ... -->
    <dependencies>
        <!-- ... -->
        <module name="org.jboss.logging"/>
    </dependencies>
</module>
Code-Sprache: HTML, XML (xml)

Anschließend kann in der Implementierung des Providers das Logging erfolgen.

public class EmailPrefixSenderProvider implements EmailSenderProvider {
  private static final Logger logger = Logger.getLogger(EmailPrefixSenderProvider.class);
  public void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
    logger.info("Sending email");
  }
  // ...
}
Code-Sprache: JavaScript (javascript)

Nach erneutem Build und einem Neustart des Containers, ist es möglich die Nutzung des Providers im Logfile zu beobachten.
Mittels docker logs -f keycloak kann das Logile verfolgt werden. Zum Testen wird in der Administrationskonsole dem angemeldeten Test-Benutzer über den Menüpunkt Users eine E-Mail-Adresse zugewiesen. Dazu den Benutzer suchen, editieren, die E-Mail-Adresse im Feld Email eintragen und die Option Email Verified aktivieren und anschließend speichern.

Zuweisung E-Mail Adresse Testuser
Konfiguration der E-Mail für den Benutzer

Im Anschluss im Menü Realm Settings unter Email auf Test connection klicken. Im Log sollte der entsprechende Eintrag „Sending email“ erscheinen.

Nutzung weiterer vorhandener Provider

Nun fehlt noch die Implementierung des eigentlichen Versands der E-Mail. Hierzu kann auf die Keycloak default-Implementierung zurückgegriffen werden. Das kann auf eine von zwei Arten erfolgen.

Variante 1 – direkte Instanzierung

In der ersten Variante wird der default-Provider einfach instanziiert. Die dazu notwendige Java-Klasse org.keycloak.email.DefaultEmailSenderProvider befindet sich im Keycloak-Modul keycloak-services. Dieses wird daher zunächst als Maven Dependency in der pom.xml ergänzt.
Darüber hinaus muss auch diesmal in der module.xml das entsprechende Modul referenziert werden.

<!-- pom.xml -->
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-services</artifactId>
    <version>${version.keycloak}</version>
    <scope>provided</scope>
</dependency>
<!-- module.xml -->
<module name="org.keycloak.keycloak-services"/>
Code-Sprache: HTML, XML (xml)

Anschließend kann der Provider instanziiert und aufgerufen werden.

public class EmailPrefixSenderProvider implements EmailSenderProvider {
  private final KeycloakSession session;
  EmailPrefixSenderProvider(KeycloakSession session) {
    this.session = session;
  }
  public void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
    String prefixedSubject = "KEYCLOAK: " + subject;
    EmailSenderProvider defaultProvider = new DefaultEmailSenderProvider(session);
    defaultProvider.send(config, user, prefixedSubject, textBody, htmlBody);
  }
}
Code-Sprache: JavaScript (javascript)

Dazu benötigt der DefaultEmailSenderProvider eine KeycloakSession, über die er die notwendigen Einstellungen für den SMTP-Server sowie Absenderadresse etc. ermitteln kann. Diese Informationen entsprechen den Einstellungen in der Administrationskonsole unter Realm Settings im Unterpunkt Email. Die KeycloakSession erhält der EmailPrefixSenderProvider ebenfalls über seinen Konstruktor. Entsprechend muss die EmailPrefixSenderProviderFactory angepasst werden.

public EmailSenderProvider create(KeycloakSession session) {
    return new EmailPrefixSenderProvider(session);
}
Code-Sprache: PHP (php)

Alternativ könnte eine Instanz des DefaultEmailSenderProvider auch über die zugehörige Factory mittels new DefaultEmailSenderProviderFactory().create(session); erzeugt werden.

Variante 2 – Provider Lookup

Keycloak nutzt diese Provider-Factories selbst auch, um entsprechende Instanzen zu erzeugen. Dabei stehen über das Interface an der org.keycloak.provider.ProviderFactory weitere Methoden, wie z.B. init oder postInit, zur Verfügung, die hauptsächlich der weiteren Konfigration der Provider dienen.
Es empfiehlt sich daher grundsätzlich eher einen Provider Lookup in Keycloak zu machen, anstatt eine direkte Instanzierung vorzunehmen. Der Provider Lookup erfolgt über die KeycloakSession wie folgt:

session.getProvider(EmailSenderProvider.class, "default");
Code-Sprache: CSS (css)

Dabei gibt der erste Parameter (EmailSenderProvider.class) die Art des Providers an, der zweite Parameter („default“) referenziert den Namen der Provider-Implementierung, die genutzt werden soll. Die KeycloakSession stellt darüber hinaus viele weitere Methoden für den Zugriff auf unterschiedlichste Provider bereit. Hier empfiehlt sich grundsätzlich ein Blick in die Java-Dokumentation.

Konfiguration

Der Mechanismus, der über die ProviderFactory bereitgestellt wird, kann auch für die Konfiguration eigener Provider genutzt werden. Als Beispiel dient das Präfix für die Betreffzeile der E-Mail. Diese ist bislang hart als „KEYCLOAK: “ kodiert.
Um sie zu konfigurieren, wird der Provider erweitert, so dass das Präfix über den Konstruktor an den Provider übergeben werden kann.

public class EmailPrefixSenderProvider implements EmailSenderProvider {
  private final KeycloakSession session;
  private final String subjectPrefix;
  EmailPrefixSenderProvider(KeycloakSession session, String subjectPrefix) {
    this.session = session;
    this.subjectPrefix = subjectPrefix;
  }
  public void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
    String prefixedSubject = subjectPrefix + subject;
    // ...
  }
Code-Sprache: JavaScript (javascript)

Der Wert wird analog zu KeycloakSession über die Factory ermittelt. Hierzu kann mittels der init-Methode in der Factory auf die Konfiguration der Provider-Implementierungen zugegriffen werden.

public class EmailPrefixSenderProviderFactory implements EmailSenderProviderFactory {
    private static final String CONFIG_SUBJECT_PREFIX = "subjectPrefix";
    private Config.Scope config;
    public EmailSenderProvider create(KeycloakSession session) {
        String subjectPrefix = config.get(CONFIG_SUBJECT_PREFIX);
        return new EmailPrefixSenderProvider(session, subjectPrefix);
    }
    public void init(Config.Scope config) {
        this.config = config;
    }
    // ...
Code-Sprache: PHP (php)

Die Provider-Factory definiert nun, das unter dem Schlüssel subjectPrefix das entsprechende Präfix in der Konfiguration zu finden ist.
Letztlich wird der Konfigurationseintrag über CLI-Skripte im Keycloak-Subsystem für die EmailSenderProvider-SPI vorgenommen. Dazu ist lediglich eine Anpassung im bereits vorgestellten CLI-Skript notwendig. Die Zeile, die den email-prefix-Provider hinzufügt, wird um das entsprechende Property in der Konfiguration erweitert.

/subsystem=keycloak-server/spi=emailSender/provider=email-prefix:add(properties={subjectPrefix => "SSO: "},enabled=true)
Code-Sprache: PHP (php)

Nach dieser Konfigurationsänderung werden nun alle E-Mails mit einem Präfix „SSO: “ im Betreff verschickt.
Auf diese Art und Weise können SPIs konfiguriert werden. Neben einfach Strings, werden auch Integers, Listen und boolesche Werte unterstützt. Darüber hinaus ist es ebenfalls möglich Ausdrücke zu nutzen, um System Properties (${subjectPrefix}) oder Umgebungsvariablen (${env.SUBJECT\_PREFIX_}) aufzulösen und Standardwerte zu setzen.

/subsystem=keycloak-server/spi=emailSender/provider=email-prefix:add(properties={subjectPrefix => "${env.SUBJECT_PREFIX:KEYCLOAK}: "},enabled=true)
Code-Sprache: PHP (php)

So liest die zuvor genannte Konfiguration beispielsweise die Umgebungsvariable SUBJECT_PREFIX. Ist diese nicht gesetzt, wird das Präfix auf KEYCLOAK gesetzt. So ist wie in vielen Container-Umgebungen heute üblich auch die Konfiguration über Umgebungsvariablen prinzipiell möglich.

Zusammenfassung / Fazit

Das vorgestellte Beispiel zeigt wie einfach Erweiterungen mittels Service Provier Interfaces in Keycloak möglich sind. Zugegebenermaßen ist das Beispiel recht trivial gewählt. Die Vielfalt an vorhandenen SPIs, wie in der Server Info Page gesehen, lässt weitaus umfangreichere Anpassungsmöglichkeiten erahnen. Dabei folgen solche Anpassungen in der Regel den vorgestellten Mustern. Dazu zählt im Kern die Implementierung der SPI durch einen konkreten Provider mit zugehöriger Factory. Ebenso ist die Registrierung der SPI und deren Konfiguration vor allem in komplexeren Szenarien zumeist notwendig. Gleiches gilt für Aspekte wie Logging oder die Nutzung vorhandener Provider über entsprechende Lookups. Sie bilden daher die Grundlagen für eigene Erweiterungen eines Keycloak-Systems. Viel Spaß beim Ausprobieren!

Dieser Artikel erschien zuerst in der Ausgabe OKT-2020 im JavaPRO-Magazin

1 Gedanke zu „Keycloak SPIs implementieren – Schritt für Schritt“

Schreibe einen Kommentar

Das könnte Dich auch noch interessieren

Durchstarten mit Keycloak und Docker

Durchstarten mit Keycloak und Docker

In diesem Beitrag erfahren Sie, wie Sie Keycloak mithilfe von Docker ausführen und konfigurieren können. Wir erläutern die Bedeutung von ...
Jenkins, Jenkins: Don't Repeat Yourself (DRY)!

Jenkins, Jenkins: Don’t Repeat Yourself (DRY)!

In modernen Microservice-Architekturen wird Software vollautomatisch über CI/CD-Pipelines ausgeliefert. Wie lassen sich diese Pipelines bei wachsender Komplexität und steigender Anzahl ...
Wie Pair Programming variiert werden kann

Wie Pair Programming variiert werden kann

Gibt es mögliche Variationen, die es für das Team effektiver machen können? Ja, die gibt es! Und ich möchte nachfolgend ...