Gestion des clés d'accès

Ce guide explique comment permettre aux utilisateurs d'afficher, de renommer et de supprimer leurs clés d'accès enregistrées, tout en synchronisant les identifiants enregistrés et les détails du profil utilisateur entre le serveur et les gestionnaires de mots de passe de l'utilisateur à l'aide de l'API Signal.

Opérations côté serveur

La couche de base de données et les points de terminaison de votre backend DOIVENT accepter les actions CRUD courantes pour les identifiants enregistrés, ainsi que la mise à jour des détails du profil utilisateur. Dissocié des bibliothèques spécifiques au framework, le serveur expose des points de terminaison pour :

  1. Lister tous les identifiants utilisateur : récupérez tous les enregistrements StoredPasskeyCredential correspondant à l'ID de l'utilisateur connecté.
  2. Mettre à jour les noms des identifiants : accepter un nouveau pseudo personnalisé pour un ID d'identifiant spécifique et conserver la mise à jour sur le serveur.
  3. Supprimer des identifiants : supprimez un ID d'identifiant spécifique de la base de données.
  4. Mettre à jour les détails du compte utilisateur : acceptez les valeurs name (nom d'utilisateur) et displayName mises à jour pour le profil de l'utilisateur connecté afin que le client puisse les synchroniser avec le fournisseur de clés d'accès.
// 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);
});

UI de gestion côté client

