Passkey-Registrierung

In diesem Leitfaden wird beschrieben, wie Sie Nutzern die Möglichkeit geben, einen Passkey für ihr Konto zu registrieren. So erhalten sie eine hochsichere, Phishing-resistente Alternative zur passwortlosen Anmeldung.

Datenbankanforderungen

Damit Passkey-Registrierungen unterstützt werden, müssen in der Tabelle mit den Datenbank-Anmeldedaten die folgenden Felder gespeichert werden:

export interface StoredPasskeyCredential {
  id: string; // Base64URL-encoded credential ID (unique lookup key)
  passkeyUserId: string; // Associated application user ID
  credentialPublicKey: string; // Base64URL-encoded public key used to verify assertion signatures
  credentialType: "public-key";
  credentialDeviceType: "singleDevice" | "multiDevice"; // Helps distinguish device-bound vs cloud-synced passkeys
  credentialBackedUp: boolean; // Boolean backup state reported by the authenticator
  aaguid: string; // Authenticator Attestation GUID
  providerIcon?: string; // Provider icon derived from the AAGUID registry (dark or light theme URLs)
  name: string; // Provider name derived from AAGUID registry
  transports: string[]; // Array of transport names (e.g. 'internal', 'hybrid') necessary for exclusion options
  lastUsedAt?: number; // Optional epoch timestamp of last sign-in
  registeredAt: number; // Registration epoch timestamp
  counter: number; // Authenticator sign-in signature counter used to prevent replay attacks
}

Serverseitig

Optionen generieren

Erstellen Sie einen Endpunkt, der WebAuthn-Erstellungsparameter generiert. Verlassen Sie sich auf eine geprüfte Bibliothek gemäß den Kategoriestandards, anstatt die Kryptografie selbst zu entwickeln.

  1. Vordefinierte RP-ID verwenden: Verwenden Sie die vordefinierte korrekte RP-ID als konstanten String.
  2. Sichere Challenge erstellen: Generieren Sie auf dem Server einen kryptografisch sicheren Zufallspuffer mit hoher Entropie, speichern Sie ihn sicher in der Nutzersitzung und codieren Sie ihn als Base64URL für die Bereitstellung der Optionen.
  3. Doppelte Passkeys vermeiden: Ordnen Sie die vorhandenen vorregistrierten Anmeldedaten-IDs des Nutzers dem Array excludeCredentials zu. So wird verhindert, dass der Authenticator doppelte Anmeldedaten für dasselbe Passkey-Anbieterkonto registriert.
  4. Auffindbare Anmeldedaten erzwingen: Legen Sie requireResidentKey: true und residentKey: "required" in den authenticatorSelection-Optionen fest, um auffindbare Anmeldedaten anzufordern, die für die auffindbare Anmeldung erforderlich sind.
  5. Nutzerbestätigung konfigurieren: Geben Sie userVerification: "preferred" oder userVerification: "required" an. Viele Compliance-Anwendungsfälle (z.B. im Finanz- oder Gesundheitswesen) erfordern, dass 'required' die Nutzerbestätigung bei der Erstellung erzwingt.
  6. Anhangsumfang festlegen:
    • Promotion Flow: Wenn Sie die Erstellung eines Passkeys direkt nach der Anmeldung mit einem Standardpasswort oder nach der Registrierung vorschlagen, setzen Sie authenticatorAttachment: "platform", um den Plattform-Authenticator zu erzwingen und Aufforderungen für externe Sicherheitsschlüssel zu umgehen.
    • Verwaltungsablauf: Wenn der Aufruf über einen speziellen Bereich für Einstellungen oder Sicherheit erfolgt, in dem neben dem Plattform-Authenticator auch externe Sicherheitsschlüssel unterstützt werden, lassen Sie das Attribut authenticatorAttachment vollständig weg.
    • Tipp: Akzeptieren Sie ein promotion: boolean-Anfrage-Flag, um beide Abläufe mit einem einzigen Endpunkt bedingt zu verarbeiten.
// Options generation example
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // Cryptographically random challenge
  rp: { id: "example.com", name: "Secure Application" },
  user: {
    id: userBase64UrlId, // Unique base64url string identifying the account
    name: "user@example.com",
    displayName: "Jane Doe",
  },
  pubKeyCredParams: [
    {
      type: "public-key",
      alg: -7,
    },
    {
      type: "public-key",
      alg: -257,
    },
  ],
  excludeCredentials: userExistingCredentials.map((cred) => ({
    type: "public-key",
    id: cred.id,
    transports: cred.transports,
  })),
  authenticatorSelection: {
    residentKey: "required",
    requireResidentKey: true,
    userVerification: "preferred",
    ...(isPromotionFlow && { authenticatorAttachment: "platform" }),
  },
};

