Keycloak: Passkey Auto-Login mit Passwort-Fallback

Keycloak 25.x Passkey-Login beschleunigen ohne die Passwort-Option zu verlieren. Theme-Override mit sessionStorage für den perfekten Kompromiss.

Passkeys sind die Zukunft der Authentifizierung: Kein Passwort merken, kein Phishing möglich, ein Fingerabdruck oder Face-ID und du bist drin. Aber was wenn der Hardware-Key gerade nicht griffbereit ist? Oder der Bluetooth-Dongle spinnt? Dann willst du auf Passwort-Login zurückfallen können — und genau hier hat Keycloak 25.x ein UX-Problem.

Das Problem#

Keycloak 25.x zeigt bei Usern mit registriertem Passkey eine Authenticator-Auswahl:

  1. Passkey (WebAuthn)
  2. Passwort

Um den Login-Flow zu beschleunigen, implementieren viele Admins ein Theme-Override das automatisch die Passkey-Option wählt. Das funktioniert — bis der User den Passkey-Dialog abbricht (ESC, “Abbrechen”, Hardware-Key nicht zur Hand).

Das Ergebnis: Der User landet wieder auf der Auswahl-Seite, aber das JavaScript klickt sofort wieder Passkey an. Eine Endlosschleife ohne Ausweg zum Passwort-Login.

Die Lösung: sessionStorage-Flag#

Wir nutzen sessionStorage, um zu merken ob der Auto-Select bereits einmal ausgeführt wurde. Beim zweiten Besuch der Seite (nach Abbruch) bleibt die Auswahl sichtbar.

Szenario Verhalten


Erster Login Passkey wird automatisch gewählt Nach Passkey-Abbruch Auswahl bleibt sichtbar, User kann Passwort wählen Neuer Browser-Tab Auto-Select wieder aktiv Browser geschlossen Auto-Select wieder aktiv

Voraussetzungen#

  • Keycloak 25.x (getestet mit 25.0.6)
  • Custom Theme mit Login-Overrides
  • Authentication Flow mit Passkey + Password Alternative

Falls du noch kein Custom Theme hast, hier die minimale Struktur:

themes/
└── mytheme/
    └── login/
        ├── theme.properties
        └── select-authenticator.ftl

theme.properties:

parent=keycloak.v2

Das Theme-Override#

Erstelle oder bearbeite select-authenticator.ftl in deinem Theme:

<#import "template.ftl" as layout>
<@layout.registrationLayout displayInfo=false; section>
    <#if section = "header" || section = "show-username">
        <#if section = "header">
            $
        </#if>
    <#elseif section = "form">

        <form id="kc-select-credential-form" class="$" action="$" method="post">
            <div class="$">
                <#list auth.authenticationSelections as authenticationSelection>
                    <button class="$" type="submit"
                            name="authenticationExecution" value="$"
                            data-icon-css="$">

                        <div class="$">
                            <i class="$']!authenticationSelection.iconCssClass} $"></i>
                        </div>
                        <div class="$">
                            <div class="$">
                                $')}
                            </div>
                            <div class="$">
                                $')}
                            </div>
                        </div>
                        <div class="$"></div>
                        <div class="$">
                            <i class="$"></i>
                        </div>
                    </button>
                </#list>
            </div>
        </form>

        <script>
            // Auto-select Passkey option to skip redundant method selector page.
            // Only auto-select ONCE per session - if user returns (e.g. cancelled Passkey),
            // they can manually choose Password.
            (function() {
                var STORAGE_KEY = 'kc-passkey-auto-selected';

                // Check if we already auto-selected in this session
                if (sessionStorage.getItem(STORAGE_KEY)) {
                    // User returned to this page - let them choose manually
                    return;
                }

                var buttons = document.querySelectorAll('#kc-select-credential-form button[name="authenticationExecution"]');
                for (var i = 0; i < buttons.length; i++) {
                    var iconCss = buttons[i].getAttribute('data-icon-css') || '';
                    if (iconCss.indexOf('WebAuthn') !== -1 || iconCss.indexOf('Passkey') !== -1) {
                        // Mark that we auto-selected, so next visit shows the selector
                        sessionStorage.setItem(STORAGE_KEY, 'true');
                        buttons[i].click();
                        return;
                    }
                }
            })();
        </script>

    </#if>
