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 :
- Lister tous les identifiants utilisateur : récupérez tous les enregistrements
StoredPasskeyCredentialcorrespondant à l'ID de l'utilisateur connecté. - 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.
- Supprimer des identifiants : supprimez un ID d'identifiant spécifique de la base de données.
- Mettre à jour les détails du compte utilisateur : acceptez les valeurs
name(nom d'utilisateur) etdisplayNamemises à 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 :
- 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").
- Mapper les métadonnées AAGUID : pour chaque clé d'accès, recherchez sa propriété
aaguiddans 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. - 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
registeredAtpersistant 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
lastUsedAtconservé 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 PASsignalCurrentUserDetails(). - Bouton "Supprimer" : déclenche la suppression de l'identifiant (
DELETE /api/credential/:id) et synchronise les ID d'identifiants restants viasignalAllAcceptedCredentials().
- UI de changement de nom de compte utilisateur :
- Fournissez une commande permettant à l'utilisateur de modifier le nom d'utilisateur de son compte (
name) oudisplayName(PUT /api/user), et synchronisez immédiatement les informations utilisateur mises à jour avec le gestionnaire de mots de passe viaPublicKeyCredential.signalCurrentUserDetails().
- Fournissez une commande permettant à l'utilisateur de modifier le nom d'utilisateur de son compte (
- 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.authenticatorAttachmentlors de l'appelnavigator.credentials.create().
- 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
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)etif (PublicKeyCredential.signalCurrentUserDetails)). - Tous les paramètres
userIdet d'ID d'identifiant transmis aux méthodes de l'API Signal (signalAllAcceptedCredentials,signalCurrentUserDetails) DOIVENT être des chaînes encodées en Base64URL.
- Détectez toujours les méthodes de l'API Signal avant de les appeler (
- 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énementsDOMContentLoaded, l'initialisation du module ou un hook de montage de composant de framework tel queuseEffect).
- L'application DOIT appeler
- 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 (nameoudisplayName) après la récupération.
- L'application DOIT appeler
// 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.