Serverseitige Passkey-Authentifizierung

Überblick

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

Passkey-Authentifizierungsablauf

  • Legen Sie die Identitätsbestätigung und weitere Optionen fest, die für die Authentifizierung mit einem Passkey erforderlich sind. Sende sie an den Client, damit du sie an deinen Passkey-Authentifizierungsaufruf übergeben kannst (navigator.credentials.get im Web). Nachdem der Nutzer die Passkey-Authentifizierung bestätigt hat, wird der Aufruf zur Passkey-Authentifizierung aufgelöst und gibt ein Ausweisdokument zurück (PublicKeyCredential). Die Anmeldedaten enthalten eine Authentifizierungsbestätigung.
  • Prüfen Sie die AuthentifizierungsAssertion.
  • Wenn die Authentifizierungsbestätigung gültig ist, wird der Nutzer authentifiziert.

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

Wettkampf erstellen

In der Praxis besteht eine Identitätsbestätigung aus einem Array von zufälligen Byte, das als ArrayBuffer-Objekt dargestellt wird.

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

Um sicherzustellen, dass der Wettkampf seinen Zweck erfüllt, müssen Sie:

  1. Achte darauf, dass die gleiche Aufgabe nie mehr als einmal verwendet wird. Bei jedem Anmeldeversuch eine neue Identitätsbestätigung erstellen. Verwerfen Sie die Identitätsbestätigung nach jedem Anmeldeversuch, unabhängig davon, ob er erfolgreich war oder fehlgeschlagen ist. Auch nach einer bestimmten Zeit kannst du die Herausforderung verwerfen. Nehmen Sie die gleiche Herausforderung in einer Antwort nie mehr als einmal an.
  2. Sorgen Sie dafür, dass die Herausforderung kryptografisch sicher ist. Eine Herausforderung sollte praktisch unmöglich zu erraten sein. Wenn Sie serverseitig eine kryptografisch sichere Herausforderung erstellen möchten, sollten Sie eine FIDO-serverseitige Bibliothek verwenden, der Sie vertrauen. Wenn Sie stattdessen Ihre eigenen Herausforderungen erstellen, nutzen Sie die integrierte kryptografische Funktion Ihres Technologie-Stacks oder suchen Sie nach Bibliotheken, die für kryptografische Anwendungsfälle entwickelt wurden. Beispiele hierfür sind iso-crypto in Node.js oder secrets in Python. Gemäß Spezifikation muss die Aufgabe mindestens 16 Byte lang sein, um als sicher zu gelten.

Nachdem du eine Identitätsbestätigung erstellt hast, speichere sie in der Nutzersitzung, um sie später zu bestätigen.

Optionen für Anmeldedatenanfrage erstellen

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

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

publicKeyCredentialRequestOptions sollte alle für die Passkey-Authentifizierung erforderlichen Informationen enthalten. Übergeben Sie diese Informationen an die Funktion in Ihrer serverseitigen FIDO-Bibliothek, die für das Erstellen des publicKeyCredentialRequestOptions-Objekts verantwortlich ist.

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

  • rpId: Mit welcher RP-ID soll das Ausweisdokument verknüpft werden, z. B. example.com. Die Authentifizierung ist nur erfolgreich, wenn die hier angegebene RP-ID mit der mit den Anmeldedaten verknüpften RP-ID übereinstimmt. Verwenden Sie zum Ausfüllen der RP-ID denselben Wert wie die RP-ID, die Sie bei der Registrierung des Berechtigungsnachweises in publicKeyCredentialCreationOptions festgelegt haben.
  • challenge: Daten, die vom Passkey-Anbieter signiert werden, um zu belegen, dass der Nutzer den Passkey zum Zeitpunkt der Authentifizierungsanfrage hat. Sieh dir die Details unter Wettkampf erstellen an.
  • allowCredentials: Array mit zulässigen Anmeldedaten für diese Authentifizierung. Übergeben Sie ein leeres Array, damit der Nutzer einen verfügbaren Passkey aus einer im Browser angezeigten Liste auswählen kann. Weitere Informationen finden Sie in den Artikeln Abfrage vom RP-Server abrufen und Ausführliche Informationen zu erkennbaren Anmeldedaten.
  • userVerification: Gibt an, ob die Nutzerbestätigung über die Displaysperre des Geräts „erforderlich“, „bevorzugt“ oder „nicht empfohlen“ ist. Weitere Informationen finden Sie unter Abfrage vom RP-Server abrufen.
  • timeout: Dauer in Millisekunden für die Authentifizierung des Nutzers. Sie sollte angemessen großzügig und kürzer als die Lebensdauer von challenge sein. Der empfohlene Standardwert ist 5 Minuten, Sie können ihn aber auf bis zu 10 Minuten erhöhen, was noch immer innerhalb des empfohlenen Bereichs liegt. Lange Zeitüberschreitungen sind sinnvoll, wenn Sie davon ausgehen, dass Nutzer den hybriden Workflow verwenden, was normalerweise etwas länger dauert. Wenn das Zeitlimit für den Vorgang überschritten wird, wird ein NotAllowedError ausgegeben.

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

publicKeyCredentialCreationOptions, die vom Server gesendet werden.
Vom Server gesendete Optionen. Die challenge-Decodierung erfolgt clientseitig.

