Serverseitige Passkey-Authentifizierung

Übersicht

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

Passkey-Authentifizierungsvorgang

  • Lege die Identitätsbestätigung und andere Optionen fest, die für die Authentifizierung mit einem Passkey erforderlich sind. Senden Sie sie an den Client, damit Sie sie an Ihren Passkey-Authentifizierungsanruf übergeben können (navigator.credentials.get im Web). Nachdem der Nutzer die Passkey-Authentifizierung bestätigt hat, wird der Passkey-Authentifizierungsanruf aufgelöst und ein Ausweisdokument (PublicKeyCredential) wird zurückgegeben. Die Anmeldedaten enthalten eine Authentifizierungs-Assertion.
  • Prüfen Sie die Assertion für die Authentifizierung.
  • Wenn die Authentifizierungs-Assertion gültig ist, authentifizieren Sie den Nutzer.

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

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

Wettkampf erstellen

In der Praxis ist eine Aufgabe ein Array von zufälligen Byte, die als ArrayBuffer-Objekt dargestellt werden.

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

Damit die Challenge ihren Zweck erfüllt, musst du:

  1. Sorge dafür, dass dieselbe Aufgabe nie mehr als einmal verwendet wird. Bei jedem Anmeldeversuch eine neue Herausforderung generieren. Verwerfen Sie die Identitätsbestätigung nach jedem Anmeldeversuch, unabhängig davon, ob er erfolgreich war oder fehlgeschlagen ist. Den Wettkampf nach einer bestimmten Dauer auch verwerfen. Nehmen Sie die gleiche Aufgabe nicht mehrmals in einer Antwort an.
  2. Achten Sie darauf, dass die Identitätsbestätigung kryptografisch sicher ist. Eine Herausforderung sollte praktisch unmöglich zu erraten sein. Um eine serverseitige kryptografisch sichere Identitätsbestätigung zu erstellen, sollten Sie sich auf eine serverseitige FIDO-Bibliothek verlassen, der Sie vertrauen. Wenn Sie stattdessen eigene Herausforderungen erstellen, können Sie die integrierten kryptografischen Funktionen Ihres Technologie-Stacks verwenden oder nach Bibliotheken suchen, die für kryptografische Anwendungsfälle entwickelt wurden. Beispiele hierfür sind iso-crypto in Node.js oder secrets in Python. Gemäß der Spezifikation muss die Challenge mindestens 16 Byte lang sein, um als sicher zu gelten.

Nachdem Sie einen Wettkampf erstellt haben, speichern Sie ihn in der Nutzersitzung, um ihn später zu bestätigen.

Optionen für Anmeldedatenanfragen erstellen

Erstellen Sie Optionen für Anmeldedatenanfragen als publicKeyCredentialRequestOptions-Objekt.

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 generateAuthenticationOptions.

publicKeyCredentialRequestOptions sollte alle Informationen enthalten, die für die Passkey-Authentifizierung erforderlich sind. Übergeben Sie diese Informationen an die Funktion in Ihrer serverseitigen FIDO-Bibliothek, die für die Erstellung des publicKeyCredentialRequestOptions-Objekts zuständig ist.

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

  • rpId: Die RP-ID, mit der die Anmeldedaten verknüpft werden sollen, z. B. example.com. Die Authentifizierung ist nur erfolgreich, wenn die hier angegebene RP-ID mit der mit dem Berechtigungsnachweis verknüpften RP-ID übereinstimmt. Verwende zum Ausfüllen der RP-ID denselben Wert wie die RP-ID, die du bei der Registrierung der Anmeldedaten in publicKeyCredentialCreationOptions festgelegt hast.
  • challenge: Ein Datenelement, das der Passkey-Anbieter signiert, um nachzuweisen, dass der Nutzer den Passkey zum Zeitpunkt der Authentifizierungsanfrage besitzt. Weitere Informationen findest du unter Herausforderung erstellen.
  • allowCredentials: ein Array zulässiger Anmeldedaten für diese Authentifizierung. Übergeben Sie ein leeres Array, damit der Nutzer einen verfügbaren Passkey aus einer vom Browser angezeigten Liste auswählen kann. Weitere Informationen finden Sie unter Herausforderung vom RP-Server abrufen und Detaillierte Informationen zu den sichtbaren Anmeldedaten.
  • userVerification: Gibt an, ob die Nutzerbestätigung über die Displaysperre des Geräts „erforderlich“ oder „bevorzugt“ ist oder „entmutigt“. Weitere Informationen finden Sie unter Herausforderung vom RP-Server abrufen.
  • timeout: Gibt an, wie lange (in Millisekunden) der Nutzer für die Authentifizierung benötigt. Es sollte relativ großzügig sein und kürzer als die Lebensdauer von challenge sein. Der empfohlene Standardwert ist 5 Minuten, aber Sie können ihn erhöhen – auf bis zu 10 Minuten, was noch innerhalb des empfohlenen Bereichs liegt. Lange Zeitlimits sind sinnvoll, wenn Sie davon ausgehen, dass Nutzer den hybriden Workflow verwenden, der normalerweise etwas länger dauert. Wenn beim Vorgang eine Zeitüberschreitung auftritt, wird ein NotAllowedError ausgelöst.

