Anmeldung einer Desktop-App mit Keycloak: Authorization Code mit PKCE über den Systembrowser

https://bildung.social/@oerinformatik/

https://oer-informatik.de/javafx-anmeldung-keycloak

JavaFX, Keycloak, OAuth 2.0, OpenID Connect, PKCE, native App, Loopback-Redirect, RFC 8252, Access Token, Refresh Token, Rollen, Kanban

tl/dr; (ca. 35 min Lesezeit): Das siebte Ticket der Desktop-App: Statt ein Token von Hand zu setzen, meldet sich die JavaFX-App selbst bei Keycloak an. Als native App ist sie ein öffentlicher Client ohne Geheimnis. Sie öffnet die Anmeldeseite im Systembrowser, nimmt den Rückruf mit einem kleinen HTTP-Server auf 127.0.0.1 entgegen und tauscht den Code mit PKCE gegen Tokens. Das Access Token erneuert sie rechtzeitig mit dem Refresh Token, und die Rollen aus dem ID Token entscheiden, welche Buttons die Oberfläche zeigt [AI-Assisted]. (Zuletzt geändert am 11.11.2026)

Dieser Artikel ist Teil einer Artikelreihe über eine Desktop-App mit JavaFX: Übersicht / Kanban-Projektstart / Projekt anlegen / FXML, Layouts und CSS / Properties, Bindings und MVVM / Backend anbinden / TableView für Buchungen / Formulare und Dialoge / Anmeldung mit Keycloak / Verwaltung und Diagramme / GUI-Tests mit TestFX / Abnahmetest und Kanban-Metriken / Paketieren mit jpackage

Schon fit in diesem Thema? Nutze die Kann-Liste und die Leitfragen unter dem Artikel als Checkliste, ob Du die relevanten Kompetenzen bereits hast.

Dieser Artikel baut auf der Kann-Liste des Artikels Formulare und Dialoge: Fahrtangebote anlegen, prüfen und absagen auf. Die Grundlagen zu OAuth 2.0, OpenID Connect und Keycloak stehen im Artikel Change Request: Keycloak, Resource Server und oauth2Login.

Als JavaFX-Grundlagen kann ich nach Durchlesen des Artikels…

  • … erklären, warum eine Desktop-App ein öffentlicher Client ist und kein Client-Secret enthalten darf.

  • … die Schritte des Authorization Code Flow mit PKCE für eine native App in die richtige Reihenfolge bringen und nennen, wer dabei welchen Wert kennt.

  • … begründen, warum die Anmeldung im Systembrowser und nicht in einem eingebetteten WebView stattfindet.

  • … einen code_verifier erzeugen und die code_challenge nach dem Verfahren S256 daraus berechnen.

  • … einen Loopback-Empfänger auf 127.0.0.1 mit einem vom Betriebssystem vergebenen Port starten und die Redirect-URI daraus bilden.

  • … den Code am Token-Endpunkt einlösen und state und nonce prüfen.

  • … Access Token, Refresh Token und ID Token unterscheiden und sagen, wofür die App welches nutzt.

  • … die Sicht der Oberfläche über Rollen aus dem ID Token steuern.

Darüber hinaus kann ich als JavaFX-Expertenwissen…

  • … ein Access Token kurz vor Ablauf erneuern und eine abgelaufene Anmeldung erkennen.

  • … abwägen, wo eine Desktop-App Tokens aufbewahrt (Arbeitsspeicher, Datei, Schlüsselbund des Betriebssystems).

  • … Fehler beim Redirect an ihren Meldungen erkennen (Invalid parameter: redirect_uri, Incorrect redirect_uri, PKCE verification failed).

Nach dem sechsten Ticket legt Ben Richter in der Desktop-App von Mitfahrlabor Fahrtangebote an und sagt sie ab. Damit das Backend die Anfragen annimmt, setzt das Team bisher ein Access Token von Hand in eine Umgebungsvariable und nennt die eigene Nutzer-ID gleich dazu. Das war der Ausweg, als JFX-04 an Projekttag 11 blockiert war. Mit Ticket JFX-07 meldet sich die App selbst an: Sie öffnet die Anmeldeseite von Keycloak im Browser, bekommt danach einen Code zurück und tauscht ihn gegen Tokens. Das Sequenzdiagramm zeigt den ganzen Ablauf, den dieser Artikel Schritt für Schritt aufbaut.

Sequenzdiagramm der Anmeldung: Die App erzeugt PKCE, state und nonce, startet den LoopbackEmpfaenger auf 127.0.0.1 mit Port 0 und öffnet die Anmeldeadresse im Systembrowser; der Browser holt die Anmeldeseite von Keycloak, Ben gibt Pseudonym und Passwort ein, Keycloak leitet per 302 auf 127.0.0.1:53712/callback mit code und state um; der Empfänger meldet den Rückruf an die App, die state prüft, den Code mit code_verifier und redirect_uri am Token-Endpunkt einlöst und das ID Token prüft; danach erneuert die App das Access Token bei Bedarf mit dem Refresh Token und schickt es als Bearer-Token an das Backend (mit KI erstellt)

Stand auf dem Board

Im Kanban-Meeting am Projekttag 21 hast Du JFX-07 „Anmelden über Keycloak, Sicht nach Rolle“ aus „Bereit“ gezogen. Das Ticket betrifft beide Sichten: Fahrer*innen sollen sich ohne Umweg anmelden, und die Verwaltung soll später eigene Funktionen bekommen. Andrea Wolf hat dafür im Replenishment eine Bedingung gestellt: Die Verwaltungsfunktionen dürfen nur für Konten mit der Rolle ORGA sichtbar sein. Die Akzeptanzkriterien:

  • Angenommen, die App startet ohne Beispieldaten, dann öffnet sie die Anmeldeseite von Keycloak im Standardbrowser des Rechners und wartet dort auf die Anmeldung.
  • Angenommen, ich melde mich als Funkenflug an, dann zeigt die App „Angemeldet als Funkenflug · NUTZER“ und nur meine eigenen Fahrtangebote, ohne dass ich ein Token oder eine Nutzer-ID eingebe.
  • Angenommen, die App ist länger als fünf Minuten offen, dann funktionieren Aktualisieren, Anlegen und Absagen weiter, ohne dass ich mich neu anmelde.
  • Angenommen, ich melde mich mit einem Konto mit Rolle ORGA an, dann sehe ich den Button „Verwaltung …“; mit Rolle NUTZER sehe ich ihn nicht.
  • Angenommen, ich klicke „Abmelden“, dann endet die Sitzung auch in Keycloak, und die App schließt.