Beispielcode: Optionen für Anmeldedatenanfrage erstellen

Wir verwenden in unseren Beispielen die SimpleWebAuthn-Bibliothek. Hier übergeben Sie das Erstellen von Optionen für Anmeldedatenanfragen an die Funktion generateAuthenticationOptions.

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 überprüfen und anmelden

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

PublicKeyCredential-Objekt, das vom Server gesendet wird
navigator.credentials.get gibt einen PublicKeyCredential zurück.

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

  • response.authenticatorDataundresponse.clientDataJSON, wie im Schritt zur Passkey-Registrierung.
  • response.signature, die eine Signatur über diese Werte enthält.

Senden Sie das PublicKeyCredential-Objekt an den Server.

Gehen Sie auf dem Server so vor:

Datenbankschema
Vorgeschlagenes Datenbankschema. Weitere Informationen zu diesem Design finden Sie unter Serverseitige Passkey-Registrierung.
  • Erfassen Sie Informationen, die Sie zur Überprüfung der Assertion und zur Authentifizierung des Nutzers benötigen:
    • Rufen Sie die erwartete Identitätsbestätigung ab, die Sie in der Sitzung gespeichert haben, als Sie die Authentifizierungsoptionen generiert haben.
    • Rufe den voraussichtlichen Ursprung und die RP-ID ab.
    • Ermitteln Sie in Ihrer Datenbank, wer der Nutzer ist. Im Fall von auffindbaren Anmeldedaten wissen Sie nicht, wer der Nutzer ist, der eine Authentifizierungsanfrage stellt. Dafür haben Sie zwei Möglichkeiten:
      • Option 1: Sie verwenden die response.userHandle im PublicKeyCredential-Objekt. Suchen Sie in der Tabelle Nutzer nach der passkey_user_id, die mit userHandle übereinstimmt.
      • Option 2: Du verwendest die Anmeldedaten id im PublicKeyCredential-Objekt. Suchen Sie in der Tabelle Anmeldedaten für öffentlichen Schlüssel nach dem Berechtigungsnachweis id, der mit den im Objekt PublicKeyCredential vorhandenen Anmeldedaten id übereinstimmt. Suchen Sie dann mit dem Fremdschlüssel passkey_user_id in der Tabelle Users nach dem entsprechenden Nutzer.
    • Suchen Sie in Ihrer Datenbank die Anmeldedaten des öffentlichen Schlüssels, die mit der erhaltenen Authentifizierungsbestätigung übereinstimmen. Suchen Sie dazu in der Tabelle Anmeldedaten für öffentlichen Schlüssel nach den Anmeldedaten id, die mit den im Objekt PublicKeyCredential vorhandenen idAnmeldedaten übereinstimmen.
  • Prüfen Sie die Authentifizierungsbestätigung. Übergeben Sie diesen Verifizierungsschritt an Ihre serverseitige FIDO-Bibliothek, die in der Regel eine Dienstfunktion für diesen Zweck bietet. SimpleWebAuthn bietet z. B. verifyAuthenticationResponse. Weitere Informationen finden Sie im Anhang: Überprüfung der Authentifizierungsantwort.

  • Lösche die Identitätsbestätigung, unabhängig davon, ob die Überprüfung erfolgreich war oder nicht, um Replay-Angriffe zu verhindern.

  • Melden Sie den Nutzer an. Wenn die Bestätigung erfolgreich war, aktualisieren Sie die Sitzungsinformationen, um den Nutzer als angemeldet zu kennzeichnen. Sie können auch ein user-Objekt an den Client zurückgeben, damit das Front-End Informationen verwenden kann, die mit dem neu angemeldeten Nutzer verknüpft sind.

Beispielcode: Nutzer verifizieren und anmelden

Wir verwenden in unseren Beispielen die SimpleWebAuthn-Bibliothek. Hier übergeben Sie 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 Authentifizierungsantwort wird folgendermaßen geprüft:

  • Stellen Sie sicher, dass die RP-ID Ihrer Website entspricht.
  • Prüfen Sie, ob der Ursprung der Anfrage mit dem Ursprung Ihrer Website übereinstimmt. Lesen Sie für Android-Apps den Artikel Herkunft bestätigen.
  • Prüfen Sie, ob das Gerät die von Ihnen gestellte Aufgabe erfüllen konnte.
  • Überprüfe, ob der Nutzer bei der Authentifizierung die von dir als RP festgelegten Anforderungen erfüllt hat. Wenn Sie eine Nutzerbestätigung anfordern, achten Sie darauf, dass das Flag uv (vom Nutzer bestätigt) in authenticatorData auf true gesetzt ist. Prüfen Sie, ob das Flag up (Nutzer vorhanden) in authenticatorData auf true gesetzt ist, da die Anwesenheit der Nutzer für Passkeys immer erforderlich ist.
  • Prüfen Sie die Signatur. Zum Prüfen der Signatur benötigen Sie Folgendes:
    • Die Signatur, also die signierte Aufgabe: response.signature
    • Der öffentliche Schlüssel, mit dem die Signatur verifiziert werden soll.
    • Die ursprünglich signierten Daten. Das sind die Daten, deren Signatur verifiziert werden soll.
    • Der kryptografische Algorithmus, mit dem die Signatur erstellt wurde.

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