Custom Keycloak User Storage Federation Provider
Stellt euch folgendes, eher ungewöhnliches Szenario vor: Ihr habt ein LDAP-System und parallel dazu ein Active Directory (AD) mit Kerberos Authentifizierung. Beide Systeme verfügen über die gleiche Nutzerbasis. Ihr möchtet nun, dass sich eure Nutzer über ihren AD Account bei Windows anmelden können (Kerberos) und danach automatisch für diverse Dienste und Webapps autorisiert sind. Aber: diese Autorisierung soll anhand der LDAP Gruppen und Rollen des Nutzers durchgeführt werden.
Klingt inszeniert? Ja, ist es aber nicht. Aus gegebenem Anlass möchten wir euch heute unseren Lösungsansatz zu dieser Problemstellung vorstellen.
Keycloak
Keycloak ist eine Identity and Access Management (IAM) Lösung von Red Hat. Sie ist komplett in Java geschrieben, Open-Source und läuft typischerweise in einem WildFly Application Server (ehemals JBoss Application Server), welcher ebenfalls in Java geschrieben und Open Source ist.
Keycloak wird gerne als Single Sign-On (SSO) Lösung eingesetzt und kann verschiedene Authentifizierungssysteme miteinander verbinden. Außerdem unterstützt Keycloak von Haus aus sowohl das LDAP, als auch das Kerberos Protokoll und bietet sich daher zur Lösung der obigen Problemstellung an.
Lösungsansatz
Keycloak enthält sogenannte User Storage Federation Provider für LDAP und für Kerberos. Wenn Nutzer- oder Authentisierungsanfragen ankommen leitet Keycloak diese an den entsprechenden Provider weiter. Dieser führt die Anfrage dem Protokoll entsprechend durch, verarbeitet das Ergebnis und gibt es zurück. Die Provider verstehen aber nur jeweils LDAP oder Kerberos und nicht beides gleichzeitig. Darum schreiben wir unseren eigenen Provider, der als eine Art Proxy fungiert und intern an die bestehenden Kerberos und LDAP Provider vermittelt. Eine Authentisierungsanfrage wird an den Kerberos Provider weitergeleitet und dort durchgeführt. Bei Erfolg nutzen wir den LDAP Provider, um uns die Nutzerberechtigungen zu holen und zurückzugeben.
Keycloak erweitern
Keycloak bietet diverse Service provider interfaces (SPI), die man implementieren und dann als eigenen Provider registrieren kann. Dafür müssen immer eine ProviderFactory und der Provider selber implementiert werden. Keycloak erstellt für jede Anfrage eine neue Instanz des Providers über die Factory, darum sollte der Provider möglichst leichtgewichtig sein. Da der Code für Keycloaks bestehende Provider natürlich öffentlich ist und insbesondere der Kerberos Provider intern genau so gebaut und registriert wird, wie ein eigener Provider, schauen wir uns dort nach einem ersten Ansatz um.
Die Factory sieht dann in etwa so aus:
public class KerberosLdapFederationProviderFactory implements UserStorageProviderFactory<KerberosLdapFederationProvider> {
...
@Override
public KerberosLdapFederationProvider create(KeycloakSession session, ComponentModel model) {
UserStorageProviderModel uspModel = new UserStorageProviderModel(model);
...
KerberosFederationProvider kerberosProvider = kerberosFederationProviderFactory.create(session, uspModel);
ldapStorageProviderFactory.init(null);
LDAPStorageProvider ldapProvider = ldapStorageProviderFactory.create(session, uspModel);
return new KerberosLdapFederationProvider(session, uspModel, this, kerberosProvider, ldapProvider);
}
...
}
In der create()
Methode erstellen wir mithilfe der bestehenden Factories direkt Instanzen der LDAP und Kerberos Provider und übergeben diese an unseren Provider, da wir sie später brauchen werden.
Der Provider muss dann das UserStorageProvider
Interface implementieren und zusätzlich noch ein paar andere, damit diverse Authentifizierungsmethoden und Validierungen unterstützt werden können:
public class KerberosLdapFederationProvider implements UserStorageProvider,
CredentialInputValidator,
CredentialAuthentication,
ImportedUserValidation {
...
}
Um nun die beschriebene Anforderung zu erfüllen ist insbesondere die authenticate()
Methode interessant:
@Override
public CredentialValidationOutput authenticate(RealmModel realm, CredentialInput input) {
//authenticate via kerberos but add user details of ldap to result
CredentialValidationOutput credResult = this.kerberosProvider.authenticate(realm, input);
if(credResult != null && credResult.getAuthenticatedUser() != null){
UserModel ldapUserModel = this.ldapProvider.getUserByUsername(
realm, credResult.getAuthenticatedUser().getUsername()
);
UserModel combinedUserModel = combineUserModels(
credResult.getAuthenticatedUser(), ldapUserModel
);
credResult = new CredentialValidationOutput(
combinedUserModel, credResult.getAuthStatus(), credResult.getState()
);
}
return credResult;
}
Hier leiten wir also die Anfrage einfach an den Kerberos Provider weiter, fragen bei Erfolg beim LDAP Provider nach den Nutzerdetails, kombinieren die Rückgabewerte dann miteinander und geben sie zurück. Aus Sicht von Keycloak wurde eine einzige Anfrage gestellt, gegen Kerberos authentifiziert und mit Details aus LDAP zurückgegeben.
Damit unser Provider (bzw. die eingebetteten Kerberos und LDAP Provider) mit den entsprechenden Systemen kommunizieren kann, brauchen wir noch ein paar Konfigurationsparameter. Diese werden in der Factory über die getConfigProperties()
Methode festgelegt:
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
private static List<ProviderConfigProperty> getConfigProps() {
List<ProviderConfigProperty> configProperties = kerberosFederationProviderFactory.getConfigProperties();
List<ProviderConfigProperty> additionalLdapConfig = ProviderConfigurationBuilder.create()
.property().name(LDAPConstants.USERNAME_LDAP_ATTRIBUTE)
.type(ProviderConfigProperty.STRING_TYPE).defaultValue("cn")
.label("username-ldap-attribute").helpText("username-ldap-attribute.tooltip")
.add()
.property().name(LDAPConstants.RDN_LDAP_ATTRIBUTE)
.type(ProviderConfigProperty.STRING_TYPE).defaultValue("cn")
.label("rdn-ldap-attribute").helpText("rdn-ldap-attribute.tooltip")
.add()
...
.build();
configProperties.addAll(additionalLdapConfig);
return configProperties;
}
Die Konfiguration des Kerberos Providers können wir einfach aus dessen Factory übernehmen. Leider nutzt der LDAP Provider intern noch ein eigenes HTML-Template, welches wir so nicht einbinden können und in dem die Übersetzungs-Labels gesetzt und eine etwas komplexere Nutzerschnittstelle gebaut wird. Darum müssen wir uns die benötigten Konfigurationsparameter aus dem Code selber zusammensuchen. Wichtig dabei ist, dass wir die richtigen LDAP Konstanten als Namen verwenden, damit der LDAP Provider diese zuordnen kann.
Insgesamt gehört natürlich noch ein bisschen mehr dazu, als die hier gezeigten Codeausschnitte, aber diese geben die wesentlichen Bestandteile wieder. Insbesondere müssen wir noch ein paar Mapper schreiben, welche die LDAP Daten nach unseren Vorstellungen auf Keycloak Daten mappen können. Dafür implementieren wir das LDAPStorageMapper
Interface und registrieren die Implementierung in unserer ProviderFactory.
Testsetup
Kennt ihr das? Man schreibt ein paar 100 Zeilen Code, stößt den Compiler an und alles funktioniert? Wir auch nicht... Um den ganzen Prozess testen zu können brauchen wir ein etwas komplexeres Testsetup, welches wir hier nur grob darstellen möchten.
Unsere Testumgebung läuft in einem Docker Compose Netzwerk und besteht aus Containern mit jeweils einem Keycloak Server (Docker Hub, einem LDAP-Server (GLAuth, Docker Hub und einem Kerberos KDC Server (Apache Kerby, https://github.com/coheigea/testcases/tree/master/apache/docker/kerby. Lokal installieren wir in unserem Ubuntu einen Kerberos Client (krb5-user, https://packages.debian.org/de/sid/krb5-user), den wir mit dem Kerberos Server verbinden und dann die Authentifizierung durchführen können. Zuletzt legen wir noch sowohl in Kerberos-, als auch im LDAP-Server unseren Testnutzer alice an.
Keycloak stellt eine kleine Webapp zur Verfügung, die wir für den SSO Test mit OpenID Connect nutzen können (https://www.keycloak.org/app/). Nachdem wir unseren Provider in Keycloak registriert und konfiguriert haben, müssen wir dort nur noch einen Client für diese Webapp anlegen und können testen.
Wir erstellen zunächst ein Kerberos Ticket für alice:
conventic@rocks:~$ kinit alice
Password for alice@EXAMPLE.COM:
conventic@rocks:~$ klist
Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: alice@EXAMPLE.COM
Valid starting Expires Service principal
03.12.2021 14:32:12 04.12.2021 14:32:12 krbtgt/EXAMPLE.COM@EXAMPLE.COM
renew until 04.12.2021 14:32:12
Dies simuliert sozusagen die Nutzeranmeldung bei Windows mit einem AD Account. Der Browser kann dieses Ticket dann automatisch aufnehmen (unter Linux muss der Browser ggf. noch konfiguriert werden, für Firefox muss z.B. der Konfigurationsparameter network.negotiate-auth.trusted-uris auf localhost gesetzt werden) und schickt es mit, wenn die Webapp sich über Keycloak authentisieren möchte.
Wir klicken auf Sign in...
...et voilà! Wir sind über Kerberos angemeldet und sehen die Nutzerdaten aus LDAP.