Passkey statt Passwort: Funktionsweise und Integration

1. Übersicht

Passwörter haben drei große Probleme: Sie sind leicht zu erraten, werden überall wiederverwendet und können leicht auf gefälschte Seiten eingegeben werden. Passkeys lösen alle drei Probleme durch Public-Key-Kryptografie. Der Server speichert kein Geheimnis des Benutzers mehr.

Dieser Artikel erklärt, was Passkeys sind und wie die Registrierungs- und Anmeldungsabläufe funktionieren. Anschließend aktivieren Sie Passkeys für eine Spring-Boot-3.x-Anwendung mit Spring Security. Am Ende folgen häufige Fehler und Fälle, in denen man Passkeys (noch) nicht verwenden sollte.

2. Was sind Passkeys?

Ein Passkey ist ein WebAuthn-Credential, also ein Credential nach dem FIDO2-Standard. Im Wesentlichen ist es ein Schlüsselpaar:

  • Privater Schlüssel befindet sich auf dem Gerät des Benutzers (Handy, Laptop, USB-Sicherheitsschlüssel) oder in einem Passwort-Manager. Dieser Schlüssel verlässt es niemals.
  • Öffentlicher Schlüssel wird bei der Registrierung an den Server gesendet. Der Server speichert diesen Schlüssel zusammen mit dem Konto.

Jedes Schlüsselpaar ist an eine Relying Party (RP), also Ihre Website oder Anwendung, durch die RP-ID gebunden. Die RP-ID ist normalerweise der Domänenname, beispielsweise shop.example.com.

Es gibt zwei Haupttypen von Passkeys:

Typ Besonderheiten
Synchronisierter Passkey Wird über das Cloud-Konto des Betriebssystems oder des Password-Managers synchronisiert. Kann auch nach Geräteverlust wiederhergestellt werden.
Gerätegebundener Passkey Existiert nur auf einem Gerät, z. B. ein Hardware-Sicherheitsschlüssel. Sicherer, aber Geräteverlust bedeutet Schlüsselverlust.

Der Benutzer entsperrt den Passkey mit dem Fingerabdruck, Gesicht oder der PIN des Geräts. Biometrische Daten werden nur für die lokale Entsperrung verwendet. Sie werden nicht an den Server gesendet.

3. Wie Passkeys funktionieren

3.1. Registrierung

  1. Der Server erstellt eine zufällige Challenge. Der Server sendet die Challenge zusammen mit RP- und Benutzerinformationen an den Browser.
  2. Der Browser ruft navigator.credentials.create() auf. Der Authenticator generiert ein neues Schlüsselpaar für die entsprechende RP-ID.
  3. Der Browser sendet den öffentlichen Schlüssel, die Credential-ID und die signierten Daten an den Server.
  4. Der Server überprüft die Challenge und den Origin und speichert dann den öffentlichen Schlüssel.

3.2. Authentifizierung

  1. Der Server erstellt eine neue Challenge.
  2. Der Browser ruft navigator.credentials.get() auf. Der Authenticator signiert die Challenge mit dem privaten Schlüssel.
  3. Der Server überprüft die Signatur mit dem gespeicherten öffentlichen Schlüssel.

Der Kern ist “mit dem privaten Schlüssel signieren, mit dem öffentlichen Schlüssel überprüfen”. Der folgende reine Java-Code demonstriert genau diesen Schritt:

import java.security.*;

public class PasskeyConcept {

    public static void main(String[] args) throws Exception {
        // Das Gerät generiert ein Schlüsselpaar bei der Registrierung (EC P-256, entspricht dem verbreiteten ES256 in WebAuthn)
        KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
        generator.initialize(256);
        KeyPair deviceKeys = generator.generateKeyPair();
        PublicKey storedOnServer = deviceKeys.getPublic();

        // Der Server erstellt eine zufällige Challenge für die Anmeldung
        byte[] challenge = randomBytes();

        // Das Gerät signiert die Challenge mit dem privaten Schlüssel
        Signature signer = Signature.getInstance("SHA256withECDSA");
        signer.initSign(deviceKeys.getPrivate());
        signer.update(challenge);
        byte[] signature = signer.sign();

        // Der Server überprüft mit dem gespeicherten öffentlichen Schlüssel
        System.out.println("Challenge korrekt: " + verify(storedOnServer, challenge, signature));
        System.out.println("Andere Challenge: " + verify(storedOnServer, randomBytes(), signature));
    }

