Passkey-Verwaltung

In diesem Leitfaden wird beschrieben, wie Sie Nutzern ermöglichen, ihre registrierten Passkeys anzusehen, umzubenennen und zu löschen. Gleichzeitig werden gespeicherte Anmeldedaten und Nutzerprofildetails zwischen dem Server und den Passwortmanagern des Nutzers mithilfe der Signal API synchronisiert.

Serverseitige Vorgänge

Ihre Backend-Datenbankebene und -Endpunkte MÜSSEN gängige CRUD-Aktionen für registrierte Anmeldedaten sowie die Aktualisierung von Nutzerprofildetails unterstützen. Der Server ist von frameworkspezifischen Bibliotheken entkoppelt und stellt Endpunkte für Folgendes bereit:

  1. Alle Nutzeranmeldedaten auflisten: Alle StoredPasskeyCredential-Einträge abrufen, die mit der ID des angemeldeten Nutzers übereinstimmen.
  2. Anmeldedatennamen aktualisieren: Akzeptieren Sie einen neuen benutzerdefinierten Alias für eine bestimmte Anmeldedaten-ID und speichern Sie die Aktualisierung auf dem Server.
  3. Anmeldedaten löschen: Entfernen Sie eine bestimmte Anmeldedaten-ID aus der Datenbank.
  4. Nutzerkontodetails aktualisieren: Akzeptieren Sie die aktualisierten Werte für name (Nutzername) und displayName für das Profil des angemeldeten Nutzers, damit der Client sie mit dem Passkey-Anbieter synchronisieren kann.
// Node.js routing example for credential CRUD and user profile rename
router.get('/api/credentials', checkUserAuthenticated, async (req, res) => {
  const list = await db.findCredentialsByUserId(req.user.id);
  return res.json(list);
});

router.put('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
  const { id } = req.params;
  const { name } = req.body;
  const cred = await db.findCredentialById(id);
  if (!cred || cred.passkeyUserId !== req.user.id) {
    return res.status(404).json({ error: 'Credential not found.' });
  }
  cred.name = name;
  await db.saveCredential(cred);
  return res.json(cred);
});

router.delete('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
  const { id } = req.params;
  const cred = await db.findCredentialById(id);
  if (!cred || cred.passkeyUserId !== req.user.id) {
    return res.status(404).json({ error: 'Credential not found.' });
  }
  await db.deleteCredential(id);
  return res.json({ success: true });
});

router.put('/api/user', checkUserAuthenticated, async (req, res) => {
  const { name, displayName } = req.body;
  const updatedUser = await db.updateUser(req.user.id, { name, displayName });
  return res.json(updatedUser);
});

Clientseitige Verwaltungsoberfläche

Rendern Sie einen speziellen Bereich für Einstellungen, in dem Nutzer ihre registrierten Authentifizierungsoptionen und Kontoprofildetails einfach prüfen und verwalten können:

  1. Gespeicherte Liste anzeigen: Ruft die Liste von deinem Endpunkt ab und rendert einzelne Zeilen mit Anmeldedaten. Wenn die Antwort leer ist, rendern Sie eine hilfreiche Meldung, z.B. „Keine Passkeys gefunden“.
  2. AAGUID-Metadaten zuordnen: Suchen Sie für jeden Passkey die zugehörige aaguid-Eigenschaft in Ihrer lokalen Registry, um die Anbieterdetails zu rendern. Weitere Informationen finden Sie im Abschnitt Passkey-Anbieter anhand der AAGUID ermitteln.
  3. Anforderungen an die Benutzeroberfläche für einzelne Elemente: Jede Zeile im Listencontainer MUSS Folgendes enthalten:
    • Anbietersymbol: Von der AAGUID abgeleitetes Bild oder Daten-URI.
    • Anbieter/Benutzerdefinierter Name: Von der AAGUID abgeleiteter Name oder vom Nutzer umbenannter String.
    • Registrierungsdatum: Der in der Datenbank gespeicherte Rohzeitstempel registeredAt, der für die Anzeige beim Kunden in ein menschenlesbares Datum formatiert wurde.
    • Datum der letzten Verwendung: Der in der Datenbank gespeicherte Roh-Zeitstempel lastUsedAt, der für die Anzeige im Client in ein menschenlesbares Datum (falls vorhanden) formatiert wurde.
    • Schaltfläche „Umbenennen“: Löst eine Umbenennungsaktion aus, um den benutzerdefinierten Alias der Anmeldedaten auf dem Server (PUT /api/credential/:id) zu aktualisieren. Hinweis: Das Umbenennen des Alias eines einzelnen Passkeys ist eine serverseitige Labeländerung und ruft signalCurrentUserDetails() NICHT auf.
    • Schaltfläche „Löschen“: Löst das Löschen des Ausweises (DELETE /api/credential/:id) aus und synchronisiert die verbleibenden Ausweis-IDs über signalAllAcceptedCredentials().
  4. Benutzeroberfläche zum Umbenennen von Nutzerkonten:
    • Stellen Sie ein Steuerelement bereit, mit dem der Nutzer den Nutzernamen seines Kontos (name) oder displayName (PUT /api/user) aktualisieren kann, und synchronisieren Sie die aktualisierten Nutzerdetails sofort über PublicKeyCredential.signalCurrentUserDetails() mit dem Passwortmanager.
  5. Bedingte Schaltfläche „Passkey erstellen“:
    • Bieten Sie auf der Verwaltungsseite eine gut sichtbare Schaltfläche zum Erstellen eines Passkeys an. Bevor dieses UI-Element gerendert wird, MUSS die Seite die Funktionen mit PublicKeyCredential.getClientCapabilities() erkennen, um zu prüfen, ob der Plattform-Authenticator unterstützt wird. Wenn Passkeys nicht unterstützt werden, blenden Sie diese Schaltfläche aus und empfehlen Sie stattdessen die Standard-MFA-Registrierung.
    • Registrierung eines Sicherheitsschlüssels zulassen, indem authenticatorSelection.authenticatorAttachment beim Aufruf von navigator.credentials.create() weggelassen wird.