JFX-07 auf dem Board
vor diesem Artikel nach diesem Artikel
JFX-06 Angebot anlegen und absagen Fertig (Projekttag 20) Fertig
JFX-07 Anmelden über Keycloak, Sicht nach Rolle Bereit Fertig (Projekttag 24)
Anmeldung Token und Nutzer-ID von Hand (MITFAHRLABOR_TOKEN, MITFAHRLABOR_FAHRER_ID) Systembrowser, Authorization Code mit PKCE, Loopback-Redirect
Tokens ein Token, läuft nach fünf Minuten ab Access Token wird mit dem Refresh Token erneuert
Sicht fest „Ben Richter“ Pseudonym und Rollen aus dem ID Token, Button „Verwaltung …“ nur mit ORGA
Keycloak-Realm Clients flurmonitor-verwaltung, mitfahrlabor-tester zusätzlich mitfahrlabor-desktop (öffentlich, PKCE)
Code-Stand src/07-formulare-dialoge/mitfahrlabor-desktop src/08-anmeldung-keycloak/mitfahrlabor-desktop

Eine Desktop-App ist ein öffentlicher Client

Keycloak unterscheidet wie OAuth 2.0 (RFC 6749) zwei Arten von Clients. Ein vertraulicher Client (confidential client) kann ein Geheimnis sicher aufbewahren, weil sein Code auf einem Server läuft, an den Nutzer*innen nicht herankommen. Der Flurmonitor im Backend ist so ein Client: Er meldet sich mit client_secret am Token-Endpunkt an. Ein öffentlicher Client (public client) kann das nicht. Eine Desktop-App wird als JAR oder Installationspaket verteilt; jedes Geheimnis darin lässt sich mit einem Decompiler oder einem Blick in den Speicher auslesen. Ein Client-Secret in der App wäre also kein Geheimnis, sondern nur eine Zeichenkette, die jeder kopieren kann.

Für solche Apps gibt es eine eigene Empfehlung: OAuth 2.0 for Native Apps (RFC 8252, auch BCP 212). Eine native App ist dort jede Anwendung, die auf dem Gerät installiert wird, also Desktop- und Smartphone-Apps. Die Kernregeln:

  • Die App nutzt den Authorization Code Flow mit PKCE (RFC 7636). PKCE ersetzt das fehlende Client-Secret: Nur wer den Code angefordert hat, kann ihn einlösen.
  • Die Anmeldung läuft in einem externen User-Agent, in der Regel dem Standardbrowser des Systems. Ein eingebetteter Browser (in JavaFX der WebView) ist ausdrücklich nicht erlaubt: Die App könnte darin das Passwort mitlesen, und Nutzer*innen könnten nicht erkennen, ob sie wirklich auf der Seite von Keycloak tippen. Außerdem fehlt im eingebetteten Browser die Anmeldung, die im Systembrowser vielleicht schon besteht.
  • Den Code bekommt die App über eine Redirect-URI zurück, die nur sie empfangen kann.

Für die Redirect-URI nennt RFC 8252 drei Wege. Die Tabelle vergleicht sie für die JavaFX-App:

Redirect-URIs für native Apps nach RFC 8252
Weg Beispiel Voraussetzung für JFX-07
Loopback-Redirect (Abschnitt 7.3) http://127.0.0.1:53712/callback App startet kurz einen HTTP-Server auf dem eigenen Rechner gewählt: läuft auf Windows, macOS und Linux ohne Installation
eigenes URI-Schema (7.1) de.mitfahrlabor.desktop:/callback Schema muss beim Betriebssystem registriert sein (Installer, Registry) erst mit dem Installationspaket aus JFX-12 möglich
„claimed“ HTTPS-URI (7.2) https://app.mitfahrlabor.example/callback Betriebssystem ordnet eine eigene Domain der App zu typisch für Smartphone-Apps, für Desktop kaum unterstützt

Für Geräte ohne Browser oder Tastatur (Fernseher, Kommandozeile auf einem Server) gibt es zusätzlich den Device Authorization Grant (RFC 8628): Das Gerät zeigt einen Code an, den man auf einem anderen Gerät eingibt. Auf einem Schulrechner mit Browser ist der Loopback-Redirect bequemer.

Im Realm mitfahrlabor gibt es damit drei Clients mit verschiedenen Aufgaben:

Clients im Realm mitfahrlabor
Client Art Flow Redirect-URI Wofür
flurmonitor-verwaltung vertraulich, mit Secret Authorization Code (mit PKCE) http://localhost:8080/login/oauth2/code/keycloak Verwaltungsseiten des Backends (oauth2Login)
mitfahrlabor-tester öffentlich nur Password Grant – Tokens für .http-Dateien und Tests, nie in einer App
mitfahrlabor-desktop öffentlich, ohne Secret Authorization Code mit PKCE (S256 Pflicht) http://127.0.0.1/callback (Port beliebig) diese JavaFX-App

Der Ablauf im Überblick

Das Sequenzdiagramm oben zeigt alle Beteiligten. Die Klasse Anmeldung im neuen Paket …desktop.anmeldung steuert den Ablauf; jeder Schritt hat eine eigene kleine Klasse:

  1. Die App erzeugt drei Zufallswerte: den code_verifier für PKCE, den state gegen untergeschobene Rückrufe und die nonce, die später im ID Token wiederkommen muss (Klasse Pkce).
  2. Sie startet einen HTTP-Server auf 127.0.0.1 mit einem freien Port (LoopbackEmpfaenger) und bildet daraus die Redirect-URI.
  3. Sie öffnet die Anmeldeadresse von Keycloak im Systembrowser (KeycloakKonfiguration.anmeldeAdresse, HostServices.showDocument). Darin stehen client_id, redirect_uri, state, nonce und die code_challenge, aber nicht der code_verifier.
  4. Ben meldet sich im Browser an. Keycloak prüft das Passwort; die App sieht davon nichts.
  5. Keycloak leitet den Browser auf die Redirect-URI um und hängt code und state an. Der Browser ruft damit den Empfänger der App auf.
  6. Die App vergleicht den state mit ihrem eigenen. Passt er nicht, löst sie den Code nicht ein.
  7. Sie schickt Code, code_verifier und Redirect-URI an den Token-Endpunkt (KeycloakTokenDienst). Keycloak prüft, dass der Hashwert des Verifiers zur Challenge aus Schritt 3 passt, und antwortet mit Access Token, Refresh Token und ID Token.
  8. Die App prüft das ID Token und liest Pseudonym, Nutzer-ID und Rollen daraus (Sitzung).
  9. Jede Anfrage an das Backend bekommt das Access Token im Header Authorization: Bearer …; kurz vor Ablauf holt die App mit dem Refresh Token ein neues.

