Serverseitige Passkey-Registrierung

Überblick

Hier finden Sie eine allgemeine Übersicht über die wichtigsten Schritte bei der Passkey-Registrierung:

Passkey-Registrierungsvorgang

  • Legen Sie Optionen zum Erstellen eines Passkeys fest. Senden Sie sie an den Client, damit Sie sie an den Aufruf zur Passkey-Erstellung übergeben können: den WebAuthn API-Aufruf navigator.credentials.create im Web und credentialManager.createCredential unter Android. Nachdem der Nutzer die Erstellung des Passkeys bestätigt hat, wird der Aufruf zur Passkey-Erstellung aufgelöst und gibt ein Ausweisdokument PublicKeyCredential zurück.
  • Überprüfen Sie die Anmeldedaten und speichern Sie sie auf dem Server.

In den folgenden Abschnitten werden die einzelnen Schritte ausführlich beschrieben.

Optionen für das Erstellen von Anmeldedaten erstellen

Als Erstes müssen Sie auf dem Server ein PublicKeyCredentialCreationOptions-Objekt erstellen.

Nutzen Sie dazu Ihre serverseitige FIDO-Bibliothek. In der Regel bietet er eine Dienstfunktion, mit der diese Optionen für Sie erstellt werden können. SimpleWebAuthn bietet z. B. generateRegistrationOptions.

PublicKeyCredentialCreationOptions sollte alles enthalten, was zum Erstellen von Passkeys erforderlich ist: Informationen über den Nutzer, den RP und eine Konfiguration für die Attribute der von Ihnen erstellten Anmeldedaten. Nachdem Sie alle Elemente definiert haben, übergeben Sie sie nach Bedarf an die Funktion in Ihrer serverseitigen FIDO-Bibliothek, die für das Erstellen des PublicKeyCredentialCreationOptions-Objekts zuständig ist.

Einige der PublicKeyCredentialCreationOptions-Felder können Konstanten sein. Andere sollten dynamisch auf dem Server definiert werden:

  • rpId: Verwenden Sie serverseitige Funktionen oder Variablen, die den Hostnamen Ihrer Webanwendung angeben, z. B. example.com, um die RP-ID auf dem Server auszufüllen.
  • user.name und user.displayName:Verwenden Sie zum Ausfüllen dieser Felder die Sitzungsinformationen des angemeldeten Nutzers (oder die Kontoinformationen des neuen Nutzers, wenn der Nutzer bei der Registrierung einen Passkey erstellt). user.name ist in der Regel eine E-Mail-Adresse und für die RP eindeutig. user.displayName ist ein nutzerfreundlicher Name. Nicht alle Plattformen verwenden displayName.
  • user.id: Ein zufälliger, eindeutiger String, der bei der Kontoerstellung generiert wird. Im Gegensatz zu Nutzernamen, die bearbeitet werden können, sollte sie dauerhaft sein. Die User-ID dient der Identifizierung eines Kontos, darf jedoch keine personenidentifizierbaren Informationen enthalten. Sie haben wahrscheinlich bereits eine User-ID in Ihrem System. Erstellen Sie bei Bedarf jedoch eine spezielle für Passkeys, damit diese keine personenidentifizierbaren Informationen enthalten.
  • excludeCredentials: Eine Liste mit vorhandenen Anmeldedaten-IDs, um das Wiederholen eines Passkeys vom Passkey-Anbieter zu verhindern. Suchen Sie in der Datenbank nach den Anmeldedaten dieses Nutzers, um dieses Feld auszufüllen. Weitere Informationen finden Sie im Hilfeartikel Das Erstellen eines neuen Passkeys verhindern, falls bereits vorhanden.
  • challenge: Die Identitätsbestätigung ist nur dann relevant, wenn Sie die Attestierung verwenden, um die Identität eines Passkey-Anbieters und die von ihm gesendeten Daten zu verifizieren. Aber auch wenn Sie keine Attestierung verwenden, ist die Aufgabe ein Pflichtfeld. In diesem Fall können Sie der Einfachheit halber für diese Herausforderung ein einzelnes 0 festlegen. Eine Anleitung zum Erstellen einer sicheren Authentifizierungsaufforderung finden Sie unter Serverseitige Passkey-Authentifizierung.

Codierung und Decodierung