Signal API-Synchronisierung

Mit der Signal API kann die Anwendung den Status von Anmeldedaten an Passwortmanager senden, sodass die synchronisierten Vaults des Nutzers und Ihre Backend-Datenbank immer auf dem gleichen Stand sind.

  • Regel für die Featureerkennung und Parametercodierung:
    • Prüfen Sie immer, ob die Signal API-Methoden unterstützt werden, bevor Sie sie aufrufen (if (PublicKeyCredential.signalAllAcceptedCredentials) und if (PublicKeyCredential.signalCurrentUserDetails)).
    • Alle userId- und Anmeldedaten-ID-Parameter, die an Signal API-Methoden (signalAllAcceptedCredentials, signalCurrentUserDetails) übergeben werden, MÜSSEN Base64URL-codierte Strings sein.
  • Synchronisierung beim Seitenaufbau initiieren:
    • Die Anwendung MUSS signalAllAcceptedCredentials() automatisch aufrufen, wenn die Verwaltungsseite geladen wird (z. B. in einem DOMContentLoaded-Event-Listener, bei der Modulinitialisierung oder in einem Framework-Komponenten-Mount-Hook wie useEffect).
  • Synchronisierung von Verwaltungsupdates:
    • Die Anwendung MUSS signalAllAcceptedCredentials() sofort im Click-Handler zum Löschen von Anmeldedaten aufrufen, nachdem die Anmeldedaten abgerufen wurden.
    • Die Anwendung MUSS signalCurrentUserDetails() sofort im Click-Handler für die Umbenennung des Nutzerkontos (name oder displayName) nach dem Abruf aufrufen.
// Client-side management synchronization ES module
import { listFetch, renameCredentialFetch, updateUserFetch, deleteFetch } from './api.js';

// Base64URL-encoded User ID string (illustration only)
const base64UrlUserId = "M2YPl-KGnA8";

async function syncAcceptedCredentials(currentCredentialsList) {
  if (!window.PublicKeyCredential || !PublicKeyCredential.signalAllAcceptedCredentials) return;
  try {
    const credentialIds = currentCredentialsList.map(c => c.id); // Array of Base64URL credential ID strings
    
    await PublicKeyCredential.signalAllAcceptedCredentials({
      rpId, // RP ID must match the one defined on the server
      userId: base64UrlUserId, // User ID Base64URL-encoded string
      allAcceptedCredentialIds: credentialIds
    });
  } catch (e) {
    console.error('SignalAllAcceptedCredentials sync failure:', e);
  }
}

async function loadManagementPanel() {
  const response = await listFetch();
  const list = await response.json();
  
  renderUI(list);
  // Sync accepted credentials on initial load
  await syncAcceptedCredentials(list);
}

async function performDelete(credentialId) {
  const response = await deleteFetch(credentialId);
  if (response.ok) {
    const updatedResponse = await listFetch();
    const updatedList = await updatedResponse.json();
    
    renderUI(updatedList);
    // Sync remaining accepted credentials after deletion
    await syncAcceptedCredentials(updatedList);
  }
}