Zwei Werte schützen vor zwei verschiedenen Angriffen. PKCE schützt den Code: Fängt ein anderes Programm auf dem Rechner den Rückruf ab, kann es den Code trotzdem nicht einlösen, weil es den code_verifier nicht kennt. Der state schützt die App: Ruft eine fremde Webseite die Loopback-Adresse mit einem eigenen Code auf (Cross-Site Request Forgery), passt der state nicht, und die App würde sonst die Sitzung eines anderen Kontos übernehmen.

Den Client in Keycloak einrichten

Der Realm-Export aus dem Backend-Artikel bekommt einen weiteren Client. Das Team pflegt ihn wie im Backend als Datei, damit alle denselben Stand importieren; die Datei liegt im Code-Stand unter src/08-anmeldung-keycloak/keycloak/mitfahrlabor-realm.json:

{
  "clientId": "mitfahrlabor-desktop",
  "name": "Mitfahrlabor Desktop (native App)",
  "publicClient": true,
  "standardFlowEnabled": true,
  "directAccessGrantsEnabled": false,
  "implicitFlowEnabled": false,
  "redirectUris": ["http://127.0.0.1/callback"],
  "attributes": {
    "pkce.code.challenge.method": "S256",
    "post.logout.redirect.uris": "http://127.0.0.1/*"
  },
  "protocolMappers": [
    { "name": "pseudonym", "protocolMapper": "oidc-usermodel-attribute-mapper", "...": "wie beim Flurmonitor" },
    { "name": "realm-rollen-im-id-token", "protocolMapper": "oidc-usermodel-realm-role-mapper", "...": "wie beim Flurmonitor" },
    { "name": "nutzer_id", "protocolMapper": "oidc-usermodel-attribute-mapper",
      "config": { "user.attribute": "nutzer_id", "claim.name": "nutzer_id", "jsonType.label": "long",
                  "id.token.claim": "true", "access.token.claim": "false" } }
  ]
}

Die wichtigsten Einstellungen:

  • publicClient: true: Keycloak erwartet am Token-Endpunkt kein Secret.
  • pkce.code.challenge.method: S256: Ohne code_challenge lehnt Keycloak die Anmeldung ab. Eine Anmeldeadresse ohne PKCE beantwortet Keycloak mit einer Umleitung auf die Redirect-URI und error=invalid_request&error_description=Missing parameter: code_challenge_method.
  • redirectUris ohne Port: Für die Loopback-Adressen 127.0.0.1 und [::1] vergleicht Keycloak den Port nicht, wie es RFC 8252 (Abschnitt 7.3) verlangt. Pfad und Host müssen aber genau stimmen. http://localhost:53712/callback wird abgelehnt, ebenso http://127.0.0.1:53712/login.
  • Der Mapper nutzer_id schreibt die ID des Kontos aus der Tabelle nutzer ins ID Token. Die App braucht sie, weil GET /angebote alle offenen Angebote liefert und die Liste nur die eigenen zeigen soll. Die Benutzer*innen im Realm haben dafür ein Attribut nutzer_id (Funkenflug 1, Morgenmuffel 2 … Admin-AW 10, Sekretariat-HB 11 wie in data.sql). Das ist eine Übergangslösung: Besser wäre ein Endpunkt im Backend, der zum Token die eigene ID liefert. Er steht als Wunsch an das Backend-Team in board.md.

Wie Du Keycloak startest, hängt davon ab, ob Du die ZIP-Distribution oder Docker nutzt. Hast Du Keycloak schon für das Backend mit dem alten Realm gestartet, legt --import-realm den neuen Client nicht an, denn ein vorhandener Realm wird nicht überschrieben. Dann beendest Du Keycloak, löschst dessen Datenbank und startest neu; alle Einstellungen kommen ja wieder aus der Datei.

Keycloak 26.8.2 aus der ZIP-Distribution läuft mit dem JDK 25 des Projekts. Den neuen Realm-Export legst Du in den Import-Ordner; die alte Datenbank liegt in data\h2:

cd C:\keycloak-26.8.2
Remove-Item -Recurse data\h2        # nur nötig, wenn der Realm schon einmal importiert wurde
copy <projekt>\src\08-anmeldung-keycloak\keycloak\mitfahrlabor-realm.json data\import\
$env:KC_BOOTSTRAP_ADMIN_USERNAME = "admin"
$env:KC_BOOTSTRAP_ADMIN_PASSWORD = "admin"
bin\kc.bat start-dev --http-port=8180 --import-realm

Unter Linux und macOS heißt das Startskript bin/kc.sh, gelöscht wird mit rm -r data/h2.

PKCE: Verifier und Challenge

Der code_verifier ist ein Zufallswert aus 43 bis 128 Zeichen der Menge A–Z, a–z, 0–9, -, ., _, ~ (RFC 7636, Abschnitt 4.1). 32 Zufallsbytes, als Base64URL ohne Füllzeichen geschrieben, ergeben genau 43 solche Zeichen. Die code_challenge ist beim Verfahren S256 der SHA-256-Hashwert des Verifiers, ebenfalls als Base64URL:

public record Pkce(String codeVerifier, String codeChallenge) {

    public static final String METHODE = "S256";
    private static final Base64.Encoder BASE64URL = Base64.getUrlEncoder().withoutPadding();

    /** 32 Zufallsbytes, Base64URL ohne Auffüllzeichen: 43 Zeichen aus [A-Za-z0-9-_]. */
    public static Pkce erzeugen(SecureRandom zufall) {
        byte[] bytes = new byte[32];
        zufall.nextBytes(bytes);
        String verifier = BASE64URL.encodeToString(bytes);
        return new Pkce(verifier, challengeFuer(verifier));
    }

