15. Januar 2024 von Alexander Böhm
Keycloak-Login über Kundennummer ermöglichen
Was ist Keycloak?
Keycloak basiert auf den Standards OAuth 2.0 und OpenID Connect und ermöglicht die Integration einer zentralen Authentifizierungs- und Autorisierungsschicht in Anwendungen. Mit Keycloak können Accounts verwaltet, deren Identitäten über verschiedene externe Identity Provider (IdPs) validiert und Single Sign-On (SSO) für mehrere Anwendungen bereitgestellt werden. Keycloak unterstützt verschiedene Authentifizierungsmethoden wie Benutzername/Passwort, Social Login über Plattformen wie Google, Facebook und GitHub, SAML 2.0 und mehr. Keycloak bietet auch die Möglichkeit, benutzerdefinierte Attribute zu definieren und zu verwenden, um zusätzliche Informationen zu Konten zu speichern.
Möchtet ihr mehr über die Grundlagen von Keycloak erfahren, dann empfehle ich euch den Blog-Beitrag zum Thema „Verwaltung von Zugriffsrechten mit Keycloak als IAM-System“.
Eine der größten Stärken von Keycloak ist seine Flexibilität und Erweiterbarkeit. Und genau hier setzt dieser Blog-Beitrag an. Standardmäßig erlaubt Keycloak nur die Anmeldung per Benutzername und/oder E-Mail. Was aber, wenn das nicht ausreicht und ein anderes eindeutiges Merkmal als Login verwendet werden soll, wie beispielsweise eine Telefon- oder Kundennummer?
Grundlegendes
Sollen sich Accounts über einen Browser bei einer durch Keycloak gesicherten Applikation anmelden, so stellt Keycloak hierfür einen entsprechenden „Browser-Flow“ zur Verfügung. Dieser Flow präsentiert eine „UsernamePasswordForm“, in der die Anmeldedaten - im Standard: Benutzername beziehungsweise E-Mail und das Passwort - eingegeben werden und im Erfolgsfall der Account als authentifiziert an die gesicherte Applikation weitergeleitet wird.
Wollen wir nun dafür sorgen, dass sich Accounts auch mit einer weiteren eindeutigen Information wie einer Kundennummer anmelden können - da das Projekt diese zum Beispiel durch eine Altdatenmigration als Anforderung vorliegen hat - so müssen wir diese „UsernamePasswordForm“ um unsere spezifische Logik erweitern.
Testbenutzer mit Kundennummer anlegen
Für das Beispiel habe ich vorab einen User über die Keycloak Admin Oberfläche angelegt und diesem eine Kundennummer als Attribut zugewiesen:
Mit dieser Kundennummer können wir uns nach den unten beschriebenen Anpassungen und Erweiterungen anmelden. In einem realen Projekt sollte diese Kundennummer bei der Registrierung automatisch mit dem Account verknüpft werden, entweder durch Keycloak oder durch ein externes System.
Erweiterung der "UsernamePasswordForm
Diese Form wird im Browser-Authentication-Flow verwendet, um die Login-Felder für Benutzername/E-Mail auszuwerten.
An dieser Stelle wollen wir ansetzen und sicherstellen, dass auch eine gültige Kundennummer in Verbindung mit dem richtigen Passwort zu einem erfolgreichen Login-Versuch führt. Dazu stellt Keycloak die Methode „UsernamePasswordForm“ zur Verfügung. Diese können wir erweitern, indem wir die Methode „validateUserAndPassword“ aus der abstrakten Klasse „AbstractUsernameFormAuthenticatot“ überschreiben. Dies gibt uns die Möglichkeit, in die Validierung der eingegebenen Werte aus der Anmeldemaske einzugreifen. So können wir hier prüfen, ob der Anmeldename eine existierende Kundennummer ist. Dazu suchen wir im aktuellen Realm nach allen Accounts, die diesen Wert als Attribut in ihrem Account gespeichert haben:
List<UserModel> users = context.getSession().users()
.searchForUserByUserAttributeStream(
context.getRealm(), "customer_number", username)
.toList();
Es muss damit gerechnet werden, dass gegebenenfalls mehrere Konten mit der Kundennummer gefunden werden, wenn dies vorher nicht korrekt unterbunden wurde. Dieser Fall ist entsprechend zu behandeln. Wird kein Konto gefunden, kann geprüft werden, ob der eingegebene Wert ein Benutzername ist:
user = context.getSession().users()
.getUserByUsername(context.getRealm(), username);
Wenn auch hier kein Account gefunden wird, kann als letztes überprüft werden, ob es sich bei dem Wert um eine E-Mail handelt. Aber Achtung: Es kann nur dann nach der E-Mail gesucht werden, wenn wir die entsprechende Einstellung im Realm aktiviert haben und somit ein Login per E-Mail erlaubt ist:
if (user == null && context.getRealm().isLoginWithEmailAllowed()) {
user = context.getSession().users()
.getUserByEmail(context.getRealm(), username);
}
Wenn wir immer noch kein Konto gefunden haben, können wir mit Sicherheit sagen, dass der eingegebene Wert weder als Kundennummer noch als Benutzername oder E-Mail existiert. In diesem Fall können wir die Anmeldung abbrechen und eine entsprechende Fehlermeldung auf der Anmeldeseite anzeigen:
setInvalidUserError(context);
return false;
private void setInvalidUserError(AuthenticationFlowContext context) {
Response challengeResponse = challenge(context, Messages.INVALID_USER);
context.failureChallenge(
AuthenticationFlowError.INVALID_USER, challengeResponse);
}
Wenn wir einen Account gefunden haben, müssen wir noch dafür sorgen, dass das Passwort entsprechend validiert wird. Dies geschieht normalerweise in der überschriebenen Methode der Superklasse. Würden wir das hier nicht machen, wäre der User erfolgreich angemeldet, auch wenn er ein ungültiges Passwort für den Account eingegeben hat.
if (user == null || !validatePassword(context, user, inputData, false)) {
setInvalidUserError(context);
return false;
}
Es ist wichtig, dass wir sowohl für ein falsches Passwort als auch für einen falschen Benutzernamen die gleiche Fehlermeldung verwenden. Damit verhindern wir, dass ein Angreifer über die Fehlermeldungen herausfindet, ob er einen gültigen Account gefunden hat.
Wenn auch das Passwort gültig ist, können wir davon ausgehen, dass sich der User erfolgreich angemeldet hat:
context.setUser(user);
return true;
Die gesamte Anpassung der “UsernamePasswordForm“ ist nochmal hier zu finden.
Konfiguration des Authentication-Flows im Keycloak
Doch bevor diese Änderungen im Keycloak wirksam werden, ist es notwendig, die Factory-Klasse für unsere Anpassungen in einer MANIFEST-Datei bereitzustellen. Dazu erzeugen wir die folgende Datei und ergänzen in dieser unsere Factory:
de.adesso.CustomerNumberAuthenticatorFactory
Außerdem müssen wir die Konfiguration des Browser-Flows anpassen. Dazu kopieren wir den Default Browser-Flow und passen die Kopie wie folgt an, damit unser neues Formular verwendet wird:
Die Kopie muss als neuer Standard für den Browser-Flow eingestellt werden. Nun ist alles vorbereitet, damit sich die Konten auch über die hinterlegte Kundennummer einloggen können.
Test-Login über Kundennummer
Um unsere Anpassungen zu testen, können wir einfach den Link zur Keycloak Accountverwaltung (http://localhost:8080/realms/adesso-blog/account/) aufrufen und versuchen, uns dort mit der hinterlegten Kundennummer des Testaccounts anzumelden:
Wenn dies erfolgreich war, können wir nun auf die entsprechenden Menüs der Kontoverwaltung zugreifen und Änderungen an unserem Profil vornehmen.
Hinweis:
Damit hier nicht nur eine „Forbidden“-Meldung angezeigt wird, muss der User die Rolle „manage-account“ und der Account-Client die vorhandenen Client-Scopes haben. In beiden Fällen wurde der User jedoch erfolgreich über seine Kundennummer eingeloggt (authentifiziert).
Fazit
Mit dieser einfachen Erweiterung des Keycloak haben wir es möglich gemacht, dass sich Accounts zusätzlich zu den Standard-Login-Merkmalen mit projektspezifischen Merkmalen anmelden können. Dies kann zum Beispiel statt einer Kundennummer auch eine Telefonnummer sein.
Das gesamte Beispielprojekt ist unter folgendem Link noch einmal zu finden: https://github.com/boehmalex/keycloak-customer-number-login. Das Projekt basiert auf der bei Redaktionsschluss aktuellen Version 23.0.3 von Keycloak.