Nachdem Sie publicKeyCredentialRequestOptions erstellt haben, senden Sie es an den Client.

<ph type="x-smartling-placeholder">
</ph> publicKeyCredentialCreationOptions vom Server gesendet
Vom Server gesendete Optionen. Die challenge-Decodierung erfolgt clientseitig.

Beispielcode: Optionen zum Erstellen von Anmeldedatenanfragen

Wir verwenden in unseren Beispielen die SimpleWebAuthn-Bibliothek. Hier übergeben wir die Erstellung von Optionen für Anmeldedatenanfragen an die zugehörige generateAuthenticationOptions-Funktion.

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

router.post('/signinRequest', csrfCheck, async (req, res) => {

  // Ensure you nest 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 {
    // Use the generateAuthenticationOptions function from SimpleWebAuthn
    const options = await generateAuthenticationOptions({
      rpID: process.env.HOSTNAME,
      allowCredentials: [],
    });
    // Save the challenge in the user session
    req.session.challenge = options.challenge;

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

Nutzer bestätigen und anmelden

Wenn navigator.credentials.get auf dem Client erfolgreich aufgelöst wird, wird ein PublicKeyCredential-Objekt zurückgegeben.

<ph type="x-smartling-placeholder">
</ph> Vom Server gesendetes PublicKeyCredential-Objekt
navigator.credentials.get gibt eine PublicKeyCredential zurück.

response ist ein AuthenticatorAssertionResponse. Er stellt die Antwort des Passkey-Anbieters auf die Anweisung des Clients dar, das zu erstellen, was für die Authentifizierung mit einem Passkey auf dem RP erforderlich ist. Es enthält:

  • response.authenticatorDataundresponse.clientDataJSON, z. B. bei der Passkey-Registrierung.
  • response.signature, die eine Signatur über diese Werte enthält.

Senden Sie das Objekt PublicKeyCredential an den Server.

Führen Sie auf dem Server folgende Schritte aus:

<ph type="x-smartling-placeholder">
</ph> Datenbankschema
Vorgeschlagenes Datenbankschema. Weitere Informationen zu diesem Design finden Sie unter Serverseitige Passkey-Registrierung.
  • Sammeln Sie die Informationen, die Sie benötigen, um die Assertion zu prüfen und den Nutzer zu authentifizieren: <ph type="x-smartling-placeholder">
      </ph>
    • Rufen Sie die erwartete Identitätsbestätigung ab, die Sie in der Sitzung gespeichert haben, als Sie die Authentifizierungsoptionen generiert haben.
    • Ruft den erwarteten origin-Wert und die RP-ID ab.
    • Finden Sie in Ihrer Datenbank heraus, wer der Nutzer ist. Bei auffindbaren Anmeldedaten wissen Sie nicht, wer der Nutzer ist, der die Authentifizierungsanfrage stellt. Sie haben zwei Möglichkeiten, dies herauszufinden: <ph type="x-smartling-placeholder">
        </ph>
      • Option 1: response.userHandle im PublicKeyCredential-Objekt verwenden Suchen Sie in der Tabelle Nutzer nach der passkey_user_id, die userHandle entspricht.
      • Option 2: Sie verwenden die Anmeldedaten id, die im Objekt PublicKeyCredential vorhanden sind. Suchen Sie in der Tabelle Anmeldedaten mit öffentlichem Schlüssel nach den Anmeldedaten id, die mit den Anmeldedaten id im Objekt PublicKeyCredential übereinstimmen. Suchen Sie dann mithilfe des Fremdschlüssels passkey_user_id in der Tabelle Users nach dem entsprechenden Nutzer.
    • Suchen Sie in Ihrer Datenbank nach den Anmeldedaten für den öffentlichen Schlüssel, die mit der Authentifizierungs-Assertion übereinstimmen, die Sie erhalten haben. Suchen Sie dazu in der Tabelle Anmeldedaten für öffentlichen Schlüssel nach den Anmeldedaten id, die mit den Anmeldedaten id im Objekt PublicKeyCredential übereinstimmen.
  • Bestätigen Sie die Authentifizierungs-Assertion. Übergeben Sie diesen Bestätigungsschritt an Ihre serverseitige FIDO-Bibliothek, die in der Regel eine Dienstprogrammfunktion für diesen Zweck bereitstellt. SimpleWebAuthn bietet zum Beispiel verifyAuthenticationResponse. Weitere Informationen dazu finden Sie im Anhang: Überprüfung der Authentifizierungsantwort.

  • Lösche die Identitätsbestätigung, unabhängig davon, ob die Bestätigung erfolgreich ist oder nicht, um Replay-Angriffe zu verhindern.

  • Melden Sie sich an. Wenn die Bestätigung erfolgreich war, aktualisieren Sie die Sitzungsinformationen, um den Nutzer als angemeldet zu kennzeichnen. Möglicherweise möchten Sie auch ein user-Objekt an den Client zurückgeben, damit das Front-End Informationen verwenden kann, die dem neu angemeldeten Nutzer zugeordnet sind.

Beispielcode: Nutzer bestätigen und anmelden

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

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

router.post('/signinResponse', csrfCheck, async (req, res) => {
  const response = req.body;
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;

  // 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 {
    // Find the credential stored to the database by the credential ID
    const cred = Credentials.findById(response.id);
    if (!cred) {
      throw new Error('Credential not found.');
    }
    // Find the user - Here alternatively we could look up the user directly
    // in the Users table via userHandle
    const user = Users.findByPasskeyUserId(cred.passkey_user_id);
    if (!user) {
      throw new Error('User not found.');
    }
    // Base64URL decode some values
    const authenticator = {
      credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
      credentialID: isoBase64URL.toBuffer(cred.id),
      transports: cred.transports,
    };

    // Verify the credential
    const { verified, authenticationInfo } = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      authenticator,
      requireUserVerification: false,
    });

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

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

    req.session.username = user.username;
    req.session['signed-in'] = 'yes';

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

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

Anhang: Überprüfung der Authentifizierungsantwort

Die Überprüfung der Authentifizierungsantwort besteht aus den folgenden Prüfungen:

  • Achten Sie darauf, dass die RP-ID mit Ihrer Website übereinstimmt.
  • Prüfen Sie, ob der Ursprung der Anfrage mit der Anmeldequelle Ihrer Website übereinstimmt. Lesen Sie für Android-Apps den Artikel Ursprung überprüfen.
  • Prüfen Sie, ob das Gerät die von Ihnen vorgegebene Aufgabe erfüllen konnte.
  • Prüfen Sie, ob der Nutzer bei der Authentifizierung die Anforderungen erfüllt hat, die Sie als RP festlegen. Wenn eine Nutzerbestätigung erforderlich ist, muss uv (vom Nutzer verifiziert) in authenticatorData den Wert true haben. Das Flag up (Nutzer anwesend) in authenticatorData muss auf true gesetzt sein, da die Anwesenheit des Nutzers für Passkeys immer erforderlich ist.
  • Bestätigen Sie die Signatur. Zum Bestätigen der Signatur benötigen Sie Folgendes: <ph type="x-smartling-placeholder">
      </ph>
    • Die Unterschrift, d. h. die signierte Aufgabe: response.signature
    • Der öffentliche Schlüssel, mit dem die Signatur verifiziert werden soll.
    • Die ursprünglichen signierten Daten. Dies sind die Daten, deren Signatur bestätigt werden soll.
    • Der kryptografische Algorithmus, mit dem die Signatur erstellt wurde.

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