Bestätigung

  1. Challenge-Überprüfung: Die Challenge wird sicher mit der erwarteten sitzungsgebundenen Challenge abgeglichen.
  2. Anwesenheit des Nutzers prüfen:
    • Achten Sie darauf, dass das Flag „User Present“ (UP), das in den geparsten Authentifikatordaten zurückgegeben wird, true ist, um die physische Anwesenheit des Nutzers zum Zeitpunkt der Erstellung zu bestätigen.
  3. Lockerung der Überprüfung für „preferred“:
    • Wenn in den Erstellungsoptionen userVerification: "preferred" angegeben ist, MUSS der Aufruf für die serverseitige Überprüfung mit requireUserVerification: false konfiguriert werden. Andernfalls lösen Authentifikatoren, die sich ohne Nutzerbestätigung registrieren (z.B. bei deaktivierter Displaysperre), fälschlicherweise Fehler bei der Serverbestätigung aus.

Clientseitige Logik

  1. UI beim Seitenaufbau sperren:
    • Rufen Sie beim Laden der Seite PublicKeyCredential.getClientCapabilities() auf und deaktivieren Sie die Schaltfläche „Passkey erstellen“, wenn conditionalGet oder passkeyPlatformAuthenticator nicht verfügbar ist.
  2. Erstellung aufrufen und serialisieren: Decodieren Sie die Serveroptionen mit PublicKeyCredential.parseCreationOptionsFromJSON() und übergeben Sie die resultierende Konfiguration an navigator.credentials.create().
    • Rufe credential.toJSON() auf, um die AuthenticatorAttestationResponse in ein gültiges, JSON-serialisierbares Objekt zu codieren, bevor du den Bestätigungsendpunkt abrufst.
  3. WebAuthn-Ausnahmen verarbeiten:
    • InvalidStateError: Ein übereinstimmender Passkey ist bereits vorhanden (Übereinstimmung durch excludeCredentials).
    • NotAllowedError: Der Nutzer hat den Passkey-Authentifizierungsdialog abgebrochen oder es ist eine Zeitüberschreitung aufgetreten.
    • AbortError: Der Vorgang wurde abgebrochen.
    • SecurityError: Fehler aufgrund von sicheren Ursprüngen (HTTPS) oder nicht übereinstimmenden RP-IDs (Konfigurationsprobleme).
  4. Try/Catch-Trennung für die Signal API:
    • Fügen Sie den Aufruf der Serverbestätigung fetch() in einen Try/Catch-Block ein. Rufen Sie signalUnknownCredential() auf, wenn der Abruf der Serverbestätigung fehlschlägt (beliebiger Status response.ok === false oder Netzwerkfehler).
// optionsFetch and registerVerifyFetch are app-defined HTTP methods
import { optionsFetch, registerVerifyFetch } from "./api.js";

async function registerPasskey(isPromotion = false) {
  // Verify passkey capability and conditional UI are available
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (
    !capabilities.passkeyPlatformAuthenticator ||
    !capabilities.conditionalGet
  ) {
    // Hide "Create passkey" buttons and fall back to password flows instead
    showStandardPasswordFallbackUI();
    return;
  }

  const creationOptionsJSON = await optionsFetch({ promotion: isPromotion });
  const publicKey =
    PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);

  let credential;
  try {
    // passkey prompt execution
    credential = await navigator.credentials.create({ publicKey });
  } catch (err) {
    if (err.name === "InvalidStateError") {
      console.log("A passkey already exists for this account.");
      alert("A passkey already exists for this account.");
    } else if (err.name === "SecurityError") {
      console.error("Configuration RP ID or Secure Context error.");
      alert("Configuration RP ID or Secure Context error.");
    } else if (err.name === "NotAllowedError") {
      console.log("User cancelled the passkey dialog.");
    } else if (err.name === "AbortError") {
      console.log("The creation operation has been aborted.");
    }
    return; // Safe API exit, do not signal unknown for standard WebAuthn cancels
  }

  // Server Verification phase (Segregated Try/Catch)
  let encodedResponse = credential.toJSON();
  try {
    const response = await registerVerifyFetch(encodedResponse);
    if (!response.ok) {
      // Server verification failed to verify/authenticate the credential (orphaned)
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encodedResponse.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Server verification network failure:", serverErr);
    await publickeycredential.signalunknowncredential({
      rpId, // RP ID must match the one defined on the server
      credentialid: encodedresponse.id, // base64url-encoded credential id
    });
  }
}

Fallback-Strategien

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-Verfahren: Wenn PublicKeyCredential.signalUnknownCredential vom Browser nicht unterstützt wird, MUSS der Aufruf sicher über die Feature-Erkennung (if (PublicKeyCredential.signalUnknownCredential)) umgangen werden. Bei der serverseitigen Überprüfung wird der Fehler dann einfach protokolliert, ohne dass Manager-Updates ausgelöst werden.

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.parseCreationOptionsFromJSON und PublicKeyCredential.prototype.toJSON unterstützt.

import 'webauthn-polyfills';