Serverseitige Passkey-Registrierung

Übersicht

Hier ist eine grobe Übersicht über die wichtigsten Schritte bei der Registrierung eines Passkeys:

Passkey-Registrierungsvorgang

  • Legen Sie Optionen zum Erstellen eines Passkeys fest. Senden Sie sie an den Client, damit Sie sie an Ihren Aufruf zur Passkey-Erstellung weitergeben 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 Anruf zum Erstellen des Passkeys aufgelöst und es werden Anmeldedaten PublicKeyCredential zurückgegeben.
  • Überprüfen Sie die Anmeldedaten und speichern Sie sie auf dem Server.

In den folgenden Abschnitten werden die einzelnen Schritte ausführlich erläutert.

<ph type="x-smartling-placeholder">

Optionen zum Erstellen von Anmeldedaten erstellen

Der erste Schritt, den Sie auf dem Server ausführen müssen, besteht darin, ein PublicKeyCredentialCreationOptions-Objekt zu erstellen.

Nutzen Sie dazu Ihre serverseitige FIDO-Bibliothek. Normalerweise wird eine Dienstfunktion zur Verfügung gestellt, die diese Optionen für Sie erstellen kann. SimpleWebAuthn bietet zum Beispiel generateRegistrationOptions.

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

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

  • rpId: Um die RP-ID auf dem Server auszufüllen, verwenden Sie serverseitige Funktionen oder Variablen, mit denen Sie den Hostnamen Ihrer Webanwendung ermitteln, z. B. example.com.
  • user.name und user.displayName:Verwenden Sie zum Ausfüllen dieser Felder die Sitzungsinformationen des angemeldeten Nutzers oder die Informationen des neuen Nutzerkontos, falls der Nutzer bei der Registrierung einen Passkey erstellt. user.name ist in der Regel eine E-Mail-Adresse und eindeutig für die RP. 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. Mit der User-ID wird ein Konto identifiziert, sie darf aber keine personenidentifizierbaren Informationen enthalten. Sie haben wahrscheinlich bereits eine Nutzer-ID in Ihrem System, aber bei Bedarf erstellen Sie eine speziell für Passkeys, damit sie keine personenidentifizierbaren Informationen enthält.
  • excludeCredentials: Eine Liste der vorhandenen Anmeldedaten IDs, um das Duplizieren eines Passkeys vom Passkey-Anbieter zu verhindern. Suchen Sie in Ihrer Datenbank nach den für diesen Nutzer vorhandenen Anmeldedaten, um dieses Feld auszufüllen. Weitere Informationen finden Sie im Hilfeartikel Erstellen eines neuen Passkeys verhindern, wenn bereits ein Passkey vorhanden ist.
  • challenge: Für die Registrierung von Anmeldedaten ist die Identitätsbestätigung nur relevant, wenn Sie eine Attestierung verwenden. Das ist eine fortgeschrittenere Methode, um die Identität eines Passkey-Anbieters und die ausgegebenen Daten zu überprüfen. Aber auch wenn Sie die Attestierung nicht verwenden, ist die Identitätsbestätigung ein Pflichtfeld. In diesem Fall können Sie diese Aufgabe der Einfachheit halber auf ein einzelnes 0 festlegen. Eine Anleitung dazu, wie Sie eine sichere Identitätsbestätigung für die Authentifizierung erstellen, finden Sie im Hilfeartikel Serverseitige Passkey-Authentifizierung.

Codierung und Decodierung

<ph type="x-smartling-placeholder">
</ph> PublicKeyCredentialCreationOptions vom Server gesendet
PublicKeyCredentialCreationOptions vom Server gesendet. challenge, user.id und excludeCredentials.credentials müssen serverseitig in base64URL codiert werden, damit PublicKeyCredentialCreationOptions über HTTPS übertragen werden kann.

