Serverseitige Passkey-Registrierung

Übersicht

Hier ist eine Übersicht über die wichtigsten Schritte bei der Passkey-Registrierung:

Ablauf der Passkey-Registrierung

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

In den folgenden Abschnitten werden die einzelnen Schritte genauer beschrieben.

Optionen zum Erstellen von Anmeldedaten erstellen

Der erste Schritt auf dem Server besteht darin, ein PublicKeyCredentialCreationOptions-Objekt zu erstellen.

Verwenden Sie dazu Ihre serverseitige FIDO-Bibliothek. In der Regel wird eine Dienstfunktion angeboten, mit der diese Optionen für Sie erstellt werden können. SimpleWebAuthn bietet beispielsweise generateRegistrationOptions.

PublicKeyCredentialCreationOptions sollte alles enthalten, was für die Passkey-Erstellung erforderlich ist: Informationen zum Nutzer, zur vertrauenden Seite und eine Konfiguration für die Attribute der Anmeldedaten, die Sie erstellen. Nachdem Sie alle diese Elemente definiert haben, übergeben Sie sie nach Bedarf an die Funktion in Ihrer serverseitigen FIDO-Bibliothek, die für das Erstellen des PublicKeyCredentialCreationOptions-Objekts verantwortlich ist.

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

  • rpId: Verwenden Sie zum Ausfüllen der RP-ID auf dem Server serverseitige Funktionen oder Variablen, die den Hostnamen Ihrer Webanwendung angeben, 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, wenn der Nutzer bei der Registrierung einen Passkey erstellt). user.name ist normalerweise eine E-Mail-Adresse und für die vertrauende Seite eindeutig. user.displayName ist ein nutzerfreundlicher Name. Beachten Sie, dass nicht alle Plattformen displayName verwenden.
  • user.id: Ein zufälliger, eindeutiger String, der bei der Kontoerstellung generiert wird. Sie sollte dauerhaft sein, im Gegensatz zu einem Nutzernamen, der möglicherweise bearbeitet werden kann. Die Nutzer-ID identifiziert ein Konto, darf aber keine personenidentifizierbaren Informationen enthalten. Wahrscheinlich haben Sie bereits eine Nutzer-ID in Ihrem System. Erstellen Sie bei Bedarf eine speziell für Passkeys, um sie frei von personenidentifizierbaren Informationen zu halten.
  • excludeCredentials: Eine Liste der IDs vorhandener Anmeldedaten, um zu verhindern, dass ein Passkey vom Passkey-Anbieter dupliziert wird. Suchen Sie in Ihrer Datenbank nach vorhandenen Anmeldedaten für diesen Nutzer, um dieses Feld auszufüllen. Weitere Informationen finden Sie unter Erstellung eines neuen Passkeys verhindern, wenn bereits einer vorhanden ist.
  • challenge: Für die Registrierung von Anmeldedaten ist die Challenge nicht relevant, es sei denn, Sie verwenden die Attestierung, eine erweiterte Technik zur Überprüfung der Identität eines Passkey-Anbieters und der von ihm ausgegebenen Daten. Auch wenn Sie die Attestierung nicht verwenden, ist die Challenge ein Pflichtfeld. Eine Anleitung zum Erstellen einer sicheren Challenge für die Authentifizierung finden Sie unter Serverseitige Passkey-Authentifizierung.

Codierung und Decodierung

Vom Server gesendete PublicKeyCredentialCreationOptions
PublicKeyCredentialCreationOptions, die vom Server gesendet wird. Binäre Felder wie challenge und user.id werden serverseitig in base64URL codiert, damit Optionen als JSON über HTTPS bereitgestellt werden können.

Das standardmäßige WebAuthn-PublicKeyCredentialCreationOptions-Objekt enthält Felder, die ArrayBuffers sind und nicht direkt mit JSON.stringify() serialisiert werden können. Wenn Sie diese Optionen über HTTPS senden möchten, müssen binäre Felder als base64URL-Strings codiert werden:

  • Auf dem Server: Ihre serverseitige FIDO-Bibliothek (z. B. SimpleWebAuthn) erzeugt automatisch ein JSON-serialisierbares Optionsobjekt (z. B. PublicKeyCredentialCreationOptionsJSON) mit Base64URL-codierten Feldern.
  • Auf dem Client unterstützen moderne Browser PublicKeyCredential.parseCreationOptionsFromJSON(), um die JSON-Optionen direkt in PublicKeyCredentialCreationOptions zu konvertieren. Wenn Sie clientseitige Bibliotheken wie @simplewebauthn/browser verwenden, wird diese Konvertierung automatisch von Funktionen wie startRegistration({ optionsJSON: options }) durchgeführt.