PublicKeyCredentialCreationOptions, die vom Server gesendet werden.
PublicKeyCredentialCreationOptions vom Server gesendet. challenge, user.id und excludeCredentials.credentials müssen serverseitig in base64URL codiert werden, damit PublicKeyCredentialCreationOptions über HTTPS gesendet werden kann.

PublicKeyCredentialCreationOptions enthalten Felder vom Typ ArrayBuffer und werden daher von JSON.stringify() nicht unterstützt. Das bedeutet, dass zur Übertragung von PublicKeyCredentialCreationOptions über HTTPS derzeit einige Felder auf dem Server mit base64URL manuell codiert und dann auf dem Client decodiert werden müssen.

  • Auf dem Server erfolgt die Codierung und Decodierung in der Regel von der serverseitigen FIDO-Bibliothek.
  • Auf dem Client muss die Codierung und Decodierung derzeit manuell erfolgen. In Zukunft wird es einfacher werden: Es wird eine Methode zum Konvertieren von Optionen als JSON in PublicKeyCredentialCreationOptions zur Verfügung stehen. Sehen Sie sich den Status der Implementierung in Chrome an.

Beispielcode: Optionen zum Erstellen von Anmeldedaten erstellen

Wir verwenden in unseren Beispielen die SimpleWebAuthn-Bibliothek. Hier übergeben wir die Erstellung von Optionen für Anmeldedaten mit öffentlichem Schlüssel an die Funktion generateRegistrationOptions.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';