// Renaming a passkey's nickname updates the server record only
async function performCredentialRename(credentialId, newCredentialName) {
  const response = await renameCredentialFetch(credentialId, { name: newCredentialName });
  if (response.ok) {
    const updatedResponse = await listFetch();
    renderUI(await updatedResponse.json());
  }
}

// Renaming the user's account username or displayName triggers signalCurrentUserDetails
async function performUserRename(rpId, userId, updatedName, updatedDisplayName) {
  const response = await updateUserFetch({ name: updatedName, displayName: updatedDisplayName });
  if (response.ok && window.PublicKeyCredential && PublicKeyCredential.signalCurrentUserDetails) {
    try {
      await PublicKeyCredential.signalCurrentUserDetails({
        rpId, // RP ID must match the one defined on the server
        userId, // Base64URL-encoded user ID
        name: updatedName, // Updated username
        displayName: updatedDisplayName // Updated display name
      });
    } catch (e) {
      console.error('SignalCurrentUserDetails sync failure:', e);
    }
  }
}

window.addEventListener('DOMContentLoaded', loadManagementPanel);

Passkey-Anbieter anhand der AAGUID ermitteln

Eine AAGUID (Authenticator Attestation Globally Unique Identifier) ist eine 128-Bit-Kennung, die das Modell des Authentifikators darstellt, nicht eine bestimmte Instanz. Sie ist bei der Passkey-Registrierung in den Authenticator-Daten enthalten und kann verwendet werden, um zu bestimmen, welcher Passkey-Anbieter (z.B. Google Passwortmanager, iCloud-Schlüsselbund, 1Password) die Anmeldedaten erstellt hat.

Die AAGUID sollte nur verwendet werden, um Nutzern bei der Verwaltung von Passkeys zu helfen. Sie kann geändert werden, sofern sie nicht kryptografisch bestätigt ist. Das wird von Plattform-Passkeys derzeit nicht unterstützt.

1. AAGUID-Registry

Eine von der Community gepflegte JSON-Zuordnung von AAGUIDs zu Anbieternamen und ‑symbolen ist hier verfügbar:

https://raw.githubusercontent.com/passkeydeveloper/passkey-authenticator-aaguids/refs/heads/main/combined_aaguid.json

Jeder Eintrag hat das folgende Schema:

{
  "<aaguid-uuid>": {
    "name": "Provider Name",
    "icon_light": "data:image/png;base64,...",
    "icon_dark": "data:image/png;base64,..."
  }
}

2. AAGUID nach der Registrierung verwenden

Nachdem Sie eine Registrierungsantwort bestätigt haben, lesen Sie die aaguid aus dem Registrierungsergebnis und suchen Sie sie in der Registry, um die name und providerIcon der Anmeldedaten zu füllen:

Prüfen Sie vor dem Suchen der AAGUID in der Registry, ob sie '00000000-0000-0000-0000-000000000000' entspricht. Wenn ja, überspringen Sie die Registrierungssuche und legen Sie name auf einen Fallback fest (z.B. Gerätename aus dem User-Agent oder „Unbekannter Passkey-Anbieter“) und providerIcon auf undefined. Suchen Sie nur nach AAGUIDs, die nicht null sind.

import aaguids from './aaguids.json' with { type: 'json' };

const { aaguid } = registrationInfo;
if (aaguid === '00000000-0000-0000-0000-000000000000') {
  // use the device name as the passkey provider based on
  // the information derived from the user agent string,
  // or just say "Unknown passkey provider"
} else {
  const provider = aaguids[aaguid];
  const credential = {
    // ...other fields
    aaguid,
    name: provider?.name || 'Unknown passkey provider',
    providerIcon: provider?.icon_light,
  };
}

Fallback-Strategien

Fallback für die Passkey-Funktionserkennung

Installieren Sie immer „webauthn-polyfills“ und importieren Sie es in den Kontext. Solange PublicKeyCredential unterstützt wird, wird auch PublicKeyCredential.getClientCapabilities unterstützt.

import 'webauthn-polyfills';

Signal API-Synchronisierungs-Fallback

Wenn der Browser PublicKeyCredential.parseRequestOptionsFromJSON nicht unterstützt, verwenden Sie die „webauthn-polyfills“:

<script type="module"> 
  if (!PublicKeyCredential.parseRequestOptionsFromJSON) { 
     await import('https://unpkg.com/webauthn-polyfills'); 
   } 
 </script> 

Dadurch wird auch die Unterstützung für PublicKeyCredential.prototype.toJSON hinzugefügt.