    static boolean verify(PublicKey key, byte[] data, byte[] signature) throws Exception {
        Signature verifier = Signature.getInstance("SHA256withECDSA");
        verifier.initVerify(key);
        verifier.update(data);
        return verifier.verify(signature);
    }

    static byte[] randomBytes() {
        byte[] bytes = new byte[32];
        new SecureRandom().nextBytes(bytes);
        return bytes;
    }
}

Ergebnis:

Challenge korrekt: true
Andere Challenge: false

Aus diesem Ergebnis können Sie zwei Dinge ableiten:

  • Der Server speichert nur storedOnServer, also den öffentlichen Schlüssel. Selbst wenn ein Angreifer die Datenbank erhält, kann er sich nicht anmelden.
  • Die Signatur ist nur für die Challenge dieses Moments gültig. Selbst wenn ein Angreifer eine alte Signatur abfängt und erneut sendet (Replay-Angriff), kann er die Verifikation nicht bestehen.

In echtem WebAuthn enthalten die signierten Daten auch den Origin der Seite. Der Browser füllt den Origin automatisch aus, die Website kann ihn nicht fälschen. Wenn der Benutzer versehentlich examp1e.com besucht, verwendet der Browser den Passkey von example.com nicht. Phishing wird auf Protokollebene blockiert und ist nicht von der Aufmerksamkeit des Benutzers abhängig.

4. Warum Passkeys immer mehr Aufmerksamkeit bekommen

  • Phishing-Schutz durch Design. Passwörter und OTPs können auf gefälschte Seiten eingegeben werden. Passkeys nicht.
  • Keine Geheimnisse auf dem Server. Datenlecks offenbaren keine Anmeldeinformationen.
  • Bessere Benutzerfreundlichkeit. Der Benutzer muss nur einen Fingerabdruck scannen, kein Passwort merken oder auf SMS warten.
  • Das Ökosystem ist bereit. Beliebte Betriebssysteme, Browser und Passwort-Manager unterstützen alle Passkey-Synchronisierung. Daher ist Geräteverlust kein so großes Hindernis mehr wie früher.
  • Framework-Unterstützung ist vorhanden. Sie müssen die kryptografische Verifikation nicht selbst schreiben, was sehr fehleranfällig wäre.

5. Passkeys mit Spring Security integrieren

Ab Spring Security 6.4 (zusammen mit Spring Boot 3.4 und neuer) hat das Framework ein fertiges DSL webAuthn(). Intern verwendet die Verifikation die WebAuthn4J-Bibliothek.

5.1. Abhängigkeiten

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>com.webauthn4j</groupId>
        <artifactId>webauthn4j-core</artifactId>
        <version>${webauthn4j.version}</version>
    </dependency>
</dependencies>

Sie deklarieren die Property webauthn4j.version mit einer Version, die mit der verwendeten Spring Security kompatibel ist. Die Spring-Security-Dokumentation gibt die getesteten Versionen an.

5.2. SecurityFilterChain konfigurieren

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .formLogin(Customizer.withDefaults())
            .webAuthn(webAuthn -> webAuthn
                .rpName("Nón Lá Shop")
                .rpId("localhost")
                .allowedOrigins("http://localhost:8080")
            );
        return http.build();
    }

    @Bean
    UserDetailsService userDetailsService() {
        UserDetails customer = User.withUsername("minh")
                .password("{noop}matkhau-tam")
                .roles("CUSTOMER")
                .build();
        return new InMemoryUserDetailsManager(customer);
    }
}

Bedeutung der einzelnen Teile:

  • rpName: Name angezeigt im Passkey-Dialog des Betriebssystems.
  • rpId: Der Domänenname, an den der Passkey gebunden ist. Dieser Wert hat kein Schema oder Port. In Produktion wäre das shop.example.com.
  • allowedOrigins: gültige Origins. Der Origin muss Schema und Port genau abgleichen.
  • formLogin: wird beibehalten. Der Benutzer muss sich zunächst mit dem Passwort anmelden, um einen Passkey zu registrieren. Passkey ist ein zusätzliches Credential zu einem bestehenden Konto.