    /** code_challenge = BASE64URL(SHA-256(ASCII(code_verifier))) */
    public static String challengeFuer(String codeVerifier) {
        try {
            byte[] hash = MessageDigest.getInstance("SHA-256")
                    .digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
            return BASE64URL.encodeToString(hash);
        } catch (NoSuchAlgorithmException fehlt) {
            throw new IllegalStateException("SHA-256 fehlt im JDK", fehlt);
        }
    }
}

SecureRandom ist Pflicht: Die Klasse Random liefert vorhersagbare Folgen, ein Angreifer könnte den Verifier dann erraten. Weil ein Hashwert sich nicht zurückrechnen lässt, darf die Challenge offen in der Adresszeile des Browsers stehen. Das Verfahren plain (Challenge = Verifier) erlaubt RFC 7636 nur, wenn S256 technisch nicht geht; Keycloak lässt es für diesen Client gar nicht zu. Ob die Berechnung stimmt, prüft PkceTest mit dem Beispiel aus Anhang B der RFC: Der Verifier dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk ergibt die Challenge E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM. Nach demselben Muster erzeugt die Methode zufallswert aus 16 Bytes die Werte für state und nonce.

Der Loopback-Empfänger

Den Rückruf nimmt ein HTTP-Server entgegen, der nur während der Anmeldung läuft. Das JDK bringt dafür den kleinen Server com.sun.net.httpserver.HttpServer mit; er liegt im Modul jdk.httpserver, das in module-info.java dazukommt (requires jdk.httpserver;). Eine zusätzliche Bibliothek ist nicht nötig.

public static LoopbackEmpfaenger starten() throws IOException {
    InetSocketAddress adresse = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0);
    HttpServer server = HttpServer.create(adresse, 0);
    LoopbackEmpfaenger empfaenger = new LoopbackEmpfaenger(server);
    server.createContext(PFAD, empfaenger::annehmen);       // PFAD = "/callback"
    server.start();
    return empfaenger;
}

/** z. B. http://127.0.0.1:53712/callback – IP-Literal statt "localhost" (RFC 8252, 8.3). */
public URI redirectUri() {
    return URI.create("http://127.0.0.1:" + server.getAddress().getPort() + PFAD);
}

Drei Entscheidungen stecken in diesen Zeilen:

  • Nur 127.0.0.1: Der Server lauscht auf der Loopback-Schnittstelle. Andere Rechner im Schulnetz können ihn nicht erreichen. Ein Server auf 0.0.0.0 würde den Code für alle im Netz erreichbar machen.
  • Port 0: Das Betriebssystem vergibt einen freien Port. Ein fester Port wie 8085 kann schon belegt sein, etwa durch eine zweite Instanz der App oder ein anderes Programm. Weil Keycloak den Port bei Loopback-Adressen nicht vergleicht, darf er sich bei jeder Anmeldung ändern.
  • IP-Literal statt localhost: localhost kann je nach System zu 127.0.0.1 oder ::1 aufgelöst werden; lauscht der Server auf der anderen Adresse, läuft der Rückruf ins Leere. RFC 8252 empfiehlt deshalb 127.0.0.1 in der Redirect-URI. Keycloak ist dabei genau: Der Client erlaubt nur 127.0.0.1, eine Anmeldeadresse mit localhost zeigt im Browser „Invalid parameter: redirect_uri“.

Der Handler liest code, state und gegebenenfalls error aus der Abfrage, schickt dem Browser eine kurze HTML-Seite („Anmeldung abgeschlossen. Du kannst dieses Browserfenster schließen …“) und erfüllt ein CompletableFuture<Rueckruf>. Auf dieses Future wartet die App. Weitere Aufrufe, etwa das automatische Laden eines Favicons, ändern das Ergebnis nicht mehr, denn ein CompletableFuture lässt sich nur einmal erfüllen. Weil LoopbackEmpfaenger das Interface AutoCloseable umsetzt, stoppt ein try-with-resources-Block den Server, sobald die Anmeldung fertig ist.

Systembrowser öffnen und Code einlösen

Die Methode anmelden der Klasse Anmeldung setzt die Teile zusammen. Sie blockiert, bis der Rückruf kommt oder die Wartezeit abläuft, und läuft deshalb wie das Laden der Angebote in einem Task im Hintergrund-Thread:

public Sitzung anmelden(Duration wartezeit) {
    Pkce pkce = Pkce.erzeugen(zufall);
    String state = Pkce.zufallswert(zufall);
    String nonce = Pkce.zufallswert(zufall);
    try (LoopbackEmpfaenger empfaenger = LoopbackEmpfaenger.starten()) {
        URI redirectUri = empfaenger.redirectUri();
        browser.accept(konfiguration.anmeldeAdresse(redirectUri, state, nonce, pkce));
        Rueckruf rueckruf = empfaenger.rueckruf().get(wartezeit.toMillis(), TimeUnit.MILLISECONDS);
        if (!state.equals(rueckruf.state())) {
            // fremder oder veralteter Rückruf (CSRF): Code nicht einlösen
            throw new AnmeldeFehler("state_falsch", "Der Rückruf gehört nicht zu dieser Anmeldung.");
        }
        if (!rueckruf.erfolgreich()) {
            throw new AnmeldeFehler(rueckruf.fehler(), "Keycloak hat die Anmeldung abgelehnt.");
        }
        Tokens neu = tokenDienst.codeEinloesen(rueckruf.code(), redirectUri, pkce.codeVerifier());
        Sitzung geprueft = Sitzung.ausIdToken(neu.idToken(), konfiguration, nonce, uhr.instant());
        synchronized (this) {
            tokens = neu;
            sitzung = geprueft;
        }
        return geprueft;
    } catch (TimeoutException zuLange) {
        throw new AnmeldeFehler("zeit_abgelaufen", "Im Browser wurde keine Anmeldung abgeschlossen.");
    } // weitere catch-Blöcke für IOException und InterruptedException im Code-Stand
}

Wie der Browser geöffnet wird, bekommt Anmeldung als Consumer<URI> von außen. In der App ist das getHostServices().showDocument(adresse.toString()), verpackt in Platform.runLater, weil anmelden im Hintergrund-Thread läuft: Die Klasse HostServices aus javafx.application öffnet eine Adresse mit dem Standardbrowser des Betriebssystems. Im Test ersetzt ein „Ersatz-Browser“ diesen Schritt (dazu unten mehr).

