Keycloak Individualisierung


Keycloak hat sich als ein zuverlässiges Tool für Identität und Zugriffsrechte in der Industrie etabliert. Zumindest in der Java-Welt ist es aus vielen Projekten nicht mehr wegzudenken. Single Sign-On, 2 Faktor-Authentifizierung und LDAP-Integration sind in einem Tools vereint und lassen sich durch APIs und eine breite Unterstützung der Community kinderleicht in das eigene Projekt integrieren. Damit diese Integration auch in dem eigenen Corporate-Style und mit abgewandelten Prozessen erfolgen kann bietet Keycloak eine Reihe von Möglichkeiten der Customization. In diesem Artikel möchte ich einen kurzen Überblick geben wie man dabei vorgehen kann. Dies soll keine Dokumentation werden, sondern ein pragmatischer Leitfaden um lediglich die Konzepte zu verstehen um das eigene Projekt starten zu können. Ein lauffähiges Projekt gibt es in meinem Github.

Themes

Themes werden benutzt um Seiten wie die Anmeldemaske oder Emails zu individualisieren. Sie werden mit der Template-Engine Freemarker geschrieben. Ein neues Theme muss unter dessen Namen im Ordner themes/ wie beispielsweise themes/mytheme hinterlegt werden. Der Themes-Ordner selbst befindet sich im Installationspfad von Keycloak.

Im Keycloak Repository gibt es bereits zwei Themes. Das base-Theme enthält alle Freemarker Templates, jedoch ohne CSS-Dateien. Das keycloak-Theme wiederum leitet vom base-Theme ab und beinhaltet die nötigen Styles in Form von CSS-Dateien. Um nun ein eigenes Theme zu schreiben sollte man es genauso machen und in erster Linie die Styles überschreiben. Wenn nötig kann man die Templates natürlich ebenfalls anpassen.

Um beispielsweise die Anmeldemaske zu verändern legt man im Theme-Ordner ein Verzeichnis mit dem Namen login/ und der Datei theme.properties an.

parent=base
import=common/keycloak

styles=css/my_custom_style.css

customProperty=customValue

Die Properties-Datei enthält vier wichtige Properties:

  • parent: Das abzuleitende Theme. Wie bereits erwähnt besitzt das base-Theme keine Styles. Es kann der Einfachheit halber auch das keycloak-Theme abgeleitet werden.
  • import: Common-Resourcen aus einem anderen Theme.
  • styles: CSS-Dateien.
  • locales: Vom Theme unterstützte Sprachen.
  • scripts: JS-Dateien.

Alle anderen Properties können beliebig gesetzt und später innerhalb der Themes in den Freemarker-Templates benutzt werden. Das base-Theme macht davon bereits gebrauch. Mit dem Property kcHtmlClass lässt sich beispielsweise der Klassenname für den HTML-Tag setzen, welcher dann wiederum im CSS vom keycloak-Theme überschrieben werden kann.

Generell sollte man beim erstellen eines Themes auf die bereits existierenden schauen. Ein kurzer Blick und simples Copy-Paste sollte meistens genügen um sein eignes Theme zu erstellen.

Um ein Theme zu aktivieren wählt man dieses einfach unter dem Reiter Theme in den Realm-Settings aus.

Styles, Bilder und Skripte

Ressourcen wie CSS-Dateien oder Bilder werden im Ordner resources/ des jeweiligen Themes hinterlegt. Für unser Login wäre das beispielsweise themes/mytheme/login/resources/. Stylesheets werden, wie bereits beschrieben, über das Property styles festgelegt. Wer ein anderes Theme als Parent importiert hat kann in diesem Property auch auf die Styles anderer Themes zugreifen. Wichtig dabei ist, dass die Stylesheets der Reihe nach eingebunden werden. Die zuletzt referenzierte CSS-Datei kann so alle anderen Styles überschreiben.

styles=css/login.css css/custom_login.css

Bilder können wie gewohnt im CSS referenziert werden. Hier muss nur beachtet werden, dass wenn man sich im css/ Befindet eine Ebene tiefer gegangen werden muss.

.login-logo {
    background-image: url('../img/logo_blank.png');
}

Im Freemarker-Template ist der Ressourcen-Ordner über das Property url.resourcesPath erreichbar.

<img src="${url.resourcesPath}/img/logo_blank.jpg">

Alles was über das einbinden von CSS-Dateien zutrifft gilt ebenfalls für Skripte. Die JS-Dateien müssen jedoch über das Property scripts eingebunden werden.

Die Common-Ressourcen welche über das Property import eingebunden werden befinden sich im Ordner common/resources/ des jeweiligen Themes. Diese sind für login, email etc. zentral verfügbar und können auch durch ein anderes Theme importiert werden, wenn das Parent-Theme nicht abgeleitet wurde. In unserem Beispiel wird das mit den Styles für patternfly so gemacht.

stylesCommon=node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css lib/zocial/zocial.css

Übersetzungen

Übersetzungen werden als schlichte Key-Value-Paare in Property-Dateien abgelegt. Referenziert wird eine Sprache über deren Ländercode wie beispielsweise de. Dieser Ländercode muss im Property locales für das Theme hinterlegt werden.

