Passkey-Authentifizierung

In diesem Leitfaden wird beschrieben, wie Sie die Authentifizierung wiederkehrender Nutzer mit auffindbaren Anmeldedaten implementieren, sowohl über explizite Schaltflächen-Trigger als auch über nahtlose Vorschläge für die automatische Vervollständigung im Browser (Conditional UI).

Serverseitig

Optionen generieren

Erstellen Sie einen Endpunkt, der WebAuthn-Anfrageparameter mithilfe einer geprüften Bibliothek gemäß den Standards generiert.

  1. Vordefinierte RP-ID verwenden: Verwenden Sie die vordefinierte korrekte RP-ID als konstanten String.
  2. Challenge generieren: Generieren Sie einen kryptografisch sicheren Zufallspuffer mit hoher Entropie, speichern Sie ihn sicher in der Nutzersitzung und codieren Sie ihn als Base64URL.
  3. Zuordnung von auffindbaren Anmeldedaten: Geben Sie ein leeres Array [] für allowCredentials an. Dadurch werden auffindbare Anmeldedaten angefordert. Der Nutzer muss also nicht zuerst seinen Nutzernamen eingeben, sondern der Passkey-Anbieter zeigt die verfügbaren Konten an.
  4. Nutzerbestätigungsstufe: Legen Sie userVerification: "preferred" fest (oder "required", wenn dies ausdrücklich durch die Compliance-Richtlinien des Unternehmens vorgeschrieben ist).
    • Die angeforderte userVerification-Einschränkungsebene MUSS im Server-Sitzungsdatensatz am Optionsendpunkt beibehalten werden, anstatt vom Client über Abfragestrings zurückgegeben zu werden. So kann der Bestätigungsendpunkt strenge Abgleichseinschränkungen sicher erzwingen, ohne dass die Gefahr einer Clientmanipulation besteht.
// Options generation example (discoverable flow)
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
  rpId: "example.com",
  allowCredentials: [], // Request discoverable passkeys
  userVerification: "preferred",
};

// Persist expected UV level to user session
req.session.expectedUserVerification = "preferred";

Bestätigungsendpunkt

Prüfen Sie die vom Client zurückgegebene Assertion, um den Nutzer zu authentifizieren:

  1. Sitzungs-Challenge validieren: Erzwingt eine strikte Übereinstimmung zwischen der Clientantwort und der erwarteten Challenge, die in der Sitzung gespeichert ist.
  2. UV-Einstellungen erzwingen:
    • Authentifikatoren ohne Nutzerbestätigung zulassen (z.B. Displaysperren für Authentifikatoren deaktiviert), wenn die expectedUserVerification der Sitzung "preferred" angefordert hat, indem requireUserVerification: false an die serverseitige Überprüfungsbibliothek übergeben wird. Wenn "required" angefordert wird, muss die Eingabe biometrischer Daten oder der PIN erzwungen werden.
  3. Clean Serverfehler 404: Wenn die vom Client zurückgegebene Anmeldedaten-ID nicht in der Datenbank gefunden wird, geben Sie einen expliziten HTTP-Fehler 404 zurück, damit der Client die Signal API auslösen kann.

Clientseitige Logik

HTML-Formularannotation

Annotieren Sie die Eingabefelder für Nutzername und Passwort, um die bedingte Benutzeroberfläche nativ zu nutzen. Autocomplete-Tokens kombinieren die Parameter der WebAuthn-Spezifikation und Autofokus löst das Browser-Autofill-Pop-up sofort aus, wenn die Eingabe fokussiert wird.

<!-- Autocomplete tokens must contain webauthn space-separated -->
<form id="signin-form">
  <input
    type="text"
    name="username"
    autocomplete="username webauthn"
    autofocus
    data-testid="username-field"
  />
  <input type="password" name="password" autocomplete="current-password" />
  <button type="submit">Sign in</button>
</form>

Expliziter Button-Ablauf

Passkey-Authentifizierung auslösen, wenn ein Nutzer auf die Schaltfläche „Mit Passkey anmelden“ klickt. Brechen Sie alle laufenden Aufrufe zum automatischen Ausfüllen von Formularen (Conditional Get) ab, bevor Sie die Passkey-Aufforderung aufrufen.

Bedingter Vermittlungsablauf (Autofill-Formular)