Die Anmeldeadresse baut KeycloakKonfiguration. Der Issuer http://localhost:8180/realms/mitfahrlabor ist zugleich die Basis aller Endpunkte; Keycloak veröffentlicht sie unter …/.well-known/openid-configuration:

public URI anmeldeAdresse(URI redirectUri, String state, String nonce, Pkce pkce) {
    Map<String, String> parameter = new LinkedHashMap<>();
    parameter.put("response_type", "code");
    parameter.put("client_id", clientId);                     // mitfahrlabor-desktop
    parameter.put("redirect_uri", redirectUri.toString());
    parameter.put("scope", "openid");                         // sonst gibt es kein ID Token
    parameter.put("state", state);
    parameter.put("nonce", nonce);
    parameter.put("code_challenge", pkce.codeChallenge());
    parameter.put("code_challenge_method", "S256");
    return URI.create(autorisierungsEndpunkt() + "?" + formular(parameter));
}

Das Einlösen übernimmt KeycloakTokenDienst mit dem HttpClient des JDK. Der Body ist ein Formular (application/x-www-form-urlencoded), nicht JSON:

@Override
public Tokens codeEinloesen(String code, URI redirectUri, String codeVerifier) {
    Map<String, String> parameter = new LinkedHashMap<>();
    parameter.put("grant_type", "authorization_code");
    parameter.put("client_id", konfiguration.clientId());
    parameter.put("code", code);
    parameter.put("redirect_uri", redirectUri.toString()); // muss genau der Wert aus der Anmeldeadresse sein
    parameter.put("code_verifier", codeVerifier);
    return tokensAus(senden(konfiguration.tokenEndpunkt(), parameter));
}

Ein client_secret fehlt bewusst. Keycloak antwortet mit JSON, aus dem tokensAus die drei Tokens und den Ablaufzeitpunkt (expires_in Sekunden ab jetzt) liest:

{ "access_token": "eyJhbGciOiJSUzI1NiIs…", "expires_in": 300,
  "refresh_token": "eyJhbGciOiJIUzUxMiIs…", "refresh_expires_in": 1800,
  "id_token": "eyJhbGciOiJSUzI1NiIs…", "token_type": "Bearer", "scope": "openid profile email" }

Lehnt Keycloak ab, kommt nach RFC 6749 (Abschnitt 5.2) ein JSON mit error und error_description, zum Beispiel {"error":"invalid_grant","error_description":"PKCE verification failed: Code mismatch"}. KeycloakTokenDienst macht daraus einen AnmeldeFehler mit dem Fehlercode. Ein Code gilt nur einmal und nur kurz; auch ein gescheiterter Versuch verbraucht ihn, der nächste Versuch endet mit Code not valid.

ID Token prüfen und Sitzung ableiten

Die Antwort enthält drei Tokens mit verschiedenen Empfängern. Das Access Token ist für das Backend bestimmt; die App reicht es nur weiter und wertet es nicht aus. Das Refresh Token ist für Keycloak bestimmt und dient nur zum Erneuern. Das ID Token aus OpenID Connect ist für die App selbst: Es sagt, wer sich angemeldet hat. Deshalb liest die App Pseudonym, Nutzer-ID und Rollen aus dem ID Token und nicht aus dem Access Token.

Ein JWT besteht aus drei Base64URL-Teilen, getrennt durch Punkte: Kopf, Nutzdaten (Claims) und Signatur. Die Claims von Bens ID Token sehen so aus (gekürzt):

{ "iss": "http://localhost:8180/realms/mitfahrlabor", "aud": "mitfahrlabor-desktop",
  "exp": 1794379200, "nonce": "Xq4…", "preferred_username": "funkenflug",
  "pseudonym": "Funkenflug", "nutzer_id": 1, "realm_access": { "roles": ["NUTZER"] } }

Bevor die App einem ID Token glaubt, prüft sie nach OpenID Connect Core (Abschnitt 3.1.3.7) vier Claims:

public static Sitzung ausIdToken(String idToken, KeycloakKonfiguration konfiguration, String nonce, Instant jetzt) {
    JsonNode claims = nutzdaten(idToken);
    if (!konfiguration.issuer().equals(claims.path("iss").asText())) {
        throw ungueltig("Aussteller (iss) passt nicht: " + claims.path("iss").asText());
    }
    if (!enthaelt(claims.path("aud"), konfiguration.clientId())) {
        throw ungueltig("Das ID Token ist nicht für diese App ausgestellt (aud).");
    }
    if (!jetzt.isBefore(Instant.ofEpochSecond(claims.path("exp").asLong(0)))) {
        throw ungueltig("Das ID Token ist abgelaufen (exp).");
    }
    if (!nonce.equals(claims.path("nonce").asText())) {
        throw ungueltig("nonce passt nicht zur Anmeldung.");
    }
    Set<String> rollen = new HashSet<>();
    claims.path("realm_access").path("roles").forEach(rolle -> rollen.add(rolle.asText()));
    return new Sitzung(claims.path("pseudonym").asText(claims.path("preferred_username").asText()),
            claims.path("nutzer_id").asLong(0), rollen);
}

aud (Audience) stellt sicher, dass das Token für diese App ausgestellt wurde und nicht etwa für den Flurmonitor. Die nonce bindet das Token an genau diese Anmeldung. Die Signatur prüft die App hier nicht selbst. Das ist erlaubt, weil sie das Token direkt vom Token-Endpunkt bekommt und nicht über den Browser; in Produktion muss diese Verbindung dann über HTTPS laufen, damit niemand die Antwort fälschen kann. Das Backend prüft die Signatur des Access Tokens dagegen bei jeder Anfrage, denn dort kommen Tokens von beliebigen Clients an.

Der Issuer hängt bei Keycloak im Entwicklungsmodus von der Adresse ab, unter der es aufgerufen wird: Wer Keycloak über 127.0.0.1:8180 anspricht, bekommt Tokens mit iss = http://127.0.0.1:8180/realms/mitfahrlabor. App und Backend müssen deshalb dieselbe Schreibweise nutzen, sonst lehnen beide das Token ab. In Produktion legt die Einstellung hostname von Keycloak den Issuer fest.

Tokens aufbewahren und erneuern

