Beheer van toegangssleutels

In deze handleiding wordt uitgelegd hoe je gebruikers hun geregistreerde toegangssleutels kunt laten bekijken, hernoemen en verwijderen, terwijl opgeslagen inloggegevens en gebruikersprofielgegevens gesynchroniseerd blijven tussen de server en de wachtwoordmanagers van de gebruiker via de Signal API.

Bewerkingen aan de serverzijde

Je backend-databaselaag en -eindpunten MOETEN algemene CRUD-acties ondersteunen voor geregistreerde inloggegevens en voor het updaten van gebruikersprofielgegevens. De server is losgekoppeld van frameworkspecifieke bibliotheken en biedt eindpunten voor het volgende:

  1. Alle inloggegevens van de gebruiker vermelden: Haal alle StoredPasskeyCredential-records op die overeenkomen met de ID van de ingelogde gebruiker.
  2. Namen van inloggegevens updaten: Accepteer een nieuwe aangepaste bijnaam voor een specifieke ID voor inloggegevens en bewaar de update op de server.
  3. Inloggegevens verwijderen: Verwijder een specifieke ID voor inloggegevens uit de database.
  4. Gebruikersaccountgegevens updaten: Accepteer geüpdatete waarden voor name (gebruikersnaam) en displayName voor het profiel van de ingelogde gebruiker, zodat de client deze kan synchroniseren met de toegangssleutelprovider.
// 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);
});

Beheer-UI aan de clientzijde

Maak een speciaal instellingenvenster waarin gebruikers hun geregistreerde verificatieopties en accountprofielgegevens makkelijk kunnen controleren en beheren:

  1. Opgeslagen lijst tonen: Haal de lijst op van je eindpunt en geef afzonderlijke rijen met inloggegevens weer. Als de reactie leeg is, geef je een nuttig bericht voor een lege status weer (bijvoorbeeld 'Geen toegangssleutels gevonden').
  2. AAGUID-metadata toewijzen: Zoek voor elke toegangssleutel de eigenschap aaguid op in je lokale register om de providergegevens weer te geven. Ga naar het gedeelte De toegangssleutelprovider bepalen op basis van de AAGUID voor meer informatie.
  3. UI-vereisten per item: Elke rij in de lijstcontainer MOET het volgende weergeven:
    • Icoon van aanbieder: Afbeelding of gegevens-URI afgeleid van AAGUID.
    • Provider/aangepaste naam: Een van de AAGUID afgeleide naam of een door de gebruiker hernoemde tekenreeks.
    • Registratiedatum: Het onbewerkte epoch-tijdstempel registeredAt dat in de database is opgeslagen, is opgemaakt als een door mensen leesbare datum voor weergave aan de klant.
    • Datum van laatste gebruik: De onbewerkte epoch-tijdstempel lastUsedAt die in de database is opgeslagen, opgemaakt als een door mensen leesbare datum (indien aanwezig) voor weergave aan de klant.
    • Knop Naam wijzigen: Triggert een actie om de naam te wijzigen om de aangepaste bijnaam van de inloggegevens op de server te updaten (PUT /api/credential/:id). Opmerking: De bijnaam van een individuele toegangssleutel wijzigen is een labelwijziging aan de serverzijde en roept signalCurrentUserDetails() NIET aan.
    • Knop Verwijderen: Hiermee wordt de verwijdering van de inloggegevens (DELETE /api/credential/:id) geactiveerd en worden de resterende inloggegevens-ID's gesynchroniseerd via signalAllAcceptedCredentials().
  4. UI voor naamswijziging van gebruikersaccount:
    • Bied een bedieningselement waarmee de gebruiker de gebruikersnaam van het account (name) of displayName (PUT /api/user) kan updaten en synchroniseer de geüpdatete gebruikersgegevens onmiddellijk met de wachtwoordmanager via PublicKeyCredential.signalCurrentUserDetails().
  5. Voorwaardelijke knop Toegangssleutel maken:
    • Bied een prominente registratietriggerknop Toegangssleutel maken aan op de beheerpagina. Voordat dit UI-element wordt gerenderd, MOET de pagina functies detecteren met PublicKeyCredential.getClientCapabilities() om te verifiëren dat de platformverificator wordt ondersteund. Als toegangssleutels niet worden ondersteund, verberg je deze knop en moedig je gebruikers aan om zich in te schrijven voor standaard MFA.
    • Registratie van een beveiligingssleutel toestaan door authenticatorSelection.authenticatorAttachment weg te laten bij de aanroep van navigator.credentials.create().