</@layout.registrationLayout>

Wie es funktioniert#

Das JavaScript macht folgendes:

  1. Prüft sessionStorage auf das Flag kc-passkey-auto-selected
  2. Falls nicht gesetzt: Sucht den Passkey-Button anhand data-icon-css, setzt das Flag, klickt den Button
  3. Falls gesetzt: Tut nichts — User sieht die Auswahl und kann manuell wählen

Warum sessionStorage statt localStorage?#

  • localStorage würde das Flag permanent speichern — User müssten immer manuell wählen
  • sessionStorage ist tab-spezifisch und wird beim Schließen gelöscht — perfekte Balance

Deployment#

# Theme-Dateien auf Server kopieren
scp -r themes/mytheme/login/* root@keycloak-server:/path/to/themes/mytheme/login/

# Keycloak restarten (Theme-Cache leeren!)
ssh root@keycloak-server "cd /path/to/keycloak && docker compose restart keycloak"

Wichtig: Keycloak cached Themes aggressiv. Ohne Restart siehst du keine Änderungen.

Testen#

  1. Login mit Passkey-Account → Passkey-Dialog sollte automatisch erscheinen
  2. Passkey abbrechen (ESC oder “Abbrechen”)
  3. Du landest auf der Auswahl → Jetzt kannst du “Passwort” wählen
  4. Neuer Tab öffnen → Auto-Select wieder aktiv

Alternative Ansätze#

Option A: Kein Auto-Select#

Einfachste Lösung: Das JavaScript komplett weglassen. User müssen immer klicken, haben aber volle Kontrolle.

Pro: Keine Überraschungen Contra: Ein Klick mehr für Passkey-User

Option B: Timeout mit Abbruch-Möglichkeit#

Auto-Select nach 2-3 Sekunden mit sichtbarem Countdown. User kann während des Countdowns “Passwort” wählen.

Pro: Visuelles Feedback Contra: Komplexer, langsamer

Option C: URL-Parameter#

?method=password in der URL überspringt Auto-Select. Nützlich für Bookmarks.

if (window.location.search.indexOf('method=password') !== -1) {
    return; // Kein Auto-Select
}

Keycloak 26.3+ : Conditional UI#

Ab Keycloak 26.3 ist das Problem nativ gelöst durch “Conditional UI” — der Browser fragt automatisch nach verfügbaren Passkeys ohne dass Keycloak einen Zwischenschritt zeigt.

Wenn du auf 26.3+ upgradest, kannst du die Theme-Overrides entfernen und die native Implementierung nutzen.

Troubleshooting#

Auto-Select funktioniert nicht#

  1. Browser-DevTools öffnen (F12)
  2. Console prüfen auf JavaScript-Fehler
  3. Network-Tab: Wird select-authenticator.ftl geladen?
  4. Keycloak restartet? Theme-Cache ist hartnäckig

User bleibt in Endlosschleife#

  • sessionStorage wird nicht gesetzt → JavaScript-Fehler prüfen
  • Falsches Template wird geladen → Theme-Zuordnung in Realm-Settings prüfen

Passkey-Dialog erscheint zweimal#

Das ist ein anderes Keycloak 25.x Problem. Dafür brauchst du zusätzlich ein Override für webauthn-authenticate.ftl das den Dialog automatisch triggert.


Zusammenfassung#

Mit diesem sessionStorage-Trick bekommst du das Beste aus beiden Welten:

  • Passkey-User: Schneller Login ohne extra Klicks
  • Passwort-Fallback: Jederzeit verfügbar nach Passkey-Abbruch
  • Neue Sessions: Auto-Select wieder aktiv

Ein kleines JavaScript-Snippet, großer UX-Gewinn.

Fragen oder Probleme? Schreib mir oder hinterlasse einen Kommentar!


Einige Links auf dieser Seite sind Affiliate-Links. Wenn du über diese Links einkaufst, erhalte ich eine kleine Provision — für dich ändert sich am Preis nichts. So unterstützt du diesen Blog und ermöglichst weitere kostenlose Tutorials. Danke!

[« Vorherige]
Die 15 besten Self-Hosted Dienste für dein Homelab (2026)