Beispielcode: Optionen zum Erstellen von Anmeldedaten erstellen

In unseren Beispielen verwenden wir die SimpleWebAuthn-Bibliothek. Hier übergeben wir die Erstellung der Optionen für die Anmeldedaten für den öffentlichen Schlüssel an die Funktion generateRegistrationOptions.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
} from '@simplewebauthn/server';
import { isoBase64URL, isoUint8Array } 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: cred.id,
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

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

    // 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

Vom Server gesendete PublicKeyCredentialCreationOptions
navigator.credentials.create gibt ein PublicKeyCredential-Objekt zurück.

Wenn navigator.credentials.create auf dem Client erfolgreich aufgelöst wird, bedeutet das, 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. Sie 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 im Anhang: AuthenticatorAttestationResponse.

Senden Sie das PublicKeyCredential-Objekt an den Server. Bestätigen Sie den Erhalt.

Übergeben Sie diesen Bestätigungsschritt an Ihre serverseitige FIDO-Bibliothek. In der Regel wird dafür eine Dienstfunktion angeboten. SimpleWebAuthn bietet beispielsweise verifyRegistrationResponse. Weitere Informationen dazu, was im Hintergrund passiert, finden Sie im Anhang: Bestätigung der Registrierungsantwort.

Speichern Sie die Anmeldedaten nach erfolgreicher Bestätigung in Ihrer Datenbank, damit sich der Nutzer später mit dem mit diesen Anmeldedaten verknüpften Passkey authentifizieren kann.

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

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

Datenbankschema für Passkeys

  • Tabelle Users:
    • user_id: Die primäre Nutzer-ID. Eine zufällige, eindeutige, permanente ID für den Nutzer. Verwenden Sie diese als Primärschlüssel für die Tabelle Users.
    • username: Ein benutzerdefinierter Nutzername, der möglicherweise bearbeitet werden kann.
    • passkey_user_id: Die Passkey-spezifische Nutzer-ID ohne personenidentifizierbare Informationen, 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 oft zu de facto personenidentifizierbaren Informationen, da sie häufig verwendet werden.
  • Tabelle Anmeldedaten für öffentliche Schlüssel:
    • id: ID der Anmeldedaten. Verwenden Sie diese ID als Primärschlüssel für die Tabelle Anmeldedaten für öffentliche Schlüssel.
    • public_key: Öffentlicher Schlüssel der Anmeldedaten.
    • passkey_user_id: Verwenden Sie dies 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 entfernen möchten. Sie können prüfen, ob der Passkey gesichert ist, indem Sie das BE-Flag in authenticatorData untersuchen oder eine serverseitige FIDO-Bibliotheksfunktion verwenden, die normalerweise verfügbar ist, um Ihnen einfachen Zugriff auf diese Informationen zu ermöglichen. Das Speichern der Sicherungsberechtigung kann hilfreich sein, um potenzielle Nutzeranfragen zu beantworten.
    • name: Optional ein Anzeigename für die Anmeldedaten, damit Nutzer benutzerdefinierte Namen für Anmeldedaten vergeben können.
    • transports: Ein Array von Transporten. Das Speichern von Transporten ist für die Nutzerauthentifizierung nützlich. Wenn Übertragungsarten verfügbar sind, kann der Browser sich entsprechend verhalten und eine Benutzeroberfläche anzeigen, die der Übertragungsart entspricht, die der Passkey-Anbieter für die Kommunikation mit Clients verwendet. Das gilt insbesondere für Fälle der erneuten Authentifizierung, in denen allowCredentials nicht leer ist.

Andere Informationen können für die Nutzerfreundlichkeit hilfreich sein, z. B. der Passkey-Anbieter, der Zeitpunkt der Erstellung der Anmeldedaten und der Zeitpunkt der letzten Verwendung. Weitere Informationen finden Sie unter Passkeys user interface design.

Beispielcode: Anmeldedaten speichern

