Ich bin es leid, mich für jede Anwendung wieder und wieder anmelden zu müssen. Wenn Sie diesen Satz auch schon gesagt haben, brauchen Sie Single Sign-on. Und wer „A“ sagt, muss auch „B“ sagen, d. h. Sie brauchen auch Single Log-out. Im Folgenden versuche ich einen kurzen Überblick, wie SSO und SLO für Jakarta-EE- und Quarkus-Anwendungen implementiert werden können, wobei wir als zentralen Identity Provider Keycloak nutzen.
Die Idee, das „Single“ zu implementieren, führt zur Nutzung eines zentralen IdP – Identity Provider -, der die eigentlichen An- und Abmeldevorgänge durchführt. Es gibt verschiedene Standards dafür. Wir nutzen hier OpenID Connect mit einem Keycloak als IdP. Es würde den Rahmen sprengen, Keycloak umfassend zu beschreiben, daher hier nur zwei seiner Konzepte:
- Ein Realm ist ein abgegrenzter Bereich, in dem Keycloak User, Passwörter, Rollen etc. ablegt. Alle Anwendungen, die von SSO profitieren sollen, müssen den gleichen Realm referenzieren.
- Ein Client wird in einem Realm angelegt und repräsentiert eine Anwendung. I. a. benötigt man für jede Anwendung einen Client.
Die Anwendungen sind Web-Anwendungen, d. h. sie können die zugehörigen Konzepte wie URLs, Query Parameter, Header, Cookies und Sessions nutzen. „Single“ setzt voraus, dass die Anwendungen aus einem Browser heraus bedient werden. Das Anmeldeverfahren verläuft bei diesen Anwendungen regelmäßig im sog. Authorization Code Flow, der stichwortartig so funktioniert:
- Der Anwender navigiert im Browser auf einen Teil der Anwendung, der eine Anmeldung erforderlich macht.
- Es erfolgt ein Redirect zum Keycloak, der einen Anmeldedialog durchführt.
- Nach erfolgreicher Anmeldung erfolgt ein Redirect zurück zur Anwendung, wobei ein einmalig und kurzzeitig nutzbarer Authorization Code mitgegeben wird.
- Die Anwendung wendet sich an den sog. Token Endpoint des IdP und tauscht den Code gegen Identity, Access und Refresh Tokens aus. ID und Access Tokens haben einer vergleichsweise kurze Gültigkeit (einige Minuten). Sie können mit Hilfe des Refresh Tokens erneuert werden. Die genaue Nutzung der Tokens ist im Folgenden nicht von Belang – üblicherweise nutzt eine Anwendung das ID Token für Informationen zum angemeldeten Nutzer und das Access Token als Berechtigungsstruktur.
- In Keycloak-spezifischen Cookies merkt sich Keycloak die validierte Identität des Benutzers (unabhängig vom ID Token der Anwendung). Sollte im aktuellen Browser später eine neue Authentifizierung notwendig sein – weil die Token abgelaufen sind oder weil eine neue Anwendung im Verbund genutzt wird -, kann der Keycloak den Nutzer ohne erneuten Anmeldedialog anmelden.
Für Jakarta-EE-10-Anwendungen kann eine CDI Bean im Application Scope zur Konfiguration der Anbindung an den IdP genutzt werden. Dazu stellt die Teilspezifikation Jakarta Security die Annotation @OpenIdAuthenticationMechanismDefinition zur Verfügung:
@ApplicationScoped
@OpenIdAuthenticationMechanismDefinition(
providerURI = "http://localhost:9090/realms/GEDOPLAN",
clientId = "jakarta-oidc-faces",
clientSecret = "insert-password-here",
redirectURI = "${baseURL}/post-login",
logout = @LogoutDefinition(redirectURI = "/post-logout")
)
public class SecurityConfig {
}
Die providerURI ist die Adresse des Realms im Keycloak, clientId und clientSecret sind die Anmeldeinformationen für den Client der Anwendung im Keycloak.
Etwas komplizierter wird es mit der redirectURI: Sie ist die Anwendungs-interne Adresse eines Servlets, das den o. a. Code-zu-Token-Tausch durchführt und danach auf die ursprüngliche Seite der Anwendung weiterleitet. Während das Token Handling im Standard bereits erledigt wird (resultierend in einem injizierbaren Objekt des Typs OpenIdContext, muss man den Redirect zur Ursprungsseite noch implementieren:
@WebServlet("/post-login")
public class PostLoginServlet extends HttpServlet {
@Inject
private OpenIdContext openIdContext;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String postLoginUri = this.openIdContext
.getStoredValue(request, response, OpenIdConstant.ORIGINAL_REQUEST)
.orElse("/");
response.sendRedirect(postLoginUri);
Noch komplizierter wird es für den Single Log-out. Hier ist ja die Idee, dass eine Abmeldung in einer Anwendung dazu führt, dass man auch in den anderen Anwendungen des SSO-Verbundes abgemeldet wird. Um dies zu erreichen, gibt es – mal wieder – verschiedene Verfahren. Wir wählen hier den sog. Backchannel Log-out, bei dem ein passender Endpunkt der Anwendung vom Keycloak aus aufgerufen werden kann, um eine Nutzerabmeldung in der Anwendung auszulösen. „Backchannel“ deshalb, weil der Aufruf vom Keycloak direkt auf die Anwendung erfolgt und nicht über den Browser – das wäre der „Frontchannel“. Die Backchannel Logout URL muss im Keycloak im entsprechenden Client konfiguriert werden.
Ein Log-out wird durchgeführt, indem eine der Anwendungen den Logout Endpoint des Keycloak aufruft. Alternativ kann man auch im Keycloak UI eine Session beenden. Keycloak ruft dann für jeden betroffenen Client dessen Backchannel Logout URL auf und übermittelt ein Logout Token. Im aufgerufenen Endpunkt müssen dann diese Schritte durchgeführt werden:
- Das Logout Token wird auf Gültigkeit überprüft. Dabei wird sichergestellt, dass das Token tatsächlich vom Keycloak kam, den richtigen Client betrifft und noch nicht abgelaufen ist.
- Im Logout Token ist eine SSO Session ID enthalten. Die zu ihr gehörenden Browser Sessions werden nun invalidiert.
Um die Zuordnung der SSO Session zu den Browser Sessions zu ermöglichen, muss man sich im oben gezeigten Login Servlet eine entsprechende Lookup-Tabelle aufbauen.
Aus Platzgründen kann der komplette Code des Backchannel Logout Servlets hier nicht aufgeführt werden, daher nur andeutungsweise:
@WebServlet("/backchannel-logout")
public class BackchannelLogoutServlet extends HttpServlet {
@Inject
OpenIdContext openIdContext;
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String logoutTokenString = request.getParameter("logout_token");
var claims = parseAndValidateToken(logoutTokenString);
String oidcSessionId = (String) claims.getClaim("sid");
// ... suche HttpSession zur oidcSession und invalidiere sie ...
response.setStatus(204);
Durch @OpenIdAuthenticationMechanismDefinition ist es also möglich geworden, JEE-Anwendungen an einen OpenID Connect Provider anzubinden, ohne die Konfiguration des genutzten Application Servers ändern zu müssen. Es bleibt aber doch noch eine Menge selbst zu implementieren. Das stellt sich für Quarkus-Anwendungen einfacher dar: Nach Einbinden der Extension io.quarkus:quarkus-oidc muss man „nur“ einige Properties setzen. Eine speziell annotierte CDI Bean wird nicht benötigt. Folgende Properties müssen gesetzt werden:
quarkus.oidc.auth-server-url=http://localhost:9090/realms/GEDOPLAN
quarkus.oidc.client-id=quarkus-oidc-faces
quarkus.oidc.credentials.secret=insert-password-here
quarkus.oidc.authentication.redirect-path=/post-login # (1)
quarkus.oidc.authentication.restore-path-after-redirect=true
quarkus.oidc.logout.path=/logout # (2)
quarkus.oidc.logout.post-logout-path=/index.xhtml
quarkus.oidc.logout.backchannel.path=/backchannel-logout # (3)
Die ersten Konfigurationswerte sind von oben bereits bekannt. Bemerkenswert sind die mit # (n) markierten URIs. Diese müssen nämlich nur deklariert werden und Quarkus (bzw. das Build Time Plugin) übernimmt die Implementierung der Endpunkte:
- Für den Redirect nach dem erfolgreichen Login im Keycloak wird ein Endpunkt (hier unter
/post-login) bereitgestellt, der den Code-zu-Token-Tausch ausführt und zur ursprünglichen Anwendungsseite weiterleitet. - Die Anwendung kann den angegebenen Logout Path (hier
/logout) für eine Abmeldung aufrufen. Das führt dann automatisch zum Aufruf des Logout Endpoints des Keycloak. - Die oben beschriebene Behandlung eines Backchannel Logouts implementiert Quarkus auch vollständig selbst – inkl. dem Handling der OIDC und HTTP Sessions.
Es konnten hier nicht alle Konfigurations-Properties gezeigt werden. Sie finden in https://quarkus.io/guides/all-config eine komplette Auflistung – filtern Sie nach quarkus.oidc. Aber Achtung: Die Liste ist lang …
Dieser Blog Post ist schon (zu?) lang und dennoch konnten nicht alle Details gezeigt werden. Kontaktieren Sie uns gerne, wenn Sie Fragen haben!