Beim Ausführen der Anwendung erstellt Spring Security automatisch die folgenden Endpoints:

GET  /webauthn/register              Passkey-Verwaltungs-/Registrierungsseite (Standard)
POST /webauthn/register/options      Registrierungs-Challenge erstellen
POST /webauthn/register              Neuen öffentlichen Schlüssel speichern
POST /webauthn/authenticate/options  Authentifizierungs-Challenge erstellen
POST /login/webauthn                 Signatur überprüfen und anmelden

Die Standard-Login-Seite hat einen zusätzlichen Button “Mit Passkey anmelden”. Der Test-Ablauf hat vier Schritte:

  1. Melden Sie sich mit minh / matkhau-tam an.
  2. Öffnen Sie /webauthn/register und erstellen Sie einen Passkey.
  3. Melden Sie sich ab.
  4. Melden Sie sich mit dem Passkey an.

5.3. Credential speichern

Standardmäßig speichert Spring Security Credentials im Speicher. Nach einem Neustart sind alle Passkeys verloren. In Produktion müssen Sie Beans bereitstellen, die zwei Schnittstellen implementieren:

  • UserCredentialRepository: speichert öffentlichen Schlüssel, Credential-ID und Signatur-Zähler.
  • PublicKeyCredentialUserEntityRepository: ordnet Ihre Benutzer den WebAuthn-Benutzer-Handles zu.

Neue Versionen von Spring Security haben fertige JDBC-Implementierungen für beide Schnittstellen. Sie können auch Ihre eigene Implementierung mit JPA schreiben. Dabei sollte jedes Credential ein separater Datensatz sein, da ein Benutzer normalerweise mehrere Passkeys auf mehreren Geräten hat.

6. Browser-Seite

Wenn Sie die Standard-Login-Seite verwenden, kümmert sich Spring Security um das JavaScript. Mit einer benutzerdefinierten Oberfläche sieht der Anmeldungsablauf wie folgt aus:

async function loginWithPasskey() {
  const csrf = document.querySelector('meta[name="_csrf"]').content;

  // 1. Challenge vom Server anfordern
  const optionsRes = await fetch('/webauthn/authenticate/options', {
    method: 'POST',
    headers: { 'X-CSRF-TOKEN': csrf }
  });
  const options = PublicKeyCredential.parseRequestOptionsFromJSON(await optionsRes.json());

  // 2. Browser zeigt Dialog, Authenticator signiert Challenge
  const credential = await navigator.credentials.get({ publicKey: options });

  // 3. Signatur zur Verifikation an den Server senden
  const res = await fetch('/login/webauthn', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
    body: JSON.stringify(credential.toJSON())
  });
  if (res.ok) window.location.href = '/';
}

Die Schritte im Code:

  • Schritt 1 ruft die Challenge ab. Die Anfrage muss ein CSRF-Token enthalten, da es sich um einen POST handelt.
  • parseRequestOptionsFromJSON konvertiert Base64url-Felder in ArrayBuffer. Ältere Browser unterstützen diese Funktion möglicherweise nicht. In diesem Fall benötigen Sie eine Helper-Bibliothek oder müssen selbst dekodieren.
  • Schritt 3 sendet das Ergebnis an den Server. Das JSON-Format muss mit dem vom Server erwarteten Format übereinstimmen. Das Skript auf der Standard-Login-Seite von Spring Security ist die beste Referenz zum Vergleich.

7. Testen

Sie können einen echten Authenticator in einem Unit-Test nicht einfach simulieren. Sie können aber den wichtigen Server-Teil testen: der Endpoint erstellt Challenges und gibt jedes Mal eine andere zurück.

@SpringBootTest
@AutoConfigureMockMvc
class PasskeyEndpointTest {

    @Autowired
    MockMvcTester mvc;

    @Test
    void authenticationOptions_containRpIdAndFreshChallenge() {
        String first = requestChallenge();
        String second = requestChallenge();

        assertThat(first).isNotBlank();
        assertThat(second).isNotEqualTo(first);
    }

    @Test
    void authenticationOptions_useConfiguredRpId() {
        assertThat(mvc.post().uri("/webauthn/authenticate/options").with(csrf()))
                .hasStatusOk()
                .bodyJson()
                .extractingPath("$.rpId")
                .isEqualTo("localhost");
    }