In unseren Beispielen verwenden wir 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 {
      aaguid,
      credential,
      credentialBackedUp
    } = registrationInfo;

    // Name the credential based on AAGUID
    const name =
      aaguid === undefined ||
      aaguid === '00000000-0000-0000-0000-000000000000' ?
        req.useragent?.platform : aaguids[aaguid]?.name;

    const base64PublicKey = isoBase64URL.fromBuffer(credential.publicKey);

    // Existing, signed-in user
    const { user } = res.locals;

    // Save the credential
    await Credentials.update({
      id: credential.id,
      passkey_user_id: user.passkey_user_id,
      publicKey: base64PublicKey,
      name,
      aaguid,
      transports: credential.transports || response.response.transports,
      backed_up: credentialBackedUp,
      registered_at: new Date().getTime()
    });

    // 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, die im Web die Daten sind, die der Browser sieht. Sie enthält den RP-Ursprung, die Challenge und androidPackageName, wenn der Client eine Android-App ist. Wenn Sie als RP clientDataJSON lesen, erhalten Sie Zugriff auf Informationen, die der Browser zum Zeitpunkt der create-Anfrage gesehen hat.
  • response.attestationObject enthält zwei Informationen:
    • attestationStatement, was nur relevant ist, wenn Sie die Attestierung verwenden.
    • authenticatorData sind Daten, wie sie vom Passkey-Anbieter gesehen werden. Als vertrauende Seite erhalten Sie durch das Lesen von authenticatorData Zugriff auf die Daten, die der Passkey-Anbieter sieht und die zum Zeitpunkt der create-Anfrage zurückgegeben werden.

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

  • Die Public-Key-Anmeldedaten selbst und eine eindeutige Anmeldedaten-ID dafür.
  • Die mit den Anmeldedaten verknüpfte RP-ID.
  • Flags, die den Nutzerstatus beim Erstellen des Passkeys beschreiben: ob ein Nutzer tatsächlich anwesend war und ob der Nutzer erfolgreich bestätigt wurde (siehe userVerification im Detail).
  • Die AAGUID ist eine Kennung für den Passkey-Anbieter, z. B. den Google Passwortmanager. Anhand der AAGUID können Sie den Passkey-Anbieter identifizieren und den Namen auf einer Seite zur Passkey-Verwaltung anzeigen. (siehe Passkey-Anbieter mit AAGUID ermitteln)

Obwohl authenticatorData in attestationObject verschachtelt ist, werden die darin enthaltenen Informationen für die Passkey-Implementierung benötigt, unabhängig davon, ob Sie die Attestierung verwenden oder nicht. authenticatorData ist codiert und enthält Felder, die in einem Binärformat codiert sind. Das Parsen und Decodieren wird in der Regel von Ihrer serverseitigen Bibliothek übernommen. Wenn Sie keine serverseitige Bibliothek verwenden, sollten Sie getAuthenticatorData() clientseitig nutzen, um sich das Parsen und Decodieren auf der Serverseite zu ersparen.

Anhang: Bestätigung der Registrierungsantwort

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

  • Achten Sie darauf, dass die RP-ID mit Ihrer Website übereinstimmt.
  • Prüfen Sie, ob der Ursprung der Anfrage ein erwarteter Ursprung für Ihre Website ist (URL der Hauptwebsite, Android-App).
  • Wenn Sie eine Nutzerbestätigung benötigen, muss das Flag für die Nutzerbestätigung authenticatorData.uv auf true gesetzt sein.
  • Das Flag für die Nutzeranwesenheit authenticatorData.up wird normalerweise auf true gesetzt. Wenn die Anmeldedaten jedoch bedingt erstellt werden, wird es auf false gesetzt.
  • Prüfen Sie, ob der Client die von Ihnen bereitgestellte Challenge bereitstellen konnte. Wenn Sie die Attestierung nicht verwenden, ist diese Prüfung nicht wichtig. Die Implementierung dieser Prüfung ist jedoch eine Best Practice: So ist Ihr Code bereit, falls Sie sich in Zukunft für die Verwendung der Attestierung entscheiden.
  • Die Anmeldedaten-ID darf noch für keinen Nutzer registriert sein.
  • Prüfen Sie, ob der vom Passkey-Anbieter zum Erstellen der Anmeldedaten verwendete Algorithmus einer der von Ihnen aufgeführten Algorithmen ist (in jedem alg-Feld von publicKeyCredentialCreationOptions.pubKeyCredParams, das normalerweise in Ihrer serverseitigen Bibliothek definiert und für Sie nicht sichtbar ist). So wird sichergestellt, dass sich Nutzer nur mit Algorithmen registrieren können, die Sie zugelassen haben.

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

Als Nächstes

Serverseitige Passkey-Authentifizierung