Die entsprechende Datei wird dann mit der jeweiligen Endung im Ordner messages/ wie beispielsweise messages/messages_de.properties gespeichert.

Templates

Templates überschreibt man, indem man lediglich ein neues Template mit dem gleichen Namen und an der gleichen Ordner-Position wie das bereits existierende erstellt. Um die Login-Maske zu verändert genügt es die Datei /login/login.ftl zu erstellen. Alle verfügbaren Templates des base-Themes finden sich im Themes-Repository von Keycloak.

Das Deployment

Einige Dinge, wie beispielsweise Themes können direkt im Installationsordner von Keycloak abgelegt werden. Im Docker Image wäre das /opt/jboss/keycloak. Dies wäre für den professionellen Einsatz eher weniger sinnvoll. Keycloak erlaubt daher auch das Deployment über ein Jar-Archiv zu bewerkstelligen. Dies hat den Vorteil, dass nicht nur eigener Code sondern auch Themes und Übersetzungen zentral, beispielsweise in einem Maven-Projekt, hinterlegt werden können.

Das Maven-Projekt muss aus nichts weiter bestehen als einer simplen pom-Datei in der die Abhängigkeiten zur Keycloak-Bibliothek enthalten sind. Diese sind nötig um Klassen später überschreiben zu können.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.florianbuchner</groupId>
    <artifactId>keycloak_custom</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-core</artifactId>
            <version>11.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-services</artifactId>
            <version>11.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-themes</artifactId>
            <version>11.0.2</version>
        </dependency>
    </dependencies>

</project>

Nachdem die Jar gebaut wurde kann sie in den Deployment-Pfad von Keycloak /opt/jboss/keycloak/standalone/deployments/ kopiert werden.

Wird beispielsweise eine Datei mit dem Namen hallo-welt.jar in diesen Ordner kopiert, so wird nach erfolgreichem Deployment von Keycloak eine weitere Datei mit dem Namen hallo-welt.jar.deployed in diesem Ordner erstellt. Bei einem Fehler wird eine Datei mit dem Namen hallo-welt.jar.failed erzeugt. Um das Deployment rückgängig zu machen muss die Datei einfach gelöscht werden. Eine genaue Beschreibung des Deployments findet sich in der README.txt des Verzeichnisses aber auch in der JBoss-Dokumentation.

Services

In Keycloak ist es möglich Services zu überschreiben oder neue hinzuzufügen. Nahezu jegliche Logik in Keycloak wird über Services gesteuert. Ob das der Versandt von E-Mails oder die Logik für den Login ist ist dabei egal. Wer möchte kann wirklich alles verändern. Aber Vorsicht: Bei Keycloak handelt es sich um einen zentralen Baustein im Bezug auf die Sicherheit im System.

Alle bestehenden Services sind im Services-Projekt von Keycloak zu finden. Dieses Projekt eignet sich daher ausgezeichnet als Ratgeber um eigene Services zu implementieren. Als Beispiel implementieren wir einen Captcha-Service, welcher nach dem Login eine kurze Captcha-Abfrage startet.

Da es sich hierbei um einen Authenticator handelt erstellen wir eine Klasse welche von Authenticator ableitet und implementieren alle nötigen Methoden.

package com.florianbuchner.keycloakcustom.login;

import org.keycloak.authentication.AbstractFormAuthenticator;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;

import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.UUID;

public class CaptchaAuthenticator implements Authenticator {

    static final String FORM = "custom_captcha.ftl";
    static final String CAPTCHA_ATTRIBUTE = "CAPTCHA";
    static final String FORM_CAPTCHA = "captcha";
    static final String FORM_USER_CAPTCHA = "user_captcha";

    @Override
    public void authenticate(AuthenticationFlowContext authenticationFlowContext) {
        UserModel user = authenticationFlowContext.getUser();
        String captcha = generateCaptcha();
        user.setSingleAttribute(CAPTCHA_ATTRIBUTE, captcha);
        LoginFormsProvider form = authenticationFlowContext.form();
        form.setAttribute(FORM_CAPTCHA, captcha);
        Response challenge = form.createForm(FORM);
        authenticationFlowContext.challenge(challenge);
    }

    boolean isCaptchaCorrect(final String captcha, final MultivaluedMap<String, String> inputData) {
        return captcha != null && captcha.equals(inputData.getFirst(FORM_USER_CAPTCHA));
    }

    @Override
    public void action(AuthenticationFlowContext authenticationFlowContext) {
        UserModel user = authenticationFlowContext.getUser();

        if (isCaptchaCorrect(user.getFirstAttribute(CAPTCHA_ATTRIBUTE), authenticationFlowContext.getHttpRequest().getFormParameters())) {
            authenticationFlowContext.success();
        }
        else {
            String captcha = generateCaptcha();
            user.removeAttribute(CAPTCHA_ATTRIBUTE);
            user.setSingleAttribute(CAPTCHA_ATTRIBUTE, captcha);
            LoginFormsProvider form = authenticationFlowContext.form();
            form.setAttribute(FORM_CAPTCHA, captcha);
            form.setError("invalidCaptcha");
            Response challenge = form.createForm(FORM);
            authenticationFlowContext.challenge(challenge);
        }
    }