Synchronisatie van Signal API

Met de Signal API kan de app de status van inloggegevens doorgeven aan wachtwoordmanagers, zodat de gesynchroniseerde kluizen van de gebruiker en je backend-database synchroon blijven.

  • Regel voor functieherkenning en parametercodering:
    • Voer altijd een functieherkenning uit voor Signal API-methoden voordat u ze aanroept (if (PublicKeyCredential.signalAllAcceptedCredentials) en if (PublicKeyCredential.signalCurrentUserDetails)).
    • Alle parameters voor userId en inloggegevens-ID die worden doorgegeven aan Signal API-methoden (signalAllAcceptedCredentials, signalCurrentUserDetails) MOETEN tekenreeksen met Base64URL-codering zijn.
  • Synchronisatie van laden van pagina starten:
    • De app MOET signalAllAcceptedCredentials() automatisch aanroepen als de beheerpagina wordt geladen (bijvoorbeeld in een DOMContentLoaded-gebeurtenislistener, module-initialisatie of frameworkcomponent-mount-hook zoals useEffect).
  • Synchronisatie van beheerupdates:
    • De app MOET signalAllAcceptedCredentials() onmiddellijk aanroepen in de click handler voor het verwijderen van inloggegevens na het ophalen.
    • De app MOET signalCurrentUserDetails() onmiddellijk aanroepen binnen de clickhandler voor het wijzigen van de naam van je gebruikersaccount (name of displayName) na het ophalen.
// 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);

De toegangssleutelprovider bepalen op basis van de AAGUID

Een AAGUID (Authenticator Attestation Globally Unique Identifier) is een 128-bits ID die het model van de authenticator vertegenwoordigt, niet een specifieke instantie. Deze wordt opgenomen in de verificatorgegevens tijdens de registratie van de toegangssleutel en kan worden gebruikt om te bepalen welke toegangssleutelprovider (bijvoorbeeld Google Wachtwoordmanager, iCloud-sleutelhanger, 1Password) een inloggegeven heeft gemaakt.

De AAGUID mag alleen worden gebruikt om gebruikers te helpen met het beheer van toegangssleutels. De naam kan worden aangepast, tenzij deze cryptografisch is geattesteerd. Dit wordt op dit moment niet ondersteund door platformtoegangssleutels.

1. AAGUID-register

Een door de community onderhouden json-toewijzing van AAGUID's aan providernamen en -iconen is beschikbaar op:

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

Elke invoer heeft het volgende schema:

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

2. AAGUID gebruiken na registratie

Nadat je een registratiereactie hebt geverifieerd, lees je de aaguid uit het registratieresultaat en zoek je deze op in het register om de name en providerIcon van de inloggegevens in te vullen:

Voordat je de AAGUID opzoekt in het register, controleer je of deze gelijk is aan '00000000-0000-0000-0000-000000000000'. Als dat het geval is, sla je de registerzoekopdracht over en stel je name in op een fallback (bijv. de apparaatnaam van de user-agent of Onbekende toegangssleutelprovider) en providerIcon op undefined. Zoek alleen het register op voor niet-nul-AAGUID's.

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,
  };
}

Reservestrategieën

Terugvaloptie voor detectie van toegangssleutelfunctie

Installeer altijd 'webauthn-polyfills' en importeer dit in de context. Zolang PublicKeyCredential wordt ondersteund, wordt PublicKeyCredential.getClientCapabilities ook ondersteund.

import 'webauthn-polyfills';

Terugvaloptie voor synchronisatie van Signal API

Als de browser PublicKeyCredential.parseRequestOptionsFromJSON niet ondersteunt, gebruik je de webauthn-polyfills:

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

Hiermee wordt ook ondersteuning voor PublicKeyCredential.prototype.toJSON toegevoegd.