Aktivieren Sie beim Seitenaufbau Vorschläge für das automatische Ausfüllen von Formularen, um die Passkey-Authentifizierung nativ anzubieten, wenn Nutzer auf Anmeldefelder klicken:

  1. Funktionserkennung: Rufen Sie beim Laden der Seite PublicKeyCredential.getClientCapabilities() auf und überspringen Sie die Anmeldung mit Passkey, wenn conditionalGet nicht verfügbar ist.
  2. Decodierungsoptionen: Decodieren Sie das abgerufene JSON-Objekt mit den Anmeldedaten mit PublicKeyCredential.parseRequestOptionsFromJSON().
  3. Bedingten GET-Aufruf ausführen: Rufen Sie navigator.credentials.get() mit mediation: "conditional" auf und übergeben Sie ein AbortController-Signal. Dadurch wird Autofill im Hintergrund registriert, ohne dass ein Passkey-Dialogfeld gerendert wird.
  4. Try/Catch-Ausnahmetrennung: Schließen Sie den navigator.credentials.get-Aufruf in einen Try/Catch-Block ein:
    • NotAllowedError: Der Nutzer hat die Passkey-Anmeldeaufforderung abgebrochen oder die Zeitüberschreitung ist abgelaufen.
    • AbortError: Die Authentifizierungsanfrage wurde programmatisch abgebrochen.
  5. Signal API aufrufen: Verpacken Sie den fetch()-Aufruf zur Serverbestätigung in einen try/catch-Block:
    • Zeigen Sie eine Fehlermeldung an, damit der Nutzer versteht, was schiefgelaufen ist.
    • Rufen Sie signalUnknownCredential() NUR auf, wenn der Server explizit mit dem HTTP-Status 404 (Anmeldedaten nicht gefunden) antwortet und der Nutzer nicht authentifiziert ist.
    • Der an signalUnknownCredential() übergebene Parameter credentialId MUSS genau der Base64URL-codierte String der Anmeldedaten-ID sein (z.B. encoded.id), NICHT das rohe ArrayBuffer-Objekt credential.rawId.
  6. Antwort codieren: Codieren Sie die Anmeldedaten AuthenticatorAssertionResponse mit .toJSON(), bevor Sie sie zur Überprüfung an den Server senden.
// optionsFetch and loginVerifyFetch are app-defined HTTP methods
import { optionsFetch, loginVerifyFetch } from "./api.js";

let autofillAbortController = new AbortController();

async function initializeConditionalAutofill() {
  // Feature detect Conditional Get autofill support
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (capabilities.conditionalGet === true) {
    const loginOptionsJSON = await optionsFetch();
    const publicKey =
      PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

    try {
      // Initiate Conditional UI form autofill suggestions
      const credential = await navigator.credentials.get({
        publicKey,
        signal: autofillAbortController.signal,
        mediation: "conditional",
      });

      // Segregated verification fetch
      const encoded = credential.toJSON();
      const response = await loginVerifyFetch(encoded);
      if (!response.ok && response.status === 404) {
        // Note: this code path runs pre-authentication, satisfying the unauth precondition
        if (PublicKeyCredential.signalUnknownCredential) {
          await PublicKeyCredential.signalUnknownCredential({
            rpId, // RP ID must match the one defined on the server
            credentialId: encoded.id,
          });
        }
      }
    } catch (err) {
      // Silently swallow expected client WebAuthn exceptions
      if (["NotAllowedError", "AbortError"].includes(err.name)) {
        return;
      }
      console.error("Unexpected conditional get error:", err);
    }
  }
}

async function triggerButtonAuthentication() {
  // Abort any pending Conditional Get call to prevent passkey prompt collisions
  autofillAbortController.abort();
  autofillAbortController = new AbortController(); // Reset controller for next triggers

  const loginOptionsJSON = await optionsFetch();
  const publicKey =
    PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

  let credential;
  try {
    // Passkey explicit prompt trigger
    credential = await navigator.credentials.get({
      publicKey,
      signal: autofillAbortController.signal,
    });
  } catch (err) {
    if (err.name === "NotAllowedError") {
      console.log("User cancelled passkey login.");
    } else if (err.name === "AbortError") {
      console.log("The authentication operation was aborted.");
    }
    // Re-arm Conditional autofill Suggestions after cancelled explicit button prompts
    initializeConditionalAutofill();
    return; // Safe exit
  }

  // Segregated verification try/catch (HTTP 404 trigger)
  const encoded = credential.toJSON();
  try {
    const response = await loginVerifyFetch(encoded);
    if (!response.ok && response.status === 404) {
      // Note: this code path runs pre-authentication, satisfying the unauth precondition
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encoded.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Verification request error:", serverErr);
  }
}

// Trigger Conditional Get on load
window.addEventListener("DOMContentLoaded", initializeConditionalAutofill);

Fallback-Strategien

Fallback für die Passkey-Funktionserkennung

Installieren Sie immer „webauthn-polyfills“ und importieren Sie es in den Kontext. Solange PublicKeyCredential unterstützt wird, wird auch PublicKeyCredential.getClientCapabilities unterstützt.

import 'webauthn-polyfills';

Signal API-Synchronisierungs-Fallback

Die WebAuthn Signal API (webauthn-signals) ist eine progressive Optimierung, mit der Passwortmanager mit dem Status der Serveranmeldedaten synchronisiert werden.

  • Fallback-Erfahrung: Über if (PublicKeyCredential.signalUnknownCredential) eingeschränkt. Wenn sie nicht unterstützt wird, wird die Synchronisierung der Hintergrundüberprüfung ohne Browserausnahmen umgangen.

Einfacher Fallback für die JSON-Serialisierung

Installieren Sie immer „webauthn-polyfills“ und importieren Sie es in den Kontext. Solange PublicKeyCredential unterstützt wird, werden auch PublicKeyCredential.parseRequestOptionsFromJSON und PublicKeyCredential.prototype.toJSON unterstützt.

import 'webauthn-polyfills';