    String generateCaptcha() {
        return UUID.randomUUID().toString().substring(0, 5);
    }

    @Override
    public boolean requiresUser() {
        return true;
    }

    @Override
    public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {
        return true;
    }

    @Override
    public void setRequiredActions(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {

    }

    @Override
    public void close() {

    }
}

Wichtig hierbei sind die Methoden authenticate und action. authenticate wird ausgeführt, wenn der aktuelle Authenticator an der Reihe ist um beispielsweise die Captcha-Maske anzuzeigen. action wird ausgeführt sobald mit dem Authenticator interagiert werden soll, nachdem der Benutzer das Captcha abgetippt hat.

Als Captcha-Template dient eine simple HTML-Seite, auf welcher der Benutzer das Captcha eingeben kann.

<#import "template.ftl" as layout>
<@layout.registrationLayout displayWide=(realm.password); section>
    <#if section = "header">
        ${msg("enterCaptcha")}
        <div class="login-logo"/>
    <#elseif section = "form">
        <div id="kc-form">
            <div id="kc-form-wrapper">
                <form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}" method="post">

                    <div class="${properties.kcFormGroupClass!}">
                        <label for="username" class="${properties.kcLabelClass!}">${msg("captcha")}</label>
                        <span>${captcha}</span>
                    </div>

                    <div class="${properties.kcFormGroupClass!}">
                        <label for="username" class="${properties.kcLabelClass!}">${msg("enterCaptcha")}</label>
                        <input tabindex="1" id="username" class="${properties.kcInputClass!}" name="user_captcha" type="text" autofocus autocomplete="off" />
                    </div>

                    <div id="kc-form-buttons" class="${properties.kcFormGroupClass!}">
                        <input tabindex="4" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/>
                    </div>
                </form>
            </div>
        </div>
    </#if>

</@layout.registrationLayout>

Jeder Service benötigt eine Factory. Diese erzeugt den Service und liefert Informationen wie Name, Hilfstext oder mögliche Aktionen. Über die Factory kann der Service zudem konfiguriert werden.

package com.florianbuchner.keycloakcustom.login;

import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;

import java.util.ArrayList;
import java.util.List;

public class CaptchaAuthenticatorFactory implements AuthenticatorFactory {

    private static final CaptchaAuthenticator AUTHENTICATOR = new CaptchaAuthenticator();

    private static final AuthenticationExecutionModel.Requirement[] REQUIREMENTS = {
            AuthenticationExecutionModel.Requirement.DISABLED,
            AuthenticationExecutionModel.Requirement.REQUIRED
    };

    @Override
    public String getDisplayType() {
        return "Captcha Authenticator";
    }

    @Override
    public String getReferenceCategory() {
        return null;
    }

    @Override
    public boolean isConfigurable() {
        return false;
    }

    @Override
    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
        return REQUIREMENTS;
    }

    @Override
    public boolean isUserSetupAllowed() {
        return false;
    }

    @Override
    public String getHelpText() {
        return "Custom captcha Authenticator";
    }

    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return new ArrayList<>();
    }

    @Override
    public Authenticator create(KeycloakSession keycloakSession) {
        return AUTHENTICATOR;
    }

    @Override
    public void init(Config.Scope scope) {

    }

    @Override
    public void postInit(KeycloakSessionFactory keycloakSessionFactory) {

    }

    @Override
    public void close() {

    }

    @Override
    public String getId() {
        return "captcha-authenticator";
    }
}

Nachdem die Factory angelegt wurde muss Keycloak noch mitgeteilt werden wo diese zu finden ist. Dies geschieht über Textdateien welche den voll qualifizierten Klassennamen enthalten. In unserem Fall wäre das die Datei org.keycloak.authentication.AuthenticationFactory.

com.florianbuchner.keycloakcustom.login.CaptchaAuthenticatorFactory

Es gibt eine Vielzahl dieser Factory-Dateien. Bereits existierende Dateien mit den Standard-Services sind im Services-Projekt zu finden.

Das komplette Beispiel findet sich in meinem Github. Nun muss nur noch ein eigener Flow erstellt und dieser als Binding für den Browser angegeben werden. Nach dem Deployment sollte der Captcha-Service nun als mögliche Execution für diesen Flow erscheinen.

Debugging

Keycloak kann im Debug-Mode gestartet werden. Dadurch ist es möglich den eigenen Code per Remote-Debugging live zu testen. Wer seinen Keycloak über Docker startet kann hierzu dem Container die benötigten Umgebungsvariablen mitgeben und Ports exponieren.

docker run -p 8080:8080 -p 8787:8787 -e DEBUG=true -e DEBUG_PORT="*:8787" quay.io/keycloak/keycloak:11.0.2

Wie bereits erwähnt möchte ich nur kurz die Vorgehensweise bei der Individualisierung von Keycloak beschreiben. Eine genaue Beschreibung der einzelnen Methoden und Klassen findet sich in der offiziellen Doku oder im Sourcecode selbst.