    private String requestChallenge() {
        MvcTestResult result = mvc.post()
                .uri("/webauthn/authenticate/options")
                .with(csrf())
                .exchange();
        assertThat(result).hasStatusOk();
        return JsonPath.read(result.getResponse().getContentAsString(), "$.challenge");
    }
}

MockMvcTester (Spring Framework 6.2+) ermöglicht Assertions im AssertJ-Stil. csrf() kommt von SecurityMockMvcRequestPostProcessors. Der erste Test bestätigt, dass Challenges nicht wiederverwendet werden, dies ist der Replay-Schutzmechanismus. Der zweite Test stellt sicher, dass rpId richtig konfiguriert ist. Dies ist der häufigste Konfigurationsfehler beim Deployment.

Für End-to-End-Tests verwenden Sie den virtuellen Authenticator des Browsers. Chrome DevTools haben einen WebAuthn-Tab, und WebDriver/Playwright unterstützen diese Funktion auch.

8. Häufige Fehler und wann man Passkeys nicht verwenden sollte

8.1. Häufige Fehler

  • Falsche rpId oder allowedOrigins. Das Schema https:// in rpId zu schreiben oder den Port im Origin zu vergessen, führt dazu, dass Browser oder Server ablehnen. Sie sollten diese beiden Werte in application.yaml pro Umgebung getrennt halten.
  • HTTPS nicht verwenden. WebAuthn läuft nur in einem sicheren Kontext. localhost ist die einzige Ausnahme.
  • Domain-Wechsel. Passkeys sind an die RP-ID gebunden. Wenn Sie die primäre Domain wechseln, funktionieren alle alten Passkeys nicht mehr.
  • In-Memory-Speicherung in Produktion. Ein Neustart und die Benutzer verlieren ihre Passkeys.
  • Nur einen Passkey registrieren zulassen. Benutzer benötigen mehrere Passkeys für mehrere Geräte. Implementieren Sie eine Seite zum Verwalten und Löschen von einzelnen Passkeys.

Hinweis: Passkeys sind nur so stark wie Ihr Wiederherstellungsprozess. Wenn das Zurücksetzen eines Passkeys nur einen Link per E-Mail erfordert, werden Angreifer Ihre E-Mail statt des Passkeys angreifen.

8.2. Wann man Passkeys nicht verwenden sollte (oder nicht allein)

  • Benutzer auf alten oder gemeinsam genutzten Geräten, wie Kioske oder Computer an der Theke. Hier benötigen Sie eine Fallback-Methode.
  • Systeme, die nicht auf Browser oder Apps basieren, wie Service-zu-Service-APIs, CLIs oder Hintergrund-Jobs. Verwenden Sie hier OAuth2 Client Credentials, mTLS oder API Keys.
  • Compliance-Anforderungen, die nur Hardware-Schlüssel akzeptieren. Synchronisierte Passkeys erfüllen möglicherweise nicht die Anforderungen. Sie benötigen gerätegebundene Passkeys und müssen die Attestation überprüfen.
  • Ihr Team hat noch keinen Prozess für die Kontowiederherstellung. Sie sollten Passkeys zuerst als zusätzliche Option implementieren, bevor Sie Passwörter entfernen.

9. Fazit

Passkeys ersetzen geteilte Geheimnisse (Passwörter) durch Schlüsselpaare, die an einen Domainnamen gebunden sind. Dadurch eliminieren Passkeys Phishing, Credential Stuffing und Datenbanklek-Risiken durch einen einzigen Mechanismus. Mit Spring Security müssen Sie nur webAuthn() zur SecurityFilterChain hinzufügen, rpId und allowedOrigins richtig konfigurieren und den In-Memory-Credential-Speicher durch einen persistenten ersetzen.

Der sicherste Implementierungsweg ist schrittweise: Ermöglichen Sie Benutzern, Passkeys zu bestehenden Konten hinzuzufügen, entwerfen Sie einen strengen Wiederherstellungsprozess und erwägen Sie erst dann, die Passwort-Anmeldung zu deaktivieren.

passkeywebauthnspring-securitysicherheittech-trends