Übungsaufgaben zur Anmeldung mit Keycloak: Mitfahrgesuche nach Rolle
https://bildung.social/@oerinformatik/
https://oer-informatik.de/javafx-anmeldung-keycloak-uebungsaufgaben
JavaFX, Keycloak, OAuth 2.0, OpenID Connect, PKCE, native App, Loopback-Redirect, Refresh Token, Rollen, Fehlersuche, Übungsaufgaben, Prüfungsvorbereitung
tl/dr; (Bearbeitungszeit insgesamt 56 min, mehrere Aufgaben): Übungen zur Anmeldung der JavaFX-Desktop-App über Keycloak: Du ordnest die Schritte des Authorization Code Flow mit PKCE und ordnest Werte und Tokens zu, steuerst das Gesuche-Fenster nach Rolle, schreibst einen Token-Speicher, der rechtzeitig erneuert, und findest die Ursachen von Redirect- und Token-Fehlern anhand ihrer Meldungen [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
Diese Übungsaufgaben gehören zum Artikel Anmeldung einer Desktop-App mit Keycloak: Authorization Code mit PKCE über den Systembrowser. Sie spielen im Projekt Mitfahrlabor und führen den Strang der Mitfahrgesuche weiter: Seit den Übungen zu Formularen und Dialogen können Mitfahrende im Gesuche-Fenster selbst ein Gesuch eintragen. Jetzt soll das Fenster wissen, wer angemeldet ist.
Hinweise zur Bearbeitung: Bis zur Teil-1-Prüfung der IT-Berufe solltest Du die Basiskompetenz beherrschen, also Aufgabe 1.1. Als FIA, FIDP oder FIDV musst Du zur Teil-2-Prüfung Aufgaben auf Kernkompetenz-Ebene lösen können, das sind Aufgabe 2.1 und Aufgabe 2.2. Von FIA wird zusätzlich erwartet, dass ihnen Aufgaben auf Profilkompetenz-Ebene (Aufgabe 3.1) keine Probleme bereiten. Als Startcode für die Aufgaben 2.1 und 2.2 dient der Code-Stand src/07-formulare-dialoge/uebung-mitfahrgesuche mit Deinem GesucheViewModel aus den letzten Übungen; das Paket …desktop.anmeldung übernimmst Du aus dem Code-Stand des Artikels (src/08-anmeldung-keycloak/mitfahrlabor-desktop), dazu requires jdk.httpserver; in module-info.java. Eine vollständige Beispiellösung mit allen Tests liegt unter src/08-anmeldung-keycloak/uebung-mitfahrgesuche. Code-Aufgaben löst Du am besten am Rechner und prüfst sie mit gradlew test (bzw. mvnw test); für Aufgabe 3.1 brauchst Du kein laufendes Keycloak. Die Operatoren (nennen, beschreiben, erläutern, beurteilen …) sind im Artikel Wie sind die Aufgaben aufgebaut? erklärt. Schreib Deine Lösung zuerst vollständig auf, bevor Du die Beispiellösung aufklappst.
Du möchtest eigene Aufgaben zum Üben von einem KI-Sprachmodell generieren lassen?
Kopiere den Text in ein KI-Sprachmodell Deiner Wahl und passe Kompetenzstufe, Anzahl, Bearbeitungszeit und Szenario bei Bedarf an. Wie die Aufgaben aufgebaut sind, beschreibt der Artikel Wie sind die Aufgaben aufgebaut?. Prüfe die Ergebnisse mit dem Lehrartikel: KI-Sprachmodelle machen Fehler.
Erstelle neue Übungsaufgaben zum Thema „Anmeldung mit Keycloak:
Mitfahrgesuche nach Rolle“.
Bauplan: Halte Dich an Aufbau, Kompetenzstufen, Operatoren,
Anforderungsbereiche, Punkte und Zeitschätzung, die im Artikel
https://oer-informatik.de/aufgaben-aufbau beschrieben sind.
Fachliche Grundlage:
- https://oer-informatik.de/javafx-anmeldung-keycloak
- https://oer-informatik.de/szenario-mitfahrlabor
- https://oer-informatik.de/javafx-kanban-projektstart
- Stilbeispiel: https://oer-informatik.de/javafx-anmeldung-keycloak-uebungsaufgaben
Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- JavaFX
- Keycloak
- OAuth 2.0
- OpenID Connect
- PKCE
- native App
- Loopback-Redirect
- Refresh Token
- Rollen
- Fehlersuche
Kompetenzstufe: Kernkompetenz (alternativ: Basiskompetenz, Profilkompetenz, Expertise)
Anzahl: 1 Aufgabe mit Teilaufgaben
Bearbeitungszeit in Minuten ca.: <5 bis 30 min>
Szenario: eines der Szenarien aus dem Bauplan
Vorgehen:
1. Lies zuerst die verlinkten Artikel. Kannst Du keine Webseiten
abrufen, sag mir das, statt zu raten.
2. Steht bei einer Angabe noch ein Platzhalter in <…>, frag mich
zuerst danach. Richte Punkte und Zahl der Teilaufgaben nach der
Bearbeitungszeit (ca. 0,9 min je Punkt).
3. Gib je Aufgabe Titel mit Kompetenzstufe, Ausgangssituation und
Teilaufgaben mit Operator und Punktzahl aus, dazu die geschätzte
Bearbeitungszeit.
4. Gib die Beispiellösungen mit Punkteverteilung erst aus, wenn ich
danach frage.
Aufgabe 1.1: Den Ablauf der Anmeldung ordnen (Basiskompetenz)
(ca. 13 min Bearbeitungszeit)
Aylin Kaya (Pseudonym Morgenmuffel) startet das Gesuche-Fenster der Desktop-App zum ersten Mal mit Anmeldung. Oksana Melnyk erklärt einer neuen Person im Übungsteam, was dabei passiert.
- Bringe die folgenden Schritte in die richtige Reihenfolge. (4 Punkte)
- Der Systembrowser ruft
http://127.0.0.1:61420/callback?code=…&state=…auf.
- Der Systembrowser ruft
- (B) Die App schickt Code,
code_verifierund Redirect-URI an den Token-Endpunkt von Keycloak. - (C) Die App erzeugt
code_verifier,stateundnonceund startet einen HTTP-Server auf127.0.0.1mit Port 0. - (D) Aylin gibt im Browser Pseudonym und Passwort ein.
- (E) Die App prüft das ID Token und zeigt „Angemeldet als Morgenmuffel · NUTZER“.
- (F) Die App öffnet die Anmeldeadresse mit
code_challengeim Standardbrowser. - (G) Die App vergleicht den
stateaus dem Rückruf mit ihrem eigenen. - (H) Keycloak antwortet mit Access Token, Refresh Token und ID Token.
- Kreuze an, welcher Wert an welcher Stelle über die Leitung geht. Ein Wert kann an mehreren Stellen vorkommen. (5 Punkte)
| Wert | in der Anmeldeadresse (Browser) | im Rückruf an 127.0.0.1 | in der Anfrage an den Token-Endpunkt |
|---|---|---|---|
code_challenge |
|||
code_verifier |
|||
state |
|||
code |
|||
nonce |
Nenne für jedes der drei Tokens, wer es auswertet und wofür die Desktop-App es nutzt. (3 Punkte)
Ein Mitglied des Übungsteams schlägt vor, die Anmeldeseite in einem
WebViewim Gesuche-Fenster anzuzeigen, „damit Aylin nicht in den Browser wechseln muss“. Nenne zwei Gründe, die dagegen sprechen. (2 Punkte)
Reihenfolge: C, F, D, A, G, B, H, E (4 Punkte; je vertauschtem Paar 1 Punkt Abzug). Die Prüfung des
state(G) muss vor dem Einlösen (B) liegen, sonst würde die App auch einen untergeschobenen Code einlösen.Je richtig ausgefüllter Zeile 1 Punkt:
| Wert | in der Anmeldeadresse (Browser) | im Rückruf an 127.0.0.1 | in der Anfrage an den Token-Endpunkt |
|---|---|---|---|
code_challenge |
x | ||
code_verifier |
x | ||
state |
x | x | |
code |
x | x | |
nonce |
x |
Der code_verifier geht als einziger Wert nie durch den Browser. Deshalb kann ein Programm, das den Rückruf abfängt, den Code nicht einlösen. Die nonce kommt nicht über den Rückruf zurück, sondern als Claim im ID Token, das die App vom Token-Endpunkt erhält.
Je Token 1 Punkt: Das Access Token wertet das Backend aus; die App schickt es nur im Header
Authorization: Bearer …mit. Das Refresh Token wertet Keycloak aus; die App holt damit ein neues Access Token. Das ID Token wertet die App selbst aus; sie liest daraus Pseudonym, Nutzer-ID und Rollen, nachdem sieiss,aud,expundnoncegeprüft hat.Je Grund 1 Punkt, z. B.: In einem eingebetteten Browser kann die App das Passwort mitlesen; Aylin kann nicht prüfen, ob sie wirklich auf der Seite von Keycloak tippt (keine Adresszeile, kein Zertifikat sichtbar); eine Anmeldung, die im Systembrowser schon besteht, wird nicht genutzt; RFC 8252 verbietet eingebettete Browser für native Apps ausdrücklich.
Aufgabe 2.1: Das Gesuche-Fenster nach Rolle steuern (Kernkompetenz)
(ca. 14 min Bearbeitungszeit)
Im Replenishment hat Heike Brinkmann aus dem Sekretariat (Pseudonym Sekretariat-HB, Rolle ORGA) gebeten, unpassende Gesuche aus der Liste entfernen zu können, etwa wenn jemand eine private Telefonnummer in die Notiz geschrieben hat. Das Übungsteam nimmt das als Ticket JFX-25 „Gesuche nach Rolle“ auf:
- Mit Rolle
NUTZERist „Gesuch eintragen …“ bedienbar, sonst gesperrt. - Mit Rolle
ORGAerscheint der Button „Gesuch entfernen“; ohneORGAist er unsichtbar und nimmt keinen Platz ein. Er ist nur bedienbar, wenn in der Tabelle ein Gesuch gewählt ist. - Das Entfernen geschieht vorerst nur in der Liste der App (wie das Eintragen); ohne
ORGAlehnt das ViewModel es ab.
Ergänze
GesucheViewModelum einen KonstruktorGesucheViewModel(GesuchQuelle quelle, Executor hintergrund, Sitzung sitzung)und die beiden schreibgeschützten PropertieseintragenErlaubtProperty()undverwaltungSichtProperty(). Der bisherige Konstruktor soll weiter funktionieren und Aylin Kaya (Morgenmuffel, Nutzer-ID 2, RolleNUTZER) annehmen. (4 Punkte)Schreibe die Methode
entfernen(Gesuch gesuch), die das Gesuch mit derselbenidaus der Liste nimmt und ohne RolleORGAeineIllegalStateExceptionwirft. (2 Punkte)Ergänze
gesuche.fxmlundGesucheControllerum den ButtongesuchEntfernenButtonund binde beide Buttons nach den Akzeptanzkriterien. (4 Punkte)Schreibe einen Komponententest
GesucheRollenTestmit mindestens drei Testfällen:NUTZER, nurORGA(mit erfolgreichem Entfernen) und Entfernen ohneORGA. (3 Punkte)Erläutere, warum JFX-25 trotz dieser Umsetzung erst „Fertig“ sein kann, wenn das Backend beteiligt ist. (2 Punkte)
- Felder, Konstruktoren und Properties (2 Punkte für die Ableitung aus der Sitzung, 1 Punkt für schreibgeschützte Properties, 1 Punkt für den alten Konstruktor):
private final Sitzung sitzung;
private final ReadOnlyBooleanWrapper eintragenErlaubt = new ReadOnlyBooleanWrapper(this, "eintragenErlaubt");
private final ReadOnlyBooleanWrapper verwaltungSicht = new ReadOnlyBooleanWrapper(this, "verwaltungSicht");
/** Ohne Anmeldung (Beispieldaten, ältere Tests): Aylin Kaya (Morgenmuffel) mit Rolle NUTZER. */
public GesucheViewModel(GesuchQuelle quelle, Executor hintergrund) {
this(quelle, hintergrund, new Sitzung("Morgenmuffel", 2, Set.of(Sitzung.ROLLE_NUTZER)));
}
public GesucheViewModel(GesuchQuelle quelle, Executor hintergrund, Sitzung sitzung) {
this.quelle = quelle;
this.hintergrund = hintergrund;
this.sitzung = sitzung;
eintragenErlaubt.set(sitzung.hatRolle(Sitzung.ROLLE_NUTZER));
verwaltungSicht.set(sitzung.hatRolle(Sitzung.ROLLE_ORGA));
// … Filter und Hinweis wie bisher
}
public ReadOnlyBooleanProperty eintragenErlaubtProperty() {
return eintragenErlaubt.getReadOnlyProperty();
}
public ReadOnlyBooleanProperty verwaltungSichtProperty() {
return verwaltungSicht.getReadOnlyProperty();
}- Je 1 Punkt für Rollenprüfung und Entfernen über die
id:
public void entfernen(Gesuch gesuch) {
if (!sitzung.hatRolle(Sitzung.ROLLE_ORGA)) {
throw new IllegalStateException("Nur die Schulverwaltung (Rolle ORGA) darf Gesuche entfernen.");
}
gesuche.removeIf(g -> g.id() == gesuch.id());
}- FXML (1 Punkt) und Bindungen (je Kriterium 1 Punkt):
<Button fx:id="gesuchEntfernenButton" text="Gesuch ent_fernen" mnemonicParsing="true"
onAction="#gesuchEntfernen" GridPane.columnIndex="1" GridPane.rowIndex="6"/>@FXML private Button gesuchEintragenButton;
@FXML private Button gesuchEntfernenButton;
// in initialize():
gesuchEintragenButton.disableProperty().bind(viewModel.eintragenErlaubtProperty().not());
gesuchEntfernenButton.visibleProperty().bind(viewModel.verwaltungSichtProperty());
gesuchEntfernenButton.managedProperty().bind(viewModel.verwaltungSichtProperty());
gesuchEntfernenButton.disableProperty().bind(
gesucheTabelle.getSelectionModel().selectedItemProperty().isNull());
@FXML
private void gesuchEntfernen() {
Gesuch gewaehlt = gesucheTabelle.getSelectionModel().getSelectedItem();
if (gewaehlt != null) {
viewModel.entfernen(gewaehlt);
}
}Ohne managedProperty bliebe an der Stelle des unsichtbaren Buttons eine Lücke.
- Je Testfall 1 Punkt. Weil
laden()einenTaskstartet, dessen Ergebnis auf dem JavaFX Application Thread ankommt, braucht der Test die gestartete Plattform (HilfsklasseFxPlattformaus den früheren Übungen):
class GesucheRollenTest {
@BeforeAll
static void plattformStarten() throws InterruptedException {
FxPlattform.starten();
}
private static GesucheViewModel viewModel(String... rollen) throws Exception {
GesucheViewModel vm = new GesucheViewModel(Beispielgesuche::offeneGesuche, Runnable::run,
new Sitzung("Testperson", 99, Set.of(rollen)));
FxPlattform.aufFxThread(() -> {
vm.laden();
return null;
});
FxPlattform.warten();
return vm;
}
@Test
void nutzerDarfEintragenSiehtAberKeineVerwaltung() throws Exception {
GesucheViewModel vm = viewModel("NUTZER");
assertTrue(vm.eintragenErlaubtProperty().get());
assertFalse(vm.verwaltungSichtProperty().get());
}
@Test
void orgaOhneNutzerDarfNichtEintragenAberEntfernen() throws Exception {
GesucheViewModel vm = viewModel("ORGA");
assertFalse(vm.eintragenErlaubtProperty().get());
assertTrue(vm.verwaltungSichtProperty().get());
int vorher = vm.getPassendeGesuche().size();
vm.entfernen(vm.getPassendeGesuche().get(0));
assertEquals(vorher - 1, vm.getPassendeGesuche().size());
}
@Test
void entfernenOhneOrgaWirdAbgelehntUndAendertNichts() throws Exception {
GesucheViewModel vm = viewModel("NUTZER");
int vorher = vm.getPassendeGesuche().size();
assertThrows(IllegalStateException.class, () -> vm.entfernen(vm.getPassendeGesuche().get(0)));
assertEquals(vorher, vm.getPassendeGesuche().size());
}
}- 2 Punkte: Das Entfernen wirkt bisher nur in der Liste der App; nach dem nächsten Laden ist das Gesuch wieder da, für andere Nutzer*innen war es nie weg. Außerdem schützt die Prüfung in der App nicht: Wer die REST-API direkt aufruft, umgeht sie. Fertig ist das Ticket erst mit einem Endpunkt im Backend (z. B.
DELETE /gesuche/{id}), der die RolleORGAim Access Token prüft und sonst403liefert, und mit einem Integrationstest für diese Ablehnung.
Aufgabe 2.2: Einen Token-Speicher schreiben (Kernkompetenz)
(ca. 12 min Bearbeitungszeit)
Das Gesuche-Fenster soll die Tokens nicht über die Klasse Anmeldung verwalten, sondern über einen eigenen, kleinen TokenSpeicher im Paket …desktop.anmeldung. Er kennt zusätzlich, wie lange das Refresh Token gilt; Keycloak liefert dafür refresh_expires_in (im Realm 1800 Sekunden). Vereinfachend kennt der Speicher nur den Ablauf des ersten Refresh Tokens; Keycloak verlängert ihn bei Nutzung. Ist das Refresh Token abgelaufen, soll der Speicher Keycloak gar nicht erst fragen.
public class TokenSpeicher {
public TokenSpeicher(TokenDienst tokenDienst, Clock uhr, Duration puffer) { … }
public void speichern(Tokens neu, Instant refreshGueltigBis) { … }
public String gueltigesAccessToken() { … }
public void vergessen() { … }
}- Schreibe die Klasse.
gueltigesAccessToken()soll
- ohne gespeicherte Tokens einen
AnmeldeFehlermit dem Codenicht_angemeldetwerfen, - das gespeicherte Access Token liefern, solange es länger als
puffergilt, - ein abgelaufenes Refresh Token erkennen, die Tokens vergessen und einen
AnmeldeFehlermit dem Codeinvalid_grantwerfen, - sonst über
tokenDienst.erneuern(…)neue Tokens holen und ein neues Refresh Token übernehmen; schickt Keycloak keines, bleibt das alte. (7 Punkte)
Schreibe Komponententests mit einer Uhr, die der Test vorstellen kann, und einem Ersatz-
TokenDienst, der seine Aufrufe mitschreibt. Teste mindestens: kein Aufruf bei gültigem Token, Erneuern innerhalb des Puffers mit Weitergabe des neuen Refresh Tokens, abgelaufenes Refresh Token ohne Aufruf bei Keycloak. (4 Punkte)Erläutere, warum
gueltigesAccessToken()alssynchronizeddeklariert sein sollte, obwohl die Oberfläche nur einen Thread hat. (2 Punkte)
- Je Anforderung 1 bis 2 Punkte (ohne Tokens 1, gültig 1, abgelaufenes Refresh Token 2, Erneuern 2, altes Refresh Token behalten 1):
public class TokenSpeicher {
private final TokenDienst tokenDienst;
private final Clock uhr;
private final Duration puffer;
private Tokens tokens;
private Instant refreshAblauf;
public TokenSpeicher(TokenDienst tokenDienst, Clock uhr, Duration puffer) {
this.tokenDienst = tokenDienst;
this.uhr = uhr;
this.puffer = puffer;
}
public synchronized void speichern(Tokens neu, Instant refreshGueltigBis) {
tokens = neu;
refreshAblauf = refreshGueltigBis;
}
public synchronized String gueltigesAccessToken() {
if (tokens == null) {
throw new AnmeldeFehler("nicht_angemeldet", "Bitte zuerst anmelden.");
}
Instant jetzt = uhr.instant();
if (!tokens.laeuftBaldAb(jetzt, puffer)) {
return tokens.accessToken();
}
if (tokens.refreshToken() == null || !jetzt.isBefore(refreshAblauf)) {
vergessen();
throw new AnmeldeFehler("invalid_grant", "Die Anmeldung ist abgelaufen. Bitte neu anmelden.");
}
Tokens neu = tokenDienst.erneuern(tokens.refreshToken());
// Rotation: neues Refresh Token übernehmen, sonst das alte behalten
tokens = new Tokens(neu.accessToken(),
neu.refreshToken() != null ? neu.refreshToken() : tokens.refreshToken(),
tokens.idToken(), neu.ablauf());
return tokens.accessToken();
}
public synchronized void vergessen() {
tokens = null;
refreshAblauf = null;
}
}Ein Fehler, der hier oft passiert: Wer das Refresh Token erst nach dem Aufruf von erneuern prüft, schickt ein abgelaufenes Token an Keycloak und bekommt dieselbe Antwort, aber mit einer unnötigen Anfrage.
Die Vereinfachung aus der Aufgabenstellung hat eine Folge: Der Speicher verlangt 30 Minuten nach der Anmeldung eine neue Anmeldung, auch wenn die App ständig genutzt wird. Keycloak schickt beim Erneuern ein neues refresh_expires_in; ein vollständiger Speicher würde refreshAblauf damit bei jedem Erneuern neu setzen.
- Je Testfall 1 Punkt, 1 Punkt für Ersatzobjekte. Die
TestUhrstammt ausAnmeldungTestim Code-Stand des Artikels:
class TokenSpeicherTest {
private final AnmeldungTest.TestUhr uhr = new AnmeldungTest.TestUhr();
private final List<String> aufrufe = new ArrayList<>();
private final TokenDienst keycloak = new TokenDienst() {
@Override public Tokens codeEinloesen(String code, URI redirectUri, String codeVerifier) {
throw new UnsupportedOperationException();
}
@Override public Tokens erneuern(String refreshToken) {
aufrufe.add("erneuern " + refreshToken);
return new Tokens("access-2", "refresh-2", null, uhr.jetzt.plusSeconds(300));
}
@Override public void abmelden(String refreshToken) {
}
};
private TokenSpeicher speicherMitTokens() {
TokenSpeicher speicher = new TokenSpeicher(keycloak, uhr, Duration.ofSeconds(30));
speicher.speichern(new Tokens("access-1", "refresh-1", null, uhr.jetzt.plusSeconds(300)),
uhr.jetzt.plusSeconds(1800));
return speicher;
}
@Test
void gueltigesTokenOhneAufrufBeiKeycloak() {
TokenSpeicher speicher = speicherMitTokens();
uhr.jetzt = uhr.jetzt.plusSeconds(269);
assertEquals("access-1", speicher.gueltigesAccessToken());
assertTrue(aufrufe.isEmpty());
}
@Test
void innerhalbDesPuffersWirdErneuertUndDasNeueRefreshTokenGenutzt() {
TokenSpeicher speicher = speicherMitTokens();
uhr.jetzt = uhr.jetzt.plusSeconds(271);
assertEquals("access-2", speicher.gueltigesAccessToken());
uhr.jetzt = uhr.jetzt.plusSeconds(280);
speicher.gueltigesAccessToken();
assertEquals(List.of("erneuern refresh-1", "erneuern refresh-2"), aufrufe);
}
@Test
void abgelaufenesRefreshTokenFragtKeycloakNichtUndVerlangtNeueAnmeldung() {
TokenSpeicher speicher = speicherMitTokens();
uhr.jetzt = uhr.jetzt.plusSeconds(1800);
AnmeldeFehler fehler = assertThrows(AnmeldeFehler.class, speicher::gueltigesAccessToken);
assertTrue(fehler.neuAnmeldenNoetig());
assertTrue(aufrufe.isEmpty());
}
}Die Beispiellösung im Code-Stand enthält zwei weitere Tests (kein neues Refresh Token, ohne Anmeldung).
- 2 Punkte: Die Anfragen an das Backend laufen nicht im JavaFX Application Thread, sondern in Hintergrund-Threads der
Tasks; in der Desktop-App können das mehrere gleichzeitig sein (Angebote und Buchungen laden). Fragen zwei Threads zugleich nach einem Token, das bald abläuft, würden ohnesynchronizedbeide erneuern. Das kostet mindestens eine überflüssige Anfrage. Ist im Realm zusätzlich eingestellt, dass jedes Refresh Token nur einmal gilt (Einstellung „Revoke Refresh Token“), schickt der zweite Thread ein schon verbrauchtes Token, Keycloak antwortet mitinvalid_grant, und Aylin müsste sich ohne Grund neu anmelden.
Aufgabe 3.1: Redirect-Fehler anhand ihrer Meldungen finden (Profilkompetenz)
(ca. 17 min Bearbeitungszeit)
Im Übungsteam hat jede Person die Anmeldung für das Gesuche-Fenster selbst nachgebaut. Der Client mitfahrlabor-desktop im Realm ist bei allen gleich (Redirect-URI http://127.0.0.1/callback, PKCE S256). Vier Fassungen scheitern; jede enthält genau einen absichtlich eingebauten Fehler. Die Ausschnitte zeigen nur die Stelle, an der sie vom Code-Stand des Artikels abweichen.
Fassung 1: Der Browser öffnet sich und zeigt sofort die Fehlerseite von Keycloak „Invalid parameter: redirect_uri“.
// ABSICHTLICH FEHLERHAFT (Fassung 1), in LoopbackEmpfaenger
public URI redirectUri() {
return URI.create("http://localhost:" + server.getAddress().getPort() + PFAD);
}Fassung 2: Die Anmeldeseite erscheint, nach Pseudonym und Passwort zeigt der Browser aber „Verbindung abgelehnt“ für http://127.0.0.1:50817/callback?state=…&code=…. Die App meldet nach fünf Minuten „Im Browser wurde keine Anmeldung abgeschlossen.“
// ABSICHTLICH FEHLERHAFT (Fassung 2), in Anmeldung.anmelden
URI redirectUri;
CompletableFuture<Rueckruf> antwort;
try (LoopbackEmpfaenger empfaenger = LoopbackEmpfaenger.starten()) {
redirectUri = empfaenger.redirectUri();
antwort = empfaenger.rueckruf();
}
browser.accept(konfiguration.anmeldeAdresse(redirectUri, state, nonce, pkce));
Rueckruf rueckruf = antwort.get(wartezeit.toMillis(), TimeUnit.MILLISECONDS);Fassung 3: Nach der Anmeldung zeigt der Browser „Anmeldung abgeschlossen“. Die App meldet invalid_grant: Incorrect redirect_uri.
// ABSICHTLICH FEHLERHAFT (Fassung 3), in Anmeldung.anmelden
Tokens neu = tokenDienst.codeEinloesen(rueckruf.code(),
URI.create("http://127.0.0.1" + LoopbackEmpfaenger.PFAD), pkce.codeVerifier());Fassung 4: Auch hier zeigt der Browser „Anmeldung abgeschlossen“. Die App meldet invalid_grant: PKCE verification failed: Code mismatch. Ein zweiter Versuch mit demselben Code endet mit invalid_grant: Code not valid.
// ABSICHTLICH FEHLERHAFT (Fassung 4), in Anmeldung.anmelden
Tokens neu = tokenDienst.codeEinloesen(rueckruf.code(), redirectUri, Pkce.erzeugen(zufall).codeVerifier());Erläutere für jede Fassung, warum der Fehler genau dieses Symptom erzeugt, und gib die korrigierte Stelle an. (12 Punkte)
Eine fünfte Fassung meldet sich fehlerfrei an. Etwa fünf Minuten nach dem Start schlägt aber jedes Laden mit
401 Unauthorizedfehl, bis die App neu gestartet wird. Erläutere die Ursache in dieser Zeile und korrigiere sie. (3 Punkte)
// ABSICHTLICH FEHLERHAFT (Fassung 5), beim Anlegen des ApiClient
String token = anmeldung.accessToken();
client.setRequestInterceptor(anfrage -> anfrage.header("Authorization", "Bearer " + token));- Nach dem Ärger mit Fassung 1 schlägt jemand vor, im Realm
"redirectUris": ["*"]einzutragen, „dann gibt es nie wieder Redirect-Fehler“. Beurteile den Vorschlag. (3 Punkte)
- Je Fassung 3 Punkte (Ursache 2, Korrektur 1):
- Fassung 1: Keycloak vergleicht die
redirect_urider Anmeldeadresse mit den Redirect-URIs des Clients. Den Port ignoriert es nur bei den Loopback-Literalen127.0.0.1und[::1];localhostist ein anderer Host und passt nicht. Keycloak bricht deshalb schon vor der Anmeldeseite ab und leitet bewusst nicht um, weil es einer unbekannten Adresse keinen Fehler zuschicken will. Korrektur:"http://127.0.0.1:" + port + PFAD. Den Client umlocalhostzu erweitern, wäre die schlechtere Lösung, weillocalhostje nach System zu::1aufgelöst werden kann, während der Server auf127.0.0.1lauscht. - Fassung 2: Der
try-with-resources-Block schließt den Empfänger, sobald er endet, also noch bevor der Browser überhaupt geöffnet ist. Wenn Keycloak später auf127.0.0.1:50817umleitet, lauscht dort niemand mehr: Der Browser bekommt „Verbindung abgelehnt“, das Future wird nie erfüllt, und die App wartet bis zum Zeitlimit. Korrektur: Browser öffnen, auf den Rückruf warten und Code einlösen innerhalb destry-Blocks wie im Code-Stand des Artikels. - Fassung 3: Beim Einlösen muss die
redirect_uriZeichen für Zeichen der aus der Anmeldeadresse entsprechen (RFC 6749, Abschnitt 4.1.3). Dort standhttp://127.0.0.1:50817/callback, beim Einlösen fehlt der Port. Die Lockerung „Port egal“ gilt nur für den Vergleich mit der Liste des Clients, nicht für diesen Vergleich. Korrektur: dieselberedirectUriausempfaenger.redirectUri()übergeben. - Fassung 4: Keycloak hat sich zur Anmeldung die
code_challengegemerkt, also den Hashwert des ersten Verifiers. Die App schickt einen neu erzeugten Verifier, dessen Hashwert nicht passt. Genau dafür ist PKCE da: Ein Code lässt sich nur mit dem passenden Verifier einlösen. Weil ein Code nur einmal eingelöst werden kann, ist er danach verbraucht („Code not valid“). Korrektur:pkce.codeVerifier()aus derselben Anmeldung übergeben.
- 3 Punkte: Das Access Token wird einmal beim Anlegen des Clients gelesen und dann im Lambda festgehalten (Variable
token). Nach fünf Minuten (accessTokenLifespan300 Sekunden) ist es abgelaufen, die App schickt es trotzdem weiter, und das Backend antwortet mit401. Das Refresh Token wird nie benutzt. Korrektur:accessToken()im Lambda aufrufen, damit jede Anfrage ein gültiges und bei Bedarf erneuertes Token bekommt:
client.setRequestInterceptor(anfrage -> anfrage.header("Authorization", "Bearer " + anmeldung.accessToken()));- 3 Punkte für eine begründete Ablehnung: Mit
*leitet Keycloak einen Code an jede Adresse weiter, die in der Anmeldeadresse steht. Ein Angreifer kann dann eine Anmeldeadresse mit seiner eigenen Redirect-URI bauen und Aylin zuschicken; nach der Anmeldung landet der Code bei ihm. PKCE hilft dagegen nicht: Der Angreifer hat die Anmeldeadresse selbst gebaut und kennt deshalb den passendencode_verifier. Er löst den Code ein und bekommt Tokens für Aylins Konto. Die Liste der Redirect-URIs ist die einzige Stelle, an der Keycloak erkennt, dass ein Code wirklich zur App gehört. Richtig ist, die Redirect-URI so eng wie möglich anzugeben (http://127.0.0.1/callback) und den Fehler in der App zu beheben.
Links und weitere Informationen
zum Artikel: Anmeldung einer Desktop-App mit Keycloak: Authorization Code mit PKCE über den Systembrowser
zum Board: Kanban-Projektstart für die Desktop-App
als Standard: RFC 8252: OAuth 2.0 for Native Apps
als Standard: RFC 7636: Proof Key for Code Exchange (PKCE)
für den Einstieg: Szenario Mitfahrlabor mit Personas und Regeln
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: “Übungsaufgaben zur Anmeldung mit Keycloak: Mitfahrgesuche nach Rolle” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/javafx-anmeldung-keycloak-uebungsaufgaben 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]