router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
  const { user } = res.locals;
  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // `excludeCredentials` prevents users from re-registering existing
    // credentials for a given passkey provider
    const excludeCredentials = [];
    const credentials = Credentials.findByUserId(user.id);
    if (credentials.length > 0) {
      for (const cred of credentials) {
        excludeCredentials.push({
          id: isoBase64URL.toBuffer(cred.id),
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

    // Generate registration options for WebAuthn create
    const options = generateRegistrationOptions({
      rpName: process.env.RP_NAME,
      rpID: process.env.HOSTNAME,
      userID: user.id,
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        requireResidentKey: true
      },
    });

    // Keep the challenge in the session
    req.session.challenge = options.challenge;

    return res.json(options);
  } catch (e) {
    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

Öffentlichen Schlüssel speichern

PublicKeyCredentialCreationOptions, die vom Server gesendet werden.
navigator.credentials.create gibt ein PublicKeyCredential-Objekt zurück.

Wenn navigator.credentials.create auf dem Client erfolgreich aufgelöst wird, bedeutet dies, dass ein Passkey erstellt wurde. Ein PublicKeyCredential-Objekt wird zurückgegeben.

Das PublicKeyCredential-Objekt enthält ein AuthenticatorAttestationResponse-Objekt, das die Antwort des Passkey-Anbieters auf die Anweisung des Clients zum Erstellen eines Passkeys darstellt. Es enthält Informationen über die neuen Anmeldedaten, die Sie als RP benötigen, um den Nutzer später zu authentifizieren. Weitere Informationen zu AuthenticatorAttestationResponse finden Sie im Anhang: AuthenticatorAttestationResponse.

Senden Sie das PublicKeyCredential-Objekt an den Server. Bestätigen Sie die E-Mail, nachdem Sie sie erhalten haben.

Übergeben Sie diesen Verifizierungsschritt an Ihre serverseitige FIDO-Bibliothek. Es bietet in der Regel eine Dienstfunktion für diesen Zweck. SimpleWebAuthn bietet z. B. verifyRegistrationResponse. Weitere Informationen finden Sie im Anhang: Bestätigung der Registrierungsantwort.

Nach erfolgreicher Verifizierung speichern Sie die Anmeldedaten in Ihrer Datenbank, damit sich der Nutzer später mit dem Passkey authentifizieren kann, der diesen Anmeldedaten zugeordnet ist.

Verwenden Sie eine dedizierte Tabelle für Anmeldedaten mit öffentlichem Schlüssel, die mit Passkeys verknüpft sind. Ein Nutzer kann nur ein einziges Passwort haben, aber mehrere Passkeys – z. B. einen Passkey, der über den Apple iCloud-Schlüsselbund synchronisiert wird, und einen, der über den Google Passwortmanager synchronisiert wird.

Hier ist ein Beispielschema, das Sie zum Speichern von Anmeldedaten verwenden können:

Datenbankschema für Passkeys

  • Tabelle Nutzer:
    • user_id: Die primäre Nutzer-ID. Eine zufällige, eindeutige, dauerhafte ID für den Nutzer. Verwenden Sie diesen Schlüssel als Primärschlüssel für die Tabelle Nutzer.
    • username: Ein benutzerdefinierter Nutzername, der möglicherweise bearbeitet werden kann.
    • passkey_user_id: Die Passkey-spezifische Nutzer-ID ohne personenidentifizierbare Informationen, dargestellt durch user.id in Ihren Registrierungsoptionen. Wenn der Nutzer später versucht, sich zu authentifizieren, stellt der Authenticator passkey_user_id in seiner Authentifizierungsantwort in userHandle zur Verfügung. Wir empfehlen, passkey_user_id nicht als Primärschlüssel festzulegen. Primärschlüssel werden in Systemen häufig zu personenidentifizierbaren Informationen, da sie häufig verwendet werden.
  • Tabelle mit Anmeldedaten für öffentlichen Schlüssel:
    • id: Anmeldedaten-ID Verwenden Sie diesen Schlüssel als Primärschlüssel für Ihre Tabelle mit den Anmeldedaten für den öffentlichen Schlüssel.
    • public_key: Öffentlicher Schlüssel des Berechtigungsnachweises.
    • passkey_user_id: Verwenden Sie diesen als Fremdschlüssel, um eine Verknüpfung mit der Tabelle Users herzustellen.
    • backed_up: Ein Passkey wird gesichert, wenn er vom Passkey-Anbieter synchronisiert wird. Das Speichern des Sicherungsstatus ist nützlich, wenn Sie in Zukunft Passwörter für Nutzer mit backed_up Passkeys löschen möchten. Ob der Passkey gesichert ist, kannst du mithilfe der Flags in authenticatorData oder mithilfe einer serverseitigen FIDO-Bibliotheksfunktion prüfen, die normalerweise für einen einfachen Zugriff auf diese Informationen zur Verfügung steht. Es kann hilfreich sein, die Voraussetzungen für die Sicherung zu speichern, um potenzielle Nutzeranfragen zu bearbeiten.
    • name: Optional ein Anzeigename für die Anmeldedaten, über den Nutzer benutzerdefinierte Namen für Anmeldedaten eingeben können
    • transports: Ein Array von Transsports. Das Speichern von Transporten ist nützlich, um sich zu authentifizieren. Wenn Transporte verfügbar sind, kann sich der Browser entsprechend verhalten und eine UI anzeigen, die dem Transport entspricht, den der Passkey-Anbieter für die Kommunikation mit Clients verwendet. Das gilt insbesondere für Anwendungsfälle zur erneuten Authentifizierung, bei denen allowCredentials nicht leer ist.

Andere Informationen können hilfreich sein, um die Nutzererfahrung zu verbessern, darunter Elemente wie der Passkey-Anbieter, der Zeitpunkt der Erstellung der Anmeldedaten und der Zeitpunkt der letzten Nutzung. Weitere Informationen finden Sie unter Design der Passkeys-Benutzeroberfläche.

Beispielcode: Anmeldedaten speichern

Wir verwenden in unseren Beispielen die SimpleWebAuthn-Bibliothek. Hier übergeben wir die Bestätigung der Registrierungsantwort an die Funktion verifyRegistrationResponse.

import { isoBase64URL } from '@simplewebauthn/server/helpers';


router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;
  const response = req.body;
  // This sample code is for registering a passkey for an existing,
  // signed-in user

  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Verify the credential
    const { verified, registrationInfo } = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      requireUserVerification: false,
    });

    if (!verified) {
      throw new Error('Verification failed.');
    }

    const { credentialPublicKey, credentialID } = registrationInfo;

    // Existing, signed-in user
    const { user } = res.locals;
    
    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      publicKey: base64PublicKey,
      // Optional: set the platform as a default name for the credential
      // (example: "Pixel 7")
      name: req.useragent.platform, 
      transports: response.response.transports,
      passkey_user_id: user.passkey_user_id,
      backed_up: registrationInfo.credentialBackedUp
    });

    // Kill the challenge for this session
    delete req.session.challenge;

    return res.json(user);
  } catch (e) {
    delete req.session.challenge;

    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

Anhang: AuthenticatorAttestationResponse

AuthenticatorAttestationResponse enthält zwei wichtige Objekte:

  • response.clientDataJSON ist eine JSON-Version von Clientdaten. Dies sind Daten, die im Web für den Browser sichtbar sind. Sie enthält den RP-Ursprung, die Aufgabe und androidPackageName, falls es sich bei dem Client um eine Android-App handelt. Da es sich bei dem Client um eine RP handelt, erhalten Sie durch Lesen von clientDataJSON Zugriff auf Informationen, die der Browser zum Zeitpunkt der create-Anfrage gesehen hat.
  • response.attestationObject enthält zwei Angaben:
    • attestationStatement, die nur dann relevant ist, wenn Sie die Attestierung verwenden.
    • authenticatorData sind die Daten, die der Passkey-Anbieter sieht. Als RP erhalten Sie durch Lesen von authenticatorData Zugriff auf die Daten, die der Passkey-Anbieter gesehen und zum Zeitpunkt der create-Anfrage zurückgegeben hat.

authenticatorData enthält wichtige Informationen zu den Anmeldedaten für den öffentlichen Schlüssel, die mit dem neu erstellten Passkey verknüpft sind:

  • Die Anmeldedaten des öffentlichen Schlüssels selbst und eine eindeutige Anmeldedaten-ID dafür.
  • Die mit dem Berechtigungsnachweis verknüpfte RP-ID.
  • Flags, die den Nutzerstatus beim Erstellen des Passkeys beschreiben: ob ein Nutzer tatsächlich anwesend war und ob er erfolgreich bestätigt wurde (siehe userVerification).
  • AAGUID: Gibt den Passkey-Anbieter an. Die Anzeige des Passkey-Anbieters kann für Ihre Nutzer hilfreich sein, insbesondere wenn sie einen Passkey bei mehreren Anbietern für Ihren Dienst registriert haben.

Obwohl authenticatorData in attestationObject verschachtelt ist, werden die darin enthaltenen Informationen für deine Passkey-Implementierung unabhängig davon benötigt, ob du die Attestierung verwendest oder nicht. authenticatorData ist codiert und enthält Felder, die in einem Binärformat codiert sind. Das Parsen und Decodieren übernimmt in der Regel Ihre serverseitige Bibliothek. Wenn Sie keine serverseitige Bibliothek verwenden, sollten Sie getAuthenticatorData() clientseitig nutzen, um sich serverseitig geparst und decodieren zu müssen.

Anhang: Bestätigung der Registrierungsantwort

Die Überprüfung der Registrierungsantwort umfasst die folgenden Überprüfungen:

  • Stellen Sie sicher, dass die RP-ID Ihrer Website entspricht.
  • Achten Sie darauf, dass der Ursprung der Anfrage ein erwarteter Ursprung für Ihre Website ist (Hauptwebsite-URL, Android-App).
  • Wenn eine Nutzerbestätigung erforderlich ist, muss das Flag authenticatorData.uv für die Nutzerbestätigung auf true gesetzt sein. Prüfen Sie, ob das Flag authenticatorData.up für die Nutzerpräsenz true ist, da die Nutzerpräsenz für Passkeys immer erforderlich ist.
  • Überprüfen Sie, ob der Kunde in der Lage war, die Aufgabe zu lösen. Wenn Sie keine Attestierung verwenden, ist diese Prüfung unwichtig. Das Implementieren dieser Prüfung ist jedoch eine Best Practice: Sie stellt sicher, dass Ihr Code bereit ist, wenn Sie sich später für die Verwendung der Attestierung entscheiden.
  • Die Anmeldedaten-ID darf noch nicht für einen Nutzer registriert sein.
  • Prüfe, ob der vom Passkey-Anbieter zum Erstellen der Anmeldedaten verwendete Algorithmus von dir in jedem alg-Feld von publicKeyCredentialCreationOptions.pubKeyCredParams angegeben wurde. Es ist normalerweise in deiner serverseitigen Bibliothek definiert und für dich nicht sichtbar. Dadurch wird sichergestellt, dass sich Nutzer nur mit Algorithmen registrieren können, die Sie zugelassen haben.

Weitere Informationen finden Sie im Quellcode für verifyRegistrationResponse von SimpleWebAuthn. Die vollständige Liste der Überprüfungen finden Sie in der Spezifikation.

Nächstes Video

Serverseitige Passkey-Authentifizierung