PublicKeyCredentialCreationOptions enthält ArrayBuffer-Felder, die von JSON.stringify() nicht unterstützt werden. Das bedeutet, dass einige Felder derzeit auf dem Server mit base64URL manuell codiert und dann auf dem Client decodiert werden müssen, um PublicKeyCredentialCreationOptions über HTTPS zu senden.

  • Auf dem Server wird die Codierung und Decodierung in der Regel von Ihrer serverseitigen FIDO-Bibliothek übernommen.
  • Auf dem Client muss die Codierung und Decodierung derzeit manuell erfolgen. In Zukunft wird dies einfacher: Es wird eine Methode zur Konvertierung von Optionen als JSON in PublicKeyCredentialCreationOptions verfügbar sein. Überprüfen Sie den Status der Implementierung in Chrome.

Beispielcode: Optionen zum Erstellen von Anmeldedaten erstellen

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

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

<ph type="x-smartling-placeholder">
</ph> PublicKeyCredentialCreationOptions vom Server gesendet
navigator.credentials.create gibt ein PublicKeyCredential-Objekt zurück.

Wenn navigator.credentials.create auf dem Client 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 zu den neuen Anmeldedaten, die Sie als RP benötigen, um den Nutzer später zu authentifizieren. Weitere Informationen zu AuthenticatorAttestationResponse finden Sie in Anhang: AuthenticatorAttestationResponse.

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

Übergeben Sie diesen Bestätigungsschritt an Ihre serverseitige FIDO-Bibliothek. Für diesen Zweck wird in der Regel eine Dienstfunktion bereitgestellt. SimpleWebAuthn bietet zum Beispiel verifyRegistrationResponse. Weitere Informationen dazu finden Sie im Anhang: Überprüfung der Registrierungsantwort.

Nach erfolgreicher Bestätigung sollten Sie Anmeldedaten in Ihrer Datenbank speichern, damit sich der Nutzer später mit dem Passkey authentifizieren kann, der mit diesen Anmeldedaten verknüpft ist.

Verwenden Sie eine dedizierte Tabelle für öffentliche Schlüssel-Anmeldedaten, die mit Passkeys verknüpft sind. Ein Nutzer kann nur ein Passwort, aber mehrere Passkeys haben, z. B. einen Passkey, der über den Apple iCloud-Schlüsselbund und einen ü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: <ph type="x-smartling-placeholder">
      </ph>
    • user_id: Die primäre Nutzer-ID. Eine zufällige, eindeutige, permanente ID für den Nutzer. Verwenden Sie diesen Schlüssel als Primärschlüssel für Ihre Tabelle Nutzer.
    • username Ein benutzerdefinierter Nutzername, der möglicherweise bearbeitet werden kann.
    • passkey_user_id: Die Nutzer-ID ohne Passkey, die in Ihren Registrierungsoptionen durch user.id dargestellt wird. Wenn der Nutzer später versucht, sich zu authentifizieren, stellt der Authenticator diese 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 in der Regel zu personenidentifizierbaren Informationen, da sie intensiv genutzt werden.
  • Tabelle mit Anmeldedaten für öffentlichen Schlüssel: <ph type="x-smartling-placeholder">
      </ph>
    • id: Anmeldedaten-ID. Verwenden Sie diesen Schlüssel als Primärschlüssel für Ihre Tabelle mit den Anmeldedaten für öffentlichen Schlüssel.
    • public_key: Der öffentliche Schlüssel des Berechtigungsnachweises.
    • passkey_user_id: Verwenden Sie dies als Fremdschlüssel, um eine Verknüpfung mit der Tabelle Nutzer herzustellen.
    • backed_up: Ein Passkey wird gesichert, wenn er vom Passkey-Anbieter synchronisiert wird. Das Speichern des Sicherungsstatus ist nützlich, wenn Sie Passwörter für Nutzer mit backed_up Passkeys in Zukunft löschen möchten. Sie können prüfen, ob der Passkey gesichert ist, indem Sie die Markierungen in authenticatorData prüfen oder eine serverseitige FIDO-Bibliotheksfunktion verwenden, die normalerweise verfügbar ist, um einfachen Zugriff auf diese Informationen zu ermöglichen. Die Speicherung der Informationen zur Sicherung kann hilfreich sein, um potenzielle Nutzeranfragen zu bearbeiten.
    • name: Optional ein Anzeigename für die Anmeldedaten, damit Nutzer den Anmeldedaten benutzerdefinierte Namen geben können
    • transports: ein Array von transports. Das Speichern von Transporten ist für die Authentifizierung des Nutzers hilfreich. Wenn Übertragungen 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 – insbesondere für Anwendungsfälle mit einer erneuten Authentifizierung, in denen allowCredentials nicht leer ist.