Ein Access Token gilt im Realm mitfahrlabor fünf Minuten (accessTokenLifespan: 300). Die App muss es also erneuern. Dafür schickt sie das Refresh Token mit grant_type=refresh_token an denselben Token-Endpunkt. Das Refresh Token gilt, solange die Sitzung in Keycloak aktiv ist: Wird es 30 Minuten nicht benutzt (ssoSessionIdleTimeout: 1800), endet die Sitzung. Die Grafik zeigt einen Vormittag: Solange die App Anfragen schickt, erneuert sie das Access Token bei Bedarf. Nach einer langen Pause ist auch das Refresh Token abgelaufen, und Ben muss sich im Browser neu anmelden.

Zeitleiste der Tokens: Anfragen an das Backend in Minute 0, 2, 4,7, 8, 12, 16 und 19, danach eine Pause bis Minute 55; das Access Token gilt jeweils 5 Minuten und wird bei den Anfragen in Minute 4,7, 12 und 19 mit dem Refresh Token erneuert; das Refresh Token läuft 30 Minuten nach der letzten Erneuerung in Minute 49 ab; die Anfrage in Minute 55 scheitert mit invalid_grant, Ben meldet sich im Browser neu an und bekommt neue Tokens (mit KI erstellt)

Erneuert wird nicht nach einem Zeitplan, sondern dann, wenn ein Token gebraucht wird. Die Methode accessToken() prüft dazu mit einem Puffer von 30 Sekunden, denn ein Token, das beim Absenden noch gilt, kann beim Backend schon abgelaufen sein:

public synchronized String accessToken() {
    if (tokens == null) {
        throw new AnmeldeFehler("nicht_angemeldet", "Bitte zuerst anmelden.");
    }
    if (tokens.laeuftBaldAb(uhr.instant(), PUFFER)) {      // PUFFER = 30 s
        if (tokens.refreshToken() == null) {
            throw new AnmeldeFehler("invalid_grant", "Die Anmeldung ist abgelaufen.");
        }
        Tokens neu = tokenDienst.erneuern(tokens.refreshToken());
        // Keycloak schickt meist ein neues Refresh Token; sonst das alte behalten
        tokens = new Tokens(neu.accessToken(),
                neu.refreshToken() != null ? neu.refreshToken() : tokens.refreshToken(),
                neu.idToken() != null ? neu.idToken() : tokens.idToken(), neu.ablauf());
    }
    return tokens.accessToken();
}

synchronized verhindert, dass zwei Hintergrund-Threads gleichzeitig erneuern und eines davon ein schon ersetztes Refresh Token schickt. Ist das Refresh Token abgelaufen oder widerrufen, antwortet Keycloak mit invalid_grant; die App zeigt dann „Anmeldung abgelaufen … Bitte melde Dich neu an.“

Das Access Token setzt der erzeugte OpenAPI-Client nicht selbst. Der ApiClient aus Backend anbinden hat aber einen Request-Interceptor, der jede Anfrage verändern darf, bevor sie abgeht. Bisher setzte er das Token aus der Umgebungsvariablen. Jetzt fragt er bei jeder Anfrage nach einem gültigen Token:

client.setRequestInterceptor(anfrage -> anfrage.header("Authorization", "Bearer " + anmeldung.accessToken()));

Wichtig ist, dass accessToken() im Lambda steht. Würde der Wert einmal beim Anlegen des Clients gelesen, bekäme jede Anfrage dasselbe Token, und nach fünf Minuten antwortete das Backend nur noch mit 401. Der Interceptor läuft in dem Thread, der die Anfrage stellt, also im Hintergrund-Thread der Tasks; ein Erneuern blockiert die Oberfläche deshalb nicht.

Bleibt die Frage, wo die App die Tokens aufbewahrt. Die Tabelle vergleicht die Möglichkeiten:

Wo bewahrt eine Desktop-App Tokens auf?
Ort Vorteil Nachteil Entscheidung
nur im Arbeitsspeicher nichts bleibt nach dem Beenden zurück nach jedem Start neu anmelden (im Browser oft ohne Passwort, solange die Keycloak-Sitzung besteht) gewählt für JFX-07
Datei im Benutzerordner Anmeldung übersteht einen Neustart jedes Programm des Kontos und jede Datensicherung kann das Refresh Token lesen und damit Tokens holen nicht verwenden
Schlüsselbund des Betriebssystems (Windows-Anmeldeinformationsverwaltung, macOS-Schlüsselbund) verschlüsselt und an das Konto gebunden Java hat dafür keine eingebaute Schnittstelle, nötig wäre eine Bibliothek je System Kandidat, wenn das Sekretariat nicht täglich neu anmelden will

Weil die Tokens nur im Arbeitsspeicher liegen, schreibt die App sie auch nie in Logs: Tokens.toString() gibt nur den Ablaufzeitpunkt aus. Beim Abmelden ruft die App den Endpunkt …/protocol/openid-connect/logout mit client_id und Refresh Token auf. Danach ist die Sitzung in Keycloak beendet, und auch das Refresh Token gilt nicht mehr.

Sicht nach Rolle

Die Rollen aus dem ID Token entscheiden, was die Oberfläche anbietet. Nach der Regel des Boards von Projekttag 9 steht diese Anzeige-Logik im ViewModel und hat dort einen Komponententest. Das neue SitzungViewModel leitet drei Werte ab:

public SitzungViewModel(Sitzung sitzung) {
    angemeldetAls.set("Angemeldet als " + sitzung.pseudonym()
            + " · " + String.join(", ", new TreeSet<>(sitzung.rollen())));
    fahrerSicht.set(sitzung.hatRolle(Sitzung.ROLLE_NUTZER));     // Angebote anlegen, absagen
    verwaltungSicht.set(sitzung.hatRolle(Sitzung.ROLLE_ORGA));   // Button „Verwaltung …“
}

Der HauptfensterController bindet die Buttons daran. Mit managedProperty gibt ein unsichtbarer Button auch seinen Platz im Layout frei; nur visible würde eine Lücke lassen:

angemeldetAls.textProperty().bind(sitzungViewModel.angemeldetAlsProperty());
sichtbarWenn(neuesAngebotButton, sitzungViewModel.fahrerSichtProperty());
sichtbarWenn(absagenButton, sitzungViewModel.fahrerSichtProperty());
sichtbarWenn(verwaltungButton, sitzungViewModel.verwaltungSichtProperty());

private static void sichtbarWenn(Button button, ObservableBooleanValue bedingung) {
    button.visibleProperty().bind(bedingung);
    button.managedProperty().bind(bedingung);
}

