Keycloak-Integration leicht gemacht: Ihre Benutzerdatenbank als Custom User Provider nutzen

Eine weit verbreitete Lösung für Identitäts- und Zugriffsverwaltung ist Keycloak. Mit diesem Open Source Tool lassen sich eigene Applikationen und Services ohne großen Aufwand absichern und bietet dabei starke Authentifizierung, Benutzerverwaltung, feinkörnige Autorisierung und mehr.
Aber was ist, wenn Sie bereits eine vorhandene Nutzerdatenbank anschließen wollen? Auch das ist möglich. Für ein bestehendes Active Directory oder LDAP-Server bietet Keycloak Adapter "out of the box" an. Doch wie man einen eigenen Provider gegen bestehende Datenbanken oder APIs implementiert, möchte ich hier kurz vorstellen.
Warum eine zentrale Zugriffsverwaltung wie Keycloak?
Gründe für die Absicherung der eigenen Kundenportale, Supportsysteme oder Dashboards gibt es wie Sand am Meer, doch wann kann es Sinn machen, dieses Thema durch ein zentrales System wie Keycloak zu lösen?
Einer der Hauptgründe ist der Single-Sign-On (kurz: SSO), welcher den Komfort bietet, mit nur einem Login alle für den Nutzer/die Nutzerin freigegebenen Systeme erreichbar zu machen. Damit können Kunden und Mitarbeiter schneller arbeiten und sind nicht durch repetitive Login Anfragen frustriert. Zudem ist es mit einer zentralen Lösung einfacher, Nutzern moderne Authentifizierungsverfahren wie Passkeys anzubieten, ohne diese in jeder Applikation einzeln implementieren zu müssen.
Die Orchestrierung an einer Stelle bietet außerdem einen geringeren administrativen Overhead und potenziell schnellere Reaktionszeiten und höhere Sicherheit, da nur ein System gewartet, aktualisiert und überwacht werden muss.
Technische Umsetzung des Custom User Provider
Im Folgenden erläutere ich die technische Umsetzung des Custom User Providers für Keycloak. Falls Sie die technischen Details überspringen möchten, können Sie hier direkt zum Fazit gelangen.
Den Ausgang dafür bildet ein neues Gradle (oder Maven) Java Projekt, das mindestens die folgenden Abhängigkeiten benötigt:
implementation 'org.keycloak:keycloak-server-spi:26.0.7'
implementation 'org.keycloak:keycloak-server-spi-private:26.0.7'
implementation 'org.keycloak:keycloak-model-jpa:26.0.7'
Zusätzlich können hier weitere Abhängigkeiten für den konkreten Anwendungsfall wie Datenbank-Connectoren, falls direkt auf die Nutzerdatenbank zugegriffen wird, oder spezielle API Bibliotheken eingebunden werden. Die drei Keycloak-Pakete sind für unser Beispiel aber zwingend erforderlich.
Implementierung der eigenen UserStorageProviderFactory
Dies ist die Klasse, über die Keycloak unserer UserProvider findet und aufruft. Mindestens die create und getID Methoden müssen hier implementiert werden. Die Methode getID gibt den Namen zurück, unter der wir den Custom User Provider letztlich im Admin Backend finden und create liefert die eigentliche Implementierung des UserStorageProviders, welchen wir im nächsten Schritt definieren.
public class MyCustomUserStorageProviderFactory implements UserStorageProviderFactory<MyCustomUserStorageProvider> {
public static final String PROVIDER_ID = "custom-user-storage";
@Override
public MyCustomUserStorageProvider create(KeycloakSession session, ComponentModel model) {
return new MyCustomUserStorageProvider(session, model);
}
@Override
public String getId() {
return PROVIDER_ID;
}
}
Implementierung des UserStorageProvider
Hier steht uns frei diverse Interfaces zu implementieren, je nach gewünschter Funktionalität. In diesem Beispiel beschränken wir uns auf eine read-only Lösung, sprich Keycloak kann Nutzer aus unserer Legacy-Nutzerdatenbank lesen und gegen diese validieren, speichert sie aber nicht selbst und kann keine Änderungen zurück an das Altsystem geben. Technisch ist auch das umsetzbar, sprengt aber den Rahmen dieses Einblicks.
Für die Umsetzung der read-only Lösung müssen wir vier Interfaces Implementieren: UserStorageProvider, UserLookupProvider, CredentialInputValidator und UserQueryProvider.
Das UserStorageProvider Interface weist uns lediglich gegenüber Keycloak als eben solchen aus und es bedarf nur der Implementierung einer close Methode. In der Implementierung der drei Anderen liegt die eigentliche Anbindung an die eigene Nutzerdatenbank. Der UserLookupProvider regelt die Abfrage von Nutzern über die Id, Nutzername und E-Mail.
public class MyCustomUserStorageProvider implements UserStorageProvider,
UserLookupProvider,
CredentialInputValidator,
UserQueryProvider {
public static final String BASE_URL = "http://api.legacy-system/keycloakInterface/";
private final KeycloakSession session;
private final ComponentModel model;
public MyCustomUserStorageProvider(KeycloakSession session, ComponentModel model) {
this.session = session;
this.model = model;
}
/* UserLookupProvider */
@Override
public UserModel getUserById(RealmModel realm, String id) {
String persistenceId = StorageId.externalId(id);
return getUserByUsername(realm, persistenceId);
}
@Override
public UserModel getUserByUsername(RealmModel realm, String username) {
String urlString = BASE_URL + "getByUsername?username=" + username;
CustomUserEntity entity = getUserEntity(urlString, realm);
if (entity == null) {
return null;
}
return entity;
}
@Override
public UserModel getUserByEmail(RealmModel realm, String email) {
...
}
...
/* UserStorageProvider */
@Override
public void close() {
logger.info("close");
}
}
Das genaue Mapping der Nutzerdaten aus der Antwort abstrahieren wir hier aus Platzgründen. Wichtig dabei ist, dass Keycloak mindestens den vollen Namen sowie einen Nutzernamen und E-Mail erwartet. Beliebige weitere Attribute sind möglich, wenn diese für die Weiterverarbeitung benötigt werden, sind aber optional.
Als nächstes wird mittels des CredentialInputValidator die Validierung eines Login Versuchs abgewickelt. Dafür können wir in unserem Beispiel Nutzername und Passwort des Logins gegen unsere API schicken und erhalten im JSON-Body der Antwort ein Boolean-Flag, ob die gegebenen Anmeldeinformationen valide sind.
public class MyCustomUserStorageProvider implements UserStorageProvider,
UserLookupProvider,
CredentialInputValidator,
UserQueryProvider {
...
/* CredentialInputValidator */
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
StorageId sid = new StorageId(user.getId());
String username = sid.getExternalId();
// get password from input
UserCredentialModel cred = (UserCredentialModel)input;
String password = cred.getValue();
boolean success = false;
String urlString = BASE_URL + "isValid?username=" + username + "&password=" + password;
try {
JSONObject jsonResponse = getResponse(urlString);
if (jsonResponse != null) {
success = jsonResponse.getBoolean("success");
}
} catch (Exception e) {
e.printStackTrace();
}
return success;
}
...
}
Zu guter Letzt dient das UserQueryProvider Interface der paginierten Abfrage von Nutzern aus dem Altsystem zur Darstellung im Keycloak Backend. Die Implementierung dieses Interfaces ist für die Funktionalität nicht zwingend erforderlich, ermöglicht aber eine schnelle und einfache Übersicht aller angezogenen Nutzer und deren definierten Attribute.
public class MyCustomUserStorageProvider implements UserStorageProvider,
UserLookupProvider,
CredentialInputValidator,
UserQueryProvider {
...
/* UserQueryProvider */
@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> params, Integer firstResult, Integer maxResults) {
String search = params.get(UserModel.SEARCH);
String urlString = BASE_URL + "searchUsers?searchTerm=" + search;
if (firstResult != null) urlString += "&firstResult=" + firstResult;
if (maxResults != null) urlString += "&maxResults=" + maxResults;
JSONObject jsonResponse = getResponse(urlString);
JSONArray users = jsonResponse.getJSONArray("users");
return StreamSupport.stream(users.spliterator(), false)
.map(expertUserObj -> {
JSONObject expertUser = (JSONObject) expertUserObj;
return new CustomUserEntity.Builder(session, realm, model, expertUser.getString("login"))
.email(expertUser.getString("email"))
.firstName(expertUser.getString("firstname"))
.lastName(expertUser.getString("lastname"))
.build();
});
}
...
}
Einbindung in Keycloak
Damit haben wir es schon fast geschafft. Sobald das Projekt gebaut ist, müssen wir nur noch das resultierende jar in unsere Keycloak-Instanz unterbringen. In unserem Fall nutzen wir Keycloak als Docker-Container und können das Dockerfile wie folgt erweitern:
FROM quay.io/keycloak/keycloak:26.0.7
COPY build/libs/keycloak-user-storage-1.0-SNAPSHOT.jar /opt/keycloak/providers/
RUN /opt/keycloak/bin/kc.sh build
# Setze den Entwicklungsmodus als Standard, NICHT für Prod nutzen!
ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start-dev"]
In diesem ziehen wir das offizielle Keycloak Docker-Image (zum Zeitpunkt des Blogposts in Version 26.0.7) an. Nun kopieren wir das erzeugte jar in den Ordner /opt/keycloak/providers/ und bauen Keycloak mit dem folgenden Befehl neu, sodass unser eigener Provider registriert wird. Die letzte Zeile startet das System im Entwicklungsmodus, für die Nutzung in Produktion muss ein entsprechendes Dockerfile angelegt werden.
Wenn wir den Container nun starten und uns im Backend als Administrator einloggen, können wir unter dem Menüpunkt User federation unseren eigenen Provider auswählen und aktivieren. Wenn die API erreichbar ist, werden uns jetzt unter Users alle verfügbaren Nutzer aufgelistet. Diese Nutzer sind bereit, sich gegen die konfigurierten Clients zu authentifizieren, was wir im nächsten Teil dieser Reihe anhand eines Angular-Demo-Clients erläutern werden.
Welchen Nutzen ziehen Sie aus einem Custom User Provider für Keycloak?
Die Einführung von Keycloak bedeutet nicht, dass sofort alle Nutzerdaten in diesen migriert werden müssen. Bestehende Nutzerdatenbanken können ohne großen Aufwand über einen Custom User Provider angebunden werden. Dadurch lassen sich moderne Features wie SSO sofort nutzen, ohne dass eine zeitaufwendige Datenmigration erforderlich ist.
Durch die Integration profitieren Sie von einer zentralen Anlaufstelle für alle Logins, wodurch Ihre Anwender von mehr Komfort und einer reibungslosen Nutzererfahrung profitieren. Gleichzeitig bleibt Ihr bestehendes System funktionsbereit, sodass im ersten Schritt keine größeren Umstrukturierungen notwendig sind.
Wir helfen Ihnen gerne bei der Entwicklung und Absicherung Ihrer modernen Kundenservice-Portale. Lesen Sie hier, bei welchen Themen wir Sie auf dem Weg zu einem zukunftsweisenden Kundenservice begleiten können.