Andere Informationen können für die Nutzer hilfreich sein, z. B. der Passkey-Anbieter, der Erstellungszeitpunkt der Anmeldedaten und die letzte Nutzung. Weitere Informationen finden Sie unter 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 der Clientdaten. Dabei handelt es sich im Web um Daten, wie sie vom Browser erfasst werden. Sie enthält den RP-Ursprung, die Aufgabe und androidPackageName, wenn der Client eine Android-App ist. Als RP bietet Ihnen das Lesen von clientDataJSON Zugriff auf Informationen, die der Browser zum Zeitpunkt der create-Anfrage gesehen hat.
  • response.attestationObjectenthält zwei Informationen: <ph type="x-smartling-placeholder">
      </ph>
    • attestationStatement. Dies ist nur relevant, wenn Sie die Attestierung verwenden.
    • authenticatorData sind Daten, wie sie dem Passkey-Anbieter angezeigt werden. Als RP bietet Ihnen das Lesen von authenticatorData Zugriff auf die Daten, die der Passkey-Anbieter sieht und zum Zeitpunkt der create-Anfrage zurückgegeben wurde.

authenticatorDataenthält wichtige Informationen zu den Public-Key-Anmeldedaten, die dem neu erstellten Passkey zugeordnet sind:

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

Obwohl authenticatorData in attestationObject verschachtelt ist, werden die darin enthaltenen Informationen für die Implementierung deines Passkeys benötigt – unabhängig davon, ob du die Bestätigung verwendest oder nicht. authenticatorData ist codiert und enthält Felder, die in einem Binärformat codiert sind. Ihre serverseitige Bibliothek übernimmt normalerweise das Parsen und die Decodierung. Wenn du keine serverseitige Bibliothek verwendest, kannst du getAuthenticatorData() clientseitig nutzen, um Arbeit serverseitig zu parsen und zu decodieren.

Anhang: Überprüfung der Registrierungsantwort

Intern umfasst die Überprüfung der Registrierungsantwort folgende Prüfungen:

  • Achten Sie darauf, dass die RP-ID mit Ihrer Website übereinstimmt.
  • 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 authenticatorData.uv true lauten. Das Flag authenticatorData.up für die Nutzerpräsenz muss auf true gesetzt sein, da die Anwesenheit des Nutzers für Passkeys immer erforderlich ist.
  • Prüfen Sie, ob der Kunde die von Ihnen verlangte Aktion durchführen konnte. Wenn Sie keine Attestierung verwenden, ist diese Prüfung unwichtig. Die Implementierung dieser Prüfung hat sich jedoch bewährt. Sie stellt sicher, dass Ihr Code bereit ist, falls Sie sich später für die Verwendung der Attestierung entscheiden.
  • Vergewissern Sie sich, dass die Berechtigungsnachweis-ID noch für keinen Nutzer registriert ist.
  • Achten Sie darauf, dass der Algorithmus, der vom Passkey-Anbieter zum Erstellen der Anmeldedaten verwendet wird, einem von Ihnen im alg-Feld von publicKeyCredentialCreationOptions.pubKeyCredParams aufgeführten Algorithmus entspricht. Dieser ist in der Regel in Ihrer serverseitigen Bibliothek definiert und für Sie 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 oder in der vollständigen Liste der Überprüfungen in der Spezifikation.

Nächstes Video

Serverseitige Passkey-Authentifizierung