In der Kopfzeile von hauptfenster.fxml ersetzt das Label angemeldetAls den festen Text „Fahrer*innen-Sicht · Ben Richter“; dazu kommen die Buttons „Verwaltung …“ und „Abmelden“. Die Verwaltungsansicht selbst entsteht erst mit JFX-08 bis JFX-10; bis dahin zeigt der Button einen Hinweis. So sieht das Hauptfenster nach Bens Anmeldung aus:

Bildschirmfoto des Hauptfensters nach der Anmeldung: in der blauen Kopfzeile „Mitfahrlabor“, „Angemeldet als Funkenflug · NUTZER“ und die Buttons Aktualisieren und Abmelden, ohne Button Verwaltung; links Bens zwei Fahrtangebote vom 19.10.2026 mit den Buttons Neues Angebot und Absagen, rechts Details und Buchungen des gewählten Angebots (mit KI erstellt)

Ausblenden ist kein Schutz. Wer die App verändert oder die REST-API direkt aufruft, kommt an jedem Button vorbei. Ob eine Anfrage erlaubt ist, entscheidet das Backend anhand des Access Tokens: Fehlt es oder ist es abgelaufen, antwortet es mit 401 Unauthorized; ist das Token gültig, aber die Rolle passt nicht zur Aktion, antwortet es mit 403 Forbidden. Die Rollen in der App dienen der Bedienung: Niemand soll Buttons sehen, die doch nur zu einem Fehler führen.

Beim Start setzt MitfahrlaborDesktopApp die Teile zusammen. Mit MITFAHRLABOR_API=beispieldaten startet die App wie bisher ohne Backend und ohne Anmeldung als Ben; die zusätzliche Einstellung MITFAHRLABOR_ROLLE=ORGA zeigt dann auch den Verwaltungsbutton. Sonst öffnet sie zuerst ein kleines Fenster „Bitte melde Dich im Browser an …“, startet die Anmeldung im Hintergrund und lädt das Hauptfenster erst danach mit RestAngebotQuelle(client, sitzung.nutzerId()). Scheitert die Anmeldung oder schließt Ben den Browser, nennt das Fenster den Grund und bietet „Erneut anmelden“ an. Die Umgebungsvariablen MITFAHRLABOR_TOKEN und MITFAHRLABOR_FAHRER_ID sind weggefallen; neu ist MITFAHRLABOR_KEYCLOAK für den Issuer, falls Keycloak nicht unter http://localhost:8180 läuft.

Testen ohne und mit Keycloak

Die Anmeldung braucht einen Browser, einen Menschen am Passwortfeld und ein laufendes Keycloak. Damit die Tests trotzdem automatisch laufen, trennt der Code die Teile über Schnittstellen: Der Browser ist ein Consumer<URI>, der Token-Endpunkt das Interface TokenDienst, die Uhr eine java.time.Clock. Die Tests ersetzen jeden Teil einzeln:

Tests der Anmeldung
Test Testebene ersetzt prüft
PkceTest, KeycloakKonfigurationTest Komponententest – Challenge nach RFC 7636, alle Parameter der Anmeldeadresse, kein Verifier darin
LoopbackEmpfaengerTest Komponententest Browser durch HttpClient Rückruf mit Code oder Fehler, nur 127.0.0.1, freie Ports
AnmeldungTest Komponententest Browser und Keycloak durch Ersatzobjekte, Uhr durch TestUhr Verifier passt zur Challenge, gleiche Redirect-URI, fremder state wird nicht eingelöst, Erneuern im Puffer, invalid_grant, Abmelden
SitzungTest Komponententest – Claims lesen, iss, aud, exp, nonce
SitzungViewModelTest, SichtNachRolleTest Komponententest Beispieldaten Text in der Kopfzeile, Buttons je Rolle
KeycloakTokenDienstIntegrationTest Integrationstest Keycloak durch WireMock Formular-Parameter ohne Secret, Antwort lesen, Fehlerantwort, Keycloak nicht erreichbar
AnmeldungKeycloakSystemTest Systemtest Browser durch HttpClient, der das Anmeldeformular ausfüllt echte Anmeldung als Funkenflug und Admin-AW, Erneuern, nach dem Abmelden invalid_grant

Der Kern von AnmeldungTest ist ein Ersatz-Browser: Er liest die Parameter aus der Anmeldeadresse, ruft die Redirect-URI mit code=c1 und dem richtigen (oder absichtlich falschen) state auf und merkt sich die code_challenge. Der Ersatz-TokenDienst merkt sich, womit die App einlöst. So lässt sich der wichtigste Satz von PKCE als Test schreiben:

@Test
void anmeldenLoestCodeMitPassendemVerifierUndGleicherRedirectUriEin() {
    Anmeldung anmeldung = anmeldung(keycloak.browser(null));

    Sitzung sitzung = anmeldung.anmelden(Duration.ofSeconds(5));

    assertEquals("Funkenflug", sitzung.pseudonym());
    assertEquals(keycloak.anmeldeParameter.get("redirect_uri"), keycloak.eingeloestMitRedirect.toString());
    // PKCE: Der eingelöste Verifier passt zur Challenge aus der Anmeldeadresse
    assertEquals(keycloak.anmeldeParameter.get("code_challenge"), Pkce.challengeFuer(keycloak.eingeloestMitVerifier));
}

Der Systemtest läuft nur, wenn die Umgebungsvariable MITFAHRLABOR_KEYCLOAK_TEST den Issuer eines laufenden Keycloak nennt (@EnabledIfEnvironmentVariable); sonst wird er übersprungen, und die Pipeline braucht kein Keycloak. Das Team hat ihn als neue Regel für die Spalte „Test“ aufgenommen: Vor „Fertig“ läuft er einmal gegen Keycloak mit dem Realm-Export aus dem Repository.

.\gradlew.bat test                                           # alle Tests ohne Keycloak (Maven: .\mvnw.cmd test)
$env:MITFAHRLABOR_KEYCLOAK_TEST = "http://localhost:8180/realms/mitfahrlabor"
.\gradlew.bat test --tests "*KeycloakSystemTest"             # Systemtest gegen laufendes Keycloak
Remove-Item Env:MITFAHRLABOR_KEYCLOAK_TEST
.\gradlew.bat run                                            # mit Keycloak (Port 8180) und Backend (Port 8080)