Affichez un panneau de paramètres dédié permettant aux utilisateurs d'auditer et de gérer facilement leurs options d'authentification enregistrées et les détails de leur profil de compte :

  1. Afficher la liste enregistrée : récupérez la liste à partir de votre point de terminaison et affichez les lignes d'identifiants individuelles. Si la réponse est vide, affichez un message d'état vide utile (par exemple, "Aucune clé d'accès trouvée").
  2. Mapper les métadonnées AAGUID : pour chaque clé d'accès, recherchez sa propriété aaguid dans votre registre local pour afficher les détails de son fournisseur. Pour en savoir plus, consultez la section Déterminer le fournisseur de clé d'accès à partir de l'AAGUID.
  3. Exigences d'UI par élément : chaque ligne du conteneur de liste DOIT afficher :
    • Icône du fournisseur : image ou URI de données dérivé de l'AAGUID.
    • Nom du fournisseur/personnalisé : nom dérivé de l'AAGUID ou chaîne renommée par l'utilisateur.
    • Registration Date (Date d'enregistrement) : code temporel brut registeredAt persistant dans la base de données, formaté en date lisible par l'humain pour l'affichage client.
    • Date de dernière utilisation : code temporel brut de l'epoch lastUsedAt conservé dans la base de données, mis en forme en tant que date lisible par l'humain (si présent) pour l'affichage client.
    • Bouton "Renommer" : déclenche une action de changement de nom pour mettre à jour le pseudo personnalisé de l'identifiant sur le serveur (PUT /api/credential/:id). Remarque : Renommer le pseudo d'une clé d'accès individuelle est une modification de libellé côté serveur et n'appelle PAS signalCurrentUserDetails().
    • Bouton "Supprimer" : déclenche la suppression de l'identifiant (DELETE /api/credential/:id) et synchronise les ID d'identifiants restants via signalAllAcceptedCredentials().
  4. UI de changement de nom de compte utilisateur :
    • Fournissez une commande permettant à l'utilisateur de modifier le nom d'utilisateur de son compte (name) ou displayName (PUT /api/user), et synchronisez immédiatement les informations utilisateur mises à jour avec le gestionnaire de mots de passe via PublicKeyCredential.signalCurrentUserDetails().
  5. Bouton "Créer une clé d'accès" conditionnel :
    • Proposez un bouton d'enregistrement "Créer une clé d'accès" bien visible sur la page de gestion. Avant d'afficher cet élément d'interface utilisateur, la page DOIT détecter les fonctionnalités à l'aide de PublicKeyCredential.getClientCapabilities() pour vérifier que l'authentificateur de la plate-forme est compatible. Si les clés d'accès ne sont pas acceptées, masquez ce bouton et encouragez plutôt les inscriptions MFA standards.
    • Autorisez l'enregistrement d'une clé de sécurité en omettant authenticatorSelection.authenticatorAttachment lors de l'appel navigator.credentials.create().

Synchronisation de l'API Signal

L'API Signal permet à l'application de communiquer l'état des identifiants aux gestionnaires de mots de passe, ce qui permet de synchroniser les coffres-forts de l'utilisateur et votre base de données backend.

  • Règle de détection des caractéristiques et d'encodage des paramètres :
    • Détectez toujours les méthodes de l'API Signal avant de les appeler (if (PublicKeyCredential.signalAllAcceptedCredentials) et if (PublicKeyCredential.signalCurrentUserDetails)).
    • Tous les paramètres userId et d'ID d'identifiant transmis aux méthodes de l'API Signal (signalAllAcceptedCredentials, signalCurrentUserDetails) DOIVENT être des chaînes encodées en Base64URL.
  • Lancement de la synchronisation du chargement de page :
    • L'application DOIT appeler signalAllAcceptedCredentials() automatiquement lorsque la page de gestion se charge (par exemple, dans un écouteur d'événements DOMContentLoaded, l'initialisation du module ou un hook de montage de composant de framework tel que useEffect).
  • Synchronisation des mises à jour de gestion :
    • L'application DOIT appeler signalAllAcceptedCredentials() immédiatement dans le gestionnaire de clics de suppression d'identifiants après la récupération.
    • L'application DOIT appeler signalCurrentUserDetails() immédiatement dans le gestionnaire de clics de changement de nom de votre compte utilisateur (name ou displayName) après la récupération.
// 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);

Déterminer le fournisseur de clés d'accès à partir de l'AAGUID

Un AAGUID (Authenticator Attestation Globally Unique Identifier) est un identifiant de 128 bits qui représente le modèle de l'authentificateur, et non une instance spécifique. Il est inclus dans les données de l'authentificateur lors de l'enregistrement de la clé d'accès et peut être utilisé pour déterminer quel fournisseur de clés d'accès (par exemple, le Gestionnaire de mots de passe de Google, le trousseau iCloud ou 1Password) a créé un identifiant.

L'AAGUID ne doit être utilisé que pour aider les utilisateurs à gérer leurs clés d'accès. Il peut être modifié, sauf s'il est attesté par chiffrement, ce que les clés d'accès de plate-forme ne permettent pas actuellement.

1. Registre des AAGUID

Un mappage JSON géré par la communauté des AAGUID vers les noms et icônes des fournisseurs est disponible à l'adresse suivante :

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

Chaque entrée possède le schéma suivant :

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

2. Utiliser l'AAGUID après l'enregistrement

Après avoir validé une réponse d'enregistrement, lisez le aaguid à partir du résultat de l'enregistrement et recherchez-le dans le registre pour renseigner les champs name et providerIcon de l'identifiant :

Avant de rechercher l'AAGUID dans le registre, vérifiez s'il est égal à '00000000-0000-0000-0000-000000000000'. Si c'est le cas, ignorez la recherche dans le registre et définissez name sur une valeur de remplacement (par exemple, le nom de l'appareil à partir de l'user-agent ou "Fournisseur de clé d'accès inconnu") et providerIcon sur undefined. Ne recherchez dans le registre que les AAGUID non nuls.

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

Stratégies de remplacement

Solution de secours pour la détection des fonctionnalités de clé d'accès

Installez toujours "webauthn-polyfills" et importez-le dans le contexte. Tant que PublicKeyCredential est compatible, PublicKeyCredential.getClientCapabilities l'est également.

import 'webauthn-polyfills';

Solution de secours pour la synchronisation de l'API Signal

Si le navigateur n'est pas compatible avec PublicKeyCredential.parseRequestOptionsFromJSON, utilisez les polyfills WebAuthn :

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

Cela ajoutera également la compatibilité avec PublicKeyCredential.prototype.toJSON.