Den vollständigen Code-Stand findest Du unter src/08-anmeldung-keycloak/mitfahrlabor-desktop, den Realm-Export unter src/08-anmeldung-keycloak/keycloak und das Board unter src/08-anmeldung-keycloak/board.md. Im Vertrag openapi/openapi.yaml des Code-Stands ist das Sicherheitsschema von HTTP Basic auf bearerAuth (JWT) nachgezogen, wie es das Backend seit dem Change Request CR-02 erwartet.

Nächster Schritt: JFX-07 nach „Fertig“, Verwaltung vorbereiten

Ben startet die App, meldet sich einmal im Browser an und arbeitet danach ohne Unterbrechung; nach einer langen Pause fragt die App nach einer neuen Anmeldung. Kein Token und kein Geheimnis steht mehr in einer Umgebungsvariablen oder im Code. JFX-07 ist am Projekttag 24 fertig, nach vier Projekttagen. Die Durchlaufzeit liegt damit an der oberen Grenze der Erwartung; im Review des Ablaufs nennt das Team als Grund das Einrichten des neuen Clients im Realm.

Als Nächstes stehen drei Tickets der Verwaltungssicht im Backlog: JFX-08 „Jede Fahrt absagen (Rolle ORGA)“, JFX-09 „Haltepunkte anzeigen und pflegen“ und JFX-10 „Auslastung der Haltepunkte als Diagramm“. Für die Haltepunkte fehlen im Backend Endpunkte zum Schreiben; im Artikel Verwaltung und Diagramme kommen sie als Expedite-Ticket dazu, und der Button „Verwaltung …“ öffnet endlich eine eigene Ansicht.

Zum Üben gibt es die Übungsaufgaben zur Anmeldung mit Keycloak: Dort ordnest Du die Schritte der Anmeldung, steuerst das Gesuche-Fenster nach Rolle, erneuerst Tokens in einem eigenen Speicher und findest die Ursachen von Redirect-Fehlern.

Leitfragen

  1. Warum darf eine Desktop-App kein Client-Secret enthalten, und was ersetzt es?

Die App wird an viele Rechner verteilt; jedes Geheimnis darin lässt sich auslesen. Sie ist deshalb ein öffentlicher Client. PKCE ersetzt das Secret: Nur wer den code_verifier kennt, kann den Code einlösen, und den kennt nur die App, die die Anmeldung gestartet hat. siehe Abschnitt „Eine Desktop-App ist ein öffentlicher Client“

  1. Warum meldet sich Ben im Systembrowser an und nicht in einem WebView der App?

RFC 8252 verbietet eingebettete Browser für die Anmeldung: Die App könnte dort das Passwort mitlesen, Nutzer*innen erkennen nicht, ob sie wirklich bei Keycloak tippen, und eine bestehende Anmeldung im Browser wird nicht genutzt. Im Systembrowser sieht die App nur den Code. siehe Abschnitt „Eine Desktop-App ist ein öffentlicher Client“

  1. Welche Werte kennt Keycloak nach dem Aufruf der Anmeldeadresse, welche erst beim Einlösen?

Mit der Anmeldeadresse bekommt Keycloak client_id, redirect_uri, state, nonce und die code_challenge. Erst beim Einlösen am Token-Endpunkt schickt die App den code_verifier, zusammen mit dem Code und derselben redirect_uri. siehe Abschnitt „Der Ablauf im Überblick“

  1. Wovor schützen PKCE und state jeweils?

PKCE schützt den Code: Ein Programm, das den Rückruf abfängt, kann ihn ohne Verifier nicht einlösen. Der state schützt die App vor untergeschobenen Rückrufen (CSRF): Ein Rückruf, den die App nicht selbst ausgelöst hat, hat einen anderen state und wird verworfen. siehe Abschnitt „Der Ablauf im Überblick“

  1. Warum lauscht der Loopback-Empfänger auf 127.0.0.1 mit Port 0, und warum steht in der Redirect-URI nicht localhost?

127.0.0.1 ist nur vom eigenen Rechner erreichbar. Port 0 lässt das Betriebssystem einen freien Port wählen, ein fester Port könnte belegt sein; Keycloak vergleicht bei Loopback-Adressen den Port nicht. localhost kann zu ::1 aufgelöst werden und passt nicht zur registrierten Redirect-URI; Keycloak meldet dann „Invalid parameter: redirect_uri“. siehe Abschnitt „Der Loopback-Empfänger“

  1. Wofür nutzt die App Access Token, Refresh Token und ID Token?

Das Access Token schickt sie im Header Authorization: Bearer … an das Backend, ohne es auszuwerten. Mit dem Refresh Token holt sie bei Keycloak ein neues Access Token. Aus dem ID Token liest sie, wer angemeldet ist (Pseudonym, Nutzer-ID, Rollen), nachdem sie iss, aud, exp und nonce geprüft hat. siehe Abschnitt „ID Token prüfen und Sitzung ableiten“

  1. Wann erneuert die App das Access Token, und wann muss Ben sich neu anmelden?

Bei jeder Anfrage prüft accessToken(), ob das Token in weniger als 30 Sekunden abläuft, und erneuert es dann mit dem Refresh Token. Neu anmelden muss Ben, wenn das Refresh Token abgelaufen ist (30 Minuten ohne Nutzung) oder nach dem Abmelden; Keycloak antwortet dann mit invalid_grant. siehe Abschnitt „Tokens aufbewahren und erneuern“

  1. Warum hält die App die Tokens nur im Arbeitsspeicher?

Ein Refresh Token in einer Datei könnte jedes Programm des Benutzerkontos lesen und damit selbst Tokens holen. Im Arbeitsspeicher verschwindet es mit dem Beenden. Der Preis ist eine Anmeldung nach jedem Start; soll sie entfallen, gehört das Refresh Token in den Schlüsselbund des Betriebssystems. siehe Abschnitt „Tokens aufbewahren und erneuern“

  1. Warum reicht es nicht, den Button „Verwaltung …“ für Konten ohne ORGA auszublenden?

Ausblenden betrifft nur die Oberfläche. Wer die REST-API direkt aufruft, kommt daran vorbei. Verbindlich prüft das Backend die Rolle im Access Token und antwortet mit 403, wenn sie fehlt. siehe Abschnitt „Sicht nach Rolle“


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Anmeldung einer Desktop-App mit Keycloak: Authorization Code mit PKCE über den Systembrowser” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/javafx-anmeldung-keycloak veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/javafx/javafx-einstieg